Session Manager(会话管理器)是Windows在用户登录前启动的关键系统组件,负责建立早期会话环境。它位于注册表HKLM\System\CurrentControlSet\Control\Session Manager下,包含三个重要REG_MULTI_SZ值:BootExecute、SetupExecute与Execute。这三项配置以多字符串(Multi-String)格式存储,解析时需要严格遵循其UTF-16边界规则,否则会导致条目丢失或越界访问。本文介绍如何使用Win32 API安全读取并正确解析这些配置,区分空列表、缺失值和格式异常。
一、理解REG_MULTI_SZ的存储结构
REG_MULTI_SZ是注册表保存多段UTF-16文本的值类型。每段文本以单个NUL(宽字符\0)结束,整个列表以额外一个NUL即双NUL标记终止。例如,一个条目“autocheck autochk *”在缓冲区中的表示为:autocheck autochk *\0\0。两个条目的列表为:first\0second\0\0。合法空列表则是\0\0。RegQueryValueExW返回的长度是字节数,因此需要先将字节数除以sizeof(wchar_t)得到字符数,再按字符索引遍历。
二、定位父键并获取只读句柄
读取前先以KEY_QUERY_VALUE权限打开Session Manager键。代码示例:- HKEY key = nullptr;
- if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
- L"System\\CurrentControlSet\\Control\\Session Manager",
- 0, KEY_QUERY_VALUE, &key) == ERROR_SUCCESS) {
- // 读取三个值
- RegCloseKey(key);
- }
复制代码 注意:CurrentControlSet是当前启动控制集别名,直接使用它才能反映本次启动的配置。
三、二次查询读取原始字节数据
先以nullptr查询获取所需字节数,再按该大小分配缓冲区读取。若遇到ERROR_MORE_DATA需重试(最多三次)。代码示例:- struct RegistryValueBytes {
- DWORD type = REG_NONE;
- std::vector<BYTE> data;
- };
- static LSTATUS ReadRegistryValueBytes(HKEY key, const wchar_t* valueName,
- RegistryValueBytes& result) {
- for (int attempt = 0; attempt != 3; ++attempt) {
- DWORD type = REG_NONE;
- DWORD requiredBytes = 0;
- LSTATUS status = RegQueryValueExW(key, valueName, nullptr,
- &type, nullptr, &requiredBytes);
- if (status != ERROR_SUCCESS) return status;
- std::vector<BYTE> bytes(requiredBytes);
- DWORD actualBytes = requiredBytes;
- status = RegQueryValueExW(key, valueName, nullptr, &type,
- bytes.empty() ? nullptr : bytes.data(),
- &actualBytes);
- if (status == ERROR_MORE_DATA) continue;
- if (status != ERROR_SUCCESS) return status;
- bytes.resize(actualBytes);
- result.type = type;
- result.data = std::move(bytes);
- return ERROR_SUCCESS;
- }
- return ERROR_MORE_DATA;
- }
复制代码 注意:只有字节数能被sizeof(wchar_t)整除时才能按UTF-16解析。
四、按分隔符解析条目列表
将缓冲区指针转为wchar_t*,从起始位置开始查找第一个NUL作为条目结束,然后跳过NUL继续下一个条目,直到遇到双NUL或到达缓冲区末尾。代码示例:- std::vector<std::wstring> entries;
- if (status == ERROR_SUCCESS && value.type == REG_MULTI_SZ &&
- value.data.size() % sizeof(wchar_t) == 0) {
- const auto* text = reinterpret_cast<const wchar_t*>(value.data.data());
- const std::size_t count = value.data.size() / sizeof(wchar_t);
- std::size_t begin = 0;
- while (begin < count) {
- std::size_t end = begin;
- while (end < count && text[end] != L'\0') ++end;
- if (end == begin) break; // 双NUL,列表结束
- entries.emplace_back(text + begin, end - begin);
- if (end == count) break; // 缺少最终双NUL,记录异常
- begin = end + 1;
- }
- }
复制代码 BootExecute的原始条目如“autocheck autochk *”应整个保存,不要按空格拆分成多个参数。
五、区分空列表、缺失值与格式异常
- 值存在且为空列表:类型为REG_MULTI_SZ,字节数为2(即一个\0\0),entries为空。
- 值不存在:RegQueryValueExW返回ERROR_FILE_NOT_FOUND。
- 类型异常:如果返回类型不是REG_MULTI_SZ,应单独记录。
- 缺少双NUL终止:解析时若while循环因为end == count退出(未遇到第二个NUL),应标注格式异常,但仍可输出已读取条目。
六、实践要点
这三个值分别对应不同启动阶段:BootExecute通常在autochk检查磁盘前执行;SetupExecute用于安装/设置场景;Execute是另一份命令列表。读取程序应分别记录每个值的原始字节长度、条目顺序、是否观察到双NUL及API返回码。不要将空列表、缺失值和异常合并为同一个空字符串输出,否则会丢失诊断信息。
完整可运行的示例项目链接:wangweicm.lanzouu.com/iZ1aq3xtkff…(原文附件)。 |