文章讲述的所有功能已经合并进入https://github.com/KSwordDEV/KSword/欢迎体验用户启动文件夹与公共启动文件夹的双重枚举Startup 文件夹中的目录项会在用户登录后由 Windows 外壳处理。枚举时要先用已知文件夹 ID 定位当前用户目录与公共目录再分别读取两个目录的一级条目。路径、属性、文件身份和签名结果需要分开保存。一、对象关系和完整流程系统处理流程从用户登录开始。Windows 建立用户会话并启动桌面外壳。桌面外壳通常是explorer.exe负责提供桌面、开始菜单并在相应阶段处理 Startup 目录项。用户登录 - Windows 建立用户会话 - 启动桌面外壳 - 处理当前用户 Startup 目录 - 处理公共 Startup 目录 - 按目录项类型继续处理枚举程序只观察目录内容。它不会触发目录项也不能证明目标程序已经运行。解析已知文件夹 ID - 得到实际绝对路径 - 枚举目录一级条目 - 记录名称和属性 - 打开普通文件 - 读取文件身份 - 保存范围、路径和错误状态1. Startup 文件夹是文件系统中的两个登录范围Startup 文件夹包含当前用户范围和公共范围。Windows 把每个用户的配置与文件放在各自的用户配置文件目录中同时提供由本机用户共享的公共目录。FOLDERID_Startup指向当前登录用户的启动文件夹。FOLDERID_CommonStartup指向公共启动文件夹。两者都表示文件系统目录。当前用户 Startup 目录的常见显示路径为%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup。%APPDATA%会因用户配置文件位置、文件夹重定向和企业策略而变化。公共 Startup 目录的常见显示路径为%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\StartUp它面向本机多个用户写入通常需要更高权限。登录范围决定记录含义。只出现在当前用户目录中的条目仅与该用户的登录环境有关。公共目录中的条目可能影响本机上多个交互式用户。读取结果要明确保存范围名称不能只给出路径文本。Windows 文件夹逻辑关系 当前登录用户 用户配置文件 AppData\Roaming\...\Programs\Startup FOLDERID_Startup 本机所有用户 ProgramData\...\Programs\StartUp FOLDERID_CommonStartup2. “已知文件夹”使用稳定的逻辑标识符目录位置要通过已知文件夹标识符获取。Windows 为常用目录分配了 GUIDGlobally Unique Identifier全局唯一标识符例如FOLDERID_Startup。系统结合用户配置、重定向规则和当前令牌解析标识符然后返回实际的绝对路径。已知文件夹 API 返回的路径指针带有明确的内存所有权。调用者拿到的PWSTR由 COM 任务分配器分配读取完文本后必须调用CoTaskMemFree。把这一指针直接交给delete[]、free或LocalFree都会破坏分配器配对关系。目录尚未创建也是一种有效状态。全新的用户配置文件、精简系统映像或管理员设置的重定向策略都可能让目标路径暂时不存在。读取程序要分别记录“路径解析成功”“路径存在”和“能够列出目录”。3. 外壳处理的是目录项目录项的种类并不相同目录项可能是文件、快捷方式或子目录。Startup 文件夹经常包含.lnk快捷方式也可以包含可执行文件、批处理文件、Internet 快捷方式或目录。目录项名称只表示文件系统中的条目。外壳怎样处理它还取决于条目类型、文件关联、快捷方式目标和登录环境。.lnk文件保存 Shell Link 数据其中可能包含目标路径、工作目录、参数和图标信息。Shell Link 是 Windows 外壳定义的快捷方式格式。枚举 Startup 目录时先把.lnk当作普通文件记录。需要分析目标时再通过 Shell Link 的 COM 接口解析。快捷方式文件名不能直接当成目标路径。文件夹内的子目录也应与直接文件分开保留。直接枚举可以完整回答“这个 Startup 文件夹当前有哪些一级条目”递归遍历回答的是另一个问题二者混在同一个列表里会掩盖真实的目录层级。4. 登录、Explorer 初始化与目录枚举有不同的时间边界目录枚举不代表执行记录。用户完成交互式登录后Windows 为该用户建立会话并启动桌面外壳。外壳随后在相应阶段处理 Startup 目录内容。目录枚举只能说明某个时刻有哪些一级条目无法证明外壳已经处理它们也无法证明目标程序成功运行。完整观察分为三层。解析已知文件夹 ID得到当前用户目录或公共目录的实际路径。读取一级目录项、属性和文件身份。按需要解析快捷方式目标、检查文件是否存在或结合进程与事件记录。每层都要保存自己的时间和错误状态。5. Shell Link、文件路径和文件对象身份需要分别记录快捷方式文件和链接目标是两个对象。Startup 目录中的.lnk文件是一个文件系统条目。链接目标是另一个对象可能不存在也可能已经被替换。快捷方式文件名、快捷方式完整路径、解析出的目标文本和目标文件身份需要分别记录。文件身份通常由卷序列号和文件索引组合表示。路径可能被改名、重解析或替换。文件索引可以在同一卷上辅助识别本次打开的文件对象。这个身份仍然与读取时间有关。文件被删除或替换后旧身份不能继续代表新路径。读取失败时要保留文件路径和GetLastError然后继续枚举其它条目。6. 文件查找句柄、文件句柄和 COM 分配内存的释放规则不同每类资源都有对应的释放 API。SHGetKnownFolderPath返回的PWSTR由 COM 任务分配器管理要用CoTaskMemFree释放。FindFirstFileW返回搜索句柄要用FindClose释放。CreateFileW返回普通HANDLE要用CloseHandle释放。三种释放函数不能互换。资源所有者应在同一段逻辑中保持清晰。路径文本复制到std::wstring后再释放PWSTR。目录遍历结束后关闭搜索句柄。每个成功打开的文件都在读取身份后关闭。某个条目访问失败时前一层资源仍能正常释放另一个 Startup 范围也能继续枚举。7. 一条目录项记录需要同时保存范围、名称、属性和读取结果每条结果都要能回到对应目录。记录至少包含范围标签、已知文件夹 API 返回的目录路径、相对名称、完整路径、目录属性、目录标志、文件身份读取状态和采样时间。范围标签说明条目来自当前用户目录还是公共目录。.lnk、.exe、.cmd、.bat或未知扩展名只表示名称或文件类型不能代替路径来源。目录可能在枚举期间变化。某个名称由FindFirstFileW返回后文件可能立即被删除后续CreateFileW就会失败。文件也可能在打开后被其它进程替换。输出应保留“目录项当时返回的名称”和“文件身份读取失败码”不能直接丢弃失败条目。当前用户目录和公共目录要独立读取。当前用户目录解析失败时继续解析公共目录。公共目录不可访问时仍然输出当前用户条目。每个错误都要保留对应的范围避免把一个范围的问题写成另一个范围的空目录。目录路径、目录项和文件身份要按同一采样时间组合输出。后续差异比较才能对应到明确范围。路径比较时可以用完整绝对路径作为展示键用文件身份辅助复核。重定向、重解析点、大小写显示和目录改名都可能改变路径文本。文件索引读取失败时要保留失败状态不能伪造身份字段。完整的状态字段可以分别解释两次枚举之间的新增、删除、替换和不可读条目。输出中的失败码要和目录范围一同保存。记录完成后再进行汇总展示避免丢失单个范围的错误状态。二、先定位两个不同范围的 Startup 文件夹1. 已知文件夹 ID 比拼接环境变量更稳定目录定位要区分当前用户与公共范围。FOLDERID_Startup对应当前用户的 Startup 文件夹常见位置为%APPDATA%\Microsoft\Windows\Start Menu\Programs\Startup。FOLDERID_CommonStartup对应公共 Startup 文件夹常见位置为%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\StartUp。实际路径要使用已知文件夹 API 获取。API 会根据当前用户配置、重定向设置和系统版本返回结果不需要手工替换%APPDATA%或%PROGRAMDATA%。// 意义释放 COM 任务分配器分配的内存。// 返回无。参数必须来自 CoTaskMemAlloc 或约定使用同一分配器的 API。voidCoTaskMemFree(LPVOID pv// 输入SHGetKnownFolderPath 成功返回的路径指针。);// 意义获取 folderId 指定的已知文件夹绝对路径。// 返回S_OK 表示成功。失败 HRESULT 表示已知文件夹不可用或访问状态异常。HRESULTSHGetKnownFolderPath(REFKNOWNFOLDERID rfid,// 输入FOLDERID_Startup、FOLDERID_CommonStartup 等 ID。DWORD dwFlags,// 输入KF_FLAG_DEFAULT 使用默认解析方式。HANDLE hToken,// 输入nullptr 表示当前用户令牌。PWSTR*ppszPath// 输出CoTaskMemAlloc 分配的宽字符串调用方必须 CoTaskMemFree。);正确做法复制 API 分配的文本再释放原始内存。输出指针来自 COM 任务分配器只能由CoTaskMemFree释放。PWSTR allocatedPathnullptr;constHRESULT resultSHGetKnownFolderPath(FOLDERID_Startup,KF_FLAG_DEFAULT,nullptr,allocatedPath);if(SUCCEEDED(result)allocatedPath!nullptr){std::wstringstartupPath(allocatedPath);CoTaskMemFree(allocatedPath);// startupPath 现在拥有独立副本。}错误示例混用分配器。下面的delete[]不匹配 API 的分配方式可能破坏堆内存。// 错误SHGetKnownFolderPath 的输出必须用 CoTaskMemFree 释放。delete[]allocatedPath;三、枚举目录时只读取一层2. 文件查找 API 返回目录项属性第一次查找文件的宽字符 API 返回搜索句柄和第一条目录项。返回INVALID_HANDLE_VALUE表示失败。搜索句柄必须由FindClose关闭不能使用CloseHandle。// 意义按 lpFileName 的通配符查找第一条目录项。// 返回有效搜索句柄表示成功。INVALID_HANDLE_VALUE 表示失败。HANDLEFindFirstFileW(LPCWSTR lpFileName,// 输入目录路径加 \\* 通配符。LPWIN32_FIND_DATAW lpFindFileData// 输出第一条名称、属性和时间信息。);// 意义读取同一搜索句柄的下一条目录项。// 返回非零表示成功。返回 0 且错误码为 ERROR_NO_MORE_FILES 表示正常结束。BOOLFindNextFileW(HANDLE hFindFile,LPWIN32_FIND_DATAW lpFindFileData);// 意义关闭 FindFirstFileW 返回的搜索句柄。// 返回非零表示成功。参数只能是搜索句柄。BOOLFindClose(HANDLE hFindFile);正确做法跳过.与..只处理目录第一层。Startup 文件夹中的普通文件、快捷方式和子目录都是不同目录项。递归进入子目录会把非直接内容混入结果。WIN32_FIND_DATAW data{};constHANDLE findHandleFindFirstFileW((startupPathL\\*).c_str(),data);if(findHandle!INVALID_HANDLE_VALUE){do{if(wcscmp(data.cFileName,L.)0||wcscmp(data.cFileName,L..)0){continue;}constboolisDirectory(data.dwFileAttributesFILE_ATTRIBUTE_DIRECTORY)!0;// 记录完整路径、目录标志和属性。普通文件再读取文件身份。}while(FindNextFileW(findHandle,data)!FALSE);FindClose(findHandle);}错误示例把FindFirstFileW返回的句柄交给CloseHandle。搜索句柄有专用关闭 API混用关闭函数会留下资源或产生错误状态。// 错误findHandle 应由 FindClose 关闭。CloseHandle(findHandle);四、用文件对象身份辅助路径比对3. 路径文本与文件身份是两类信息文件打开的宽字符 API 返回普通文件句柄。FILE_READ_ATTRIBUTES只请求元数据读取文件共享标志允许并发读取、写入和删除避免因另一个进程占用文件而不必要失败。// 意义打开已有文件并获得文件对象句柄。// 返回有效 HANDLE 表示成功。INVALID_HANDLE_VALUE 表示失败。HANDLECreateFileW(LPCWSTR lpFileName,DWORD dwDesiredAccess,DWORD dwShareMode,LPSECURITY_ATTRIBUTES lpSecurityAttributes,DWORD dwCreationDisposition,DWORD dwFlagsAndAttributes,HANDLE hTemplateFile);// 意义关闭 CreateFileW 成功返回的普通文件句柄。// 返回非零表示成功。搜索句柄和 SC_HANDLE 不使用此函数关闭。BOOLCloseHandle(HANDLE hObject);// 意义读取文件卷序列号、文件索引、属性与时间等身份信息。// 返回非零表示成功。返回 0 表示失败。BOOLGetFileInformationByHandle(HANDLE hFile,// 输入CreateFileW 成功返回的文件句柄。LPBY_HANDLE_FILE_INFORMATION lpInfo// 输出文件对象信息。);正确做法同时保存完整路径、卷序列号与文件索引。路径表示 Startup 位置。卷序列号和文件索引可以辅助识别同一路径被替换后的不同文件对象。constHANDLE fileCreateFileW(path.c_str(),FILE_READ_ATTRIBUTES,FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE,nullptr,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,nullptr);if(file!INVALID_HANDLE_VALUE){BY_HANDLE_FILE_INFORMATION info{};if(GetFileInformationByHandle(file,info)!FALSE){constunsignedlonglongfileIndex(static_castunsignedlonglong(info.nFileIndexHigh)32)|info.nFileIndexLow;// 保存 info.dwVolumeSerialNumber 与 fileIndex。}CloseHandle(file);}错误示例用扩展名判断文件是否可信。.lnk、.exe、.cmd、.bat和.url都可能出现在 Startup 目录。扩展名只描述名称形式。签名验证、签名主体、目标解析和文件身份都需要独立信息。五、保持目录范围和签名结论独立4. 签名信息不能替代路径事实文件位置与签名结果要分开记录。文件位于用户 Startup 或公共 Startup属于目录事实。签名是否有效、证书主体是谁、证书链是否可信属于独立的密码学检查结果。这两类结论不能互相替代。目录枚举由多次文件系统调用组成。文件可能在查找期间被创建、删除或替换。打开文件身份失败时记录路径和错误状态再继续读取其它目录项。完整可运行程序在附件。https://wangweicm.lanzouu.com/ijfMn3wwjebc