Active Setup按用户首次登录执行的组件初始化本篇的目标是只读列出 Active Setup 注册表中的组件来源、组件标识和StubPath原文。Active Setup 是 Windows 用来协调用户级组件初始化的注册表机制。用户登录时系统可依据机器级组件定义和当前用户状态记录判断某项初始化条件。本文只读取登记信息不运行StubPath。要想完成上述读取分为四步定位四项注册表来源分别打开HKLM与HKCU的 64 位和 32 位视图。枚举组件子键读取每个来源中的直接子键名并把子键名和来源绑定。按字节长度读取StubPath区分值缺失、空值、值过大和 API 读取失败。验证类型并显示文本仅将合法的REG_SZ或REG_EXPAND_SZ数据解码为 UTF-16 文本保留原始类型和状态。读取链路为四项来源 - 组件子键 -StubPath原始字节 - 类型验证与文本显示。一、目标对象和四步流程1. 第一步定位四项注册表来源第一步要先区分机器级定义、当前用户状态和注册表视图。HKLM\Software\Microsoft\Active Setup\Installed Components保存计算机范围的组件定义。HKCU\Software\Microsoft\Active Setup\Installed Components保存当前用户范围的相关状态。64 位和 32 位视图也要分别读取因为不同架构的进程可看到不同的软件注册表位置。同一个 CLSID 样式子键名出现在不同根键或视图时仍是来源不同的记录。读取时应保留根键、视图、完整路径和组件名不能只留下一个“已注册”标签。完成第一步后已经取得四项独立的读取范围。接下来要在每个范围内找出真正代表组件身份的子键。2. 第二步枚举组件子键第二步要把一级子键名当作组件身份读取。Installed Components 下的直接子键通常使用 CLSID 样式名称子键名用于连接机器级定义和当前用户状态。StubPath只是组件子键内可能存在的一个值不能替代组件身份。组件子键即使没有StubPath、值为空或读取失败也应保留为一条组件记录。完整记录至少包括来源根键、32/64 位视图、组件子键名和完整路径。这样可以区分“组件存在但未设置StubPath”与“当前来源中没有该组件”。完成第二步后每个组件都有来源和身份。接下来需要读取该组件中的StubPath同时保持原始数据边界。3. 第三步按字节长度读取StubPath第三步要把StubPath当作类型化注册表值读取。读取时先取得数据类型和字节长度再以返回的字节数分配缓冲区并读取实际数据。值不存在、值长度为零、值超过限制和读取失败对应不同状态不能合并为“没有命令”。Version、Locale、IsInstalled等其它值也有各自含义但本次读取目标是组件身份、来源和StubPath。机器级与用户级记录可用于理解 Active Setup 的配置条件单次注册表读取不能证明登录时实际处理过某条命令。完成第三步后已经获得带有真实长度和类型的原始字节。接下来要判断这些字节能否安全解释为文本。4. 第四步验证类型并显示读取状态第四步要限制文本解码的前提。只有REG_SZ或REG_EXPAND_SZ并且字节长度能够按 UTF-16 宽字符对齐时原始数据才适合显示为字符串。REG_EXPAND_SZ里的%SystemRoot%、%ProgramFiles%等环境变量应先保留原文环境展开属于另一份派生结果。读取结果还要保留值类型、原始字节长度和错误状态。这样遇到REG_BINARY、缺少结尾 NUL、访问拒绝或组件在读取期间变化时记录能说明问题位于哪一层。完成第四步后已得到来源明确、组件身份完整且文本解释受类型约束的读取结果。后续背景说明将这些记录放回 Active Setup 的登录初始化机制中理解。5. 用户登录时比较的是组件身份和状态不能只看 StubPath机器定义与用户状态通过组件身份配对。系统使用组件子键名作为配对身份机器范围的组件定义提供版本和安装相关属性当前用户范围的同名子键提供该用户已经处理到的状态。读取过程应把两个位置的组件名建立关联表同时保留双方各自的完整键路径、视图、值类型和读取时间。Version通常是文本版本字段IsInstalled常以数值或类型化数据表达安装状态Locale可表达区域或语言相关条件。每个字段都需要按真实注册表类型读取字符串只有在 UTF-16 长度对齐时解码REG_DWORD只有数据恰好四字节时读取为数值未知类型保留原始字节。不能因为StubPath存在就跳过版本、安装状态和当前用户记录。机器级组件 {A} 的 Version 1,0,0,0 - 与当前用户组件 {A} 的 Version 0,9,0,0 比较 - 两侧记录存在实际登录处理仍需独立观察上图中的“结果”只说明配置比较所需的字段。系统实际是否运行初始化、命令是否成功、用户是否已经退出登录流程需要登录时的进程、事件或安装记录作为独立证据。6. 组件子键枚举需要处理 UTF-16 名称长度和并发变化组件标识枚举需要处理 UTF-16 长度和并发变化。RegEnumKeyExW只返回当前键的直接子键名输入输出长度以 UTF-16 宽字符个数计不包含结尾 NUL。初始缓冲区可来自RegQueryInfoKeyW的最长子键名也可以从合理小容量开始。收到ERROR_MORE_DATA时扩大名称缓冲区并重试同一索引。ERROR_NO_MORE_ITEMS表示当前枚举结束。ERROR_ACCESS_DENIED表示当前令牌没有获得该来源的读取权限。组件在枚举和打开之间被删除时RegOpenKeyExW可能失败。这应生成“子键在读取期间变化”的记录继续其它索引不把失败替换为空组件。HKEY parentnullptr;constLSTATUS openedRegOpenKeyExW(HKEY_LOCAL_MACHINE,// 输入机器范围根键。系统所有调用方不关闭。componentPath,// 输入NUL 结尾 UTF-16 Active Setup 父路径。0,// 输入保留必须为 0。KEY_ENUMERATE_SUB_KEYS|KEY_WOW64_64KEY,// 输入最小枚举权限与明确视图。parent);// 输出成功时得到 HKEY本段负责关闭。if(openedERROR_SUCCESS){std::vectorwchar_tname(256,L\0);DWORD nameCapacitystatic_castDWORD(name.size());// 容量单位为 UTF-16 字符数。constLSTATUS statusRegEnumKeyExW(parent,0,name.data(),nameCapacity,nullptr,nullptr,nullptr,nullptr);if(statusERROR_SUCCESS){// 以 nameCapacity 个字符复制组件名再单独打开子键和读取其属性。}elseif(statusERROR_MORE_DATA){// 正确扩大 name 后重试索引 0。不能把它视为没有组件。}RegCloseKey(parent);// 释放本段成功打开的 HKEY。}7. StubPath 文本需要保留原文、类型和命令边界StubPath 文本不能直接当作已验证程序路径。StubPath若为REG_EXPAND_SZ可能包含%SystemRoot%、%ProgramFiles%等环境变量原文和当前环境下的展开结果是两份数据。若文本包含引号、参数、rundll32调用或其它解释器形式文件路径、参数和工作目录也不能用第一个空格简单切分。正确记录至少包含组件身份、来源根键、视图、值名、REG 类型、原始字节长度、严格按返回长度得到的文本以及可选展开结果。路径是否存在、目标文件签名、登录时是否执行和命令返回状态属于后续验证阶段。这样即使文本为空、缺少 NUL、类型异常或展开失败组件记录仍然完整可追溯。8. 删除、修改与读取的影响范围不同键和值属于不同对象。删除StubPath值会保留组件身份和其它属性。删除整个组件子键会同时移除版本、安装状态和初始化文本。修改当前用户状态又只影响该用户范围。每种动作对后续登录初始化的含义不同检查或维护前必须备份完整键树和类型化值。只读枚举不需要创建、写入或删除注册表权限。读取过程应请求KEY_QUERY_VALUE或KEY_ENUMERATE_SUB_KEYS等最小权限把访问拒绝如实记录。这样可以观察计算机级和用户级差异又不会因为检查动作改变用户下次登录时的配置条件。9. 读取状态应区分键缺失、值缺失、空值和类型异常读取状态要准确区分“没有文本”的不同来源。父键不存在时RegOpenKeyExW返回ERROR_FILE_NOT_FOUND表示当前来源没有组件目录。父键存在而某个组件子键不存在表示该组件没有出现在该来源。组件子键存在而StubPath缺失表示身份和其它属性仍可读取。StubPath存在但长度为零表示一个空值。类型为REG_BINARY或长度不能组成 UTF-16 时表示类型或格式异常。这五种状态都不应简化为“没有启动项”。它们对应不同的配置层级和维护动作。输出时保存根键、视图、组件名、值名、状态码和读取时间。后续比较两个用户或两次登录时才能准确判断变化发生在组件树、属性值还是解码阶段。10. 组件登记、文件检查和登录执行应使用三组证据注册表配置不能直接推导登录结果。第一组证据是注册表组件登记它回答机器和用户范围各自保存了什么。第二组证据是文本解析与文件检查它回答 StubPath 原文能否展开、候选路径是否存在、文件架构和签名状态如何。第三组证据是实际登录运行它回答相应初始化是否被处理、何时启动、返回什么结果。三组证据的时间点和权限范围可能不同。一个用户的 HKCU 状态只对该用户有效。机器级定义对其它用户也可能可见。文件检查受当前访问令牌影响。登录运行时记录还受系统版本和策略影响。把它们分字段保留文章读者可以看见从“配置存在”到“实际执行”的每一步距离。11. HKEY、REGSAM、DWORD 和 BSTR 等名称应按责任理解代码中的HKEY是注册表键句柄成功打开后由调用方关闭。REGSAM是请求访问权限的位掩码。DWORD是 32 位无符号整数注册表 API 中既可能表示字符数也可能表示字节数。已知文件夹、COM 或 Shell API 若返回BSTR、PWSTR等指针则必须按各自文档规定的分配器释放不能把注册表缓冲区和 COM 字符串的释放规则混用。这些名称看似属于实现细节实际决定了读取过程是否会泄漏句柄、截断文本或释放错误内存。文章中的每次 API 调用都应说明哪一方分配、哪一方释放、长度采用什么单位以及失败后还能否使用输出指针。二、第一步定位四项注册表来源第一步要以最小权限打开 Active Setup 的父键。HKLM表示计算机范围HKCU表示当前用户范围。两个根键都要使用 64 位和 32 位视图打开因此读取范围共有四项。父键由系统维护成功打开后返回的HKEY由调用方关闭。四项来源中即使有某一项父键不存在也应记录该来源的状态后继续读取其它来源。父键不存在只能说明当前范围未登记 Active Setup 组件不能替代其它范围的结果。// 意义打开注册表子键。// 返回ERROR_SUCCESS 表示成功。其它返回值是 Win32 错误码。// 成功时 phkResult 接收 HKEY调用方必须关闭。LSTATUSRegOpenKeyExW(HKEY hKey,LPCWSTR lpSubKey,DWORD ulOptions,REGSAM samDesired,PHKEY phkResult);// 意义关闭打开成功的键句柄。// 返回ERROR_SUCCESS 表示成功。LSTATUSRegCloseKey(HKEY hKey);conststruct{HKEY root;REGSAM view;}sources[]{{HKEY_LOCAL_MACHINE,KEY_WOW64_64KEY},{HKEY_LOCAL_MACHINE,KEY_WOW64_32KEY},{HKEY_CURRENT_USER,KEY_WOW64_64KEY},{HKEY_CURRENT_USER,KEY_WOW64_32KEY},};完成第一步后读取范围已经固定为四个互不混淆的来源。下一步需要在每个来源中找到组件身份才能知道后续读取的值属于哪个组件。三、第二步枚举组件子键组件名称长度不应由固定数组限制。RegEnumKeyExW的名称长度单位是 UTF-16 字符数返回ERROR_MORE_DATA时需要扩大名称缓冲区后重试。// 意义按索引读取一级子键名。// 返回ERROR_SUCCESS 表示成功。ERROR_NO_MORE_ITEMS 表示枚举结束。// ERROR_MORE_DATA 表示 lpName 的字符容量不足。LSTATUSRegEnumKeyExW(HKEY hKey,DWORD dwIndex,LPWSTR lpName,LPDWORD lpcchName,LPDWORD lpReserved,LPWSTR lpClass,LPDWORD lpcchClass,PFILETIME lpftLastWriteTime);正确代码用最小枚举权限打开父键从初始容量开始扩容。父键只允许子键枚举时仍可继续遍历不依赖额外的键查询权限。std::vectorwchar_tname(256,L\0);DWORD charsstatic_castDWORD(name.size());LSTATUS statusRegEnumKeyExW(parentKey,index,name.data(),chars,nullptr,nullptr,nullptr,nullptr);if(statusERROR_SUCCESS){std::wstringcomponent(name.data(),chars);std::wstring childPathparentPathL\\component;}// 错误示例只在 StubPath 存在时保存组件。if(QueryStubPath(childKey)){records.push_back(component);}完成第二步后每条记录都有来源、视图和组件子键名。下一步读取StubPath时需要以字节数为边界避免把未知长度的数据当作字符串。四、第三步按字节长度读取StubPathStubPath是组件子键中的命令文本字段。先查询字节长度再读取原始数据。值不存在、类型错误、超出读取上限和读取失败都要与空字符串区分。// 意义读取注册表值的类型和原始数据。// 返回ERROR_SUCCESS 表示成功。ERROR_FILE_NOT_FOUND 表示值未设置。// ERROR_MORE_DATA 表示 lpData 的字节容量不足。LSTATUSRegQueryValueExW(HKEY hKey,LPCWSTR lpValueName,LPDWORD lpReserved,LPDWORD lpType,LPBYTE lpData,LPDWORD lpcbData);DWORD typeREG_NONE;DWORD byteCount0;LSTATUS statusRegQueryValueExW(childKey,LStubPath,nullptr,type,nullptr,byteCount);if(statusERROR_SUCCESS){std::vectorBYTEbytes(byteCount);DWORD copiedBytesbyteCount;statusRegQueryValueExW(childKey,LStubPath,nullptr,type,bytes.empty()?nullptr:bytes.data(),copiedBytes);if(statusERROR_SUCCESS)bytes.resize(copiedBytes);}文本只在REG_SZ或REG_EXPAND_SZ且字节数满足 UTF-16 对齐时转换。REG_EXPAND_SZ的%SystemRoot%等标记应先保留原始文本环境展开属于另一份派生结果。// 错误示例把 StubPath 当作组件子键的唯一身份。std::wstring identitydecodedStubPath;// 组件 CLSID、机器级或当前用户来源、注册表视图都会丢失。完成第三步后记录中已有StubPath的原始数据、类型和读取状态。下一步只对满足字符串条件的数据进行解码并保留不能解码的原因。五、第四步验证类型并显示读取状态组件清理或审计时需要以组件子键为单位考虑。删除单个StubPath值与删除整个组件注册树具有不同效果记录中应保留完整组件路径不能只保存命令文本。文本只应在REG_SZ或REG_EXPAND_SZ且字节数为sizeof(wchar_t)整数倍时解码。值为REG_BINARY、长度未对齐或 API 返回错误时应显示类型、字节数和错误码不应伪造字符串结果。NUL、回车和换行等控制字符也应转义显示避免它们改变输出行的边界。完成第四步后记录可以同时表达组件是否存在、值是否存在、数据能否解码和文本原文。机器级注册与当前用户状态共同描述按用户执行的条件单独读取任一侧都不能证明某次登录已经实际处理过初始化。完整可运行程序在附件https://wangweicm.lanzouu.com/ivlpf3yj1m0f