Window 系统错误代码 ERROR_SUCCESS,本博客中一律使用 NO_ERROR 代替。虽然 ERROR_SUCCESS 与 NO_ERROR 是完全等价的,都代表成功,但是后者却和其他错误代码一样,使用 ERROR 前缀,容易让人误认为是错误代码。而 NO_ERROR 意义很明显,就是无错误。还有另外一个宏 NOERROR 也表示成功,但是使用较少。Windows 系统错误代码的数据类型,其类型微软并没有具体说明。来自 advapi32.dll 中的注册表操作函数多使用 LONG 作为返回值,而来自 shlwapi.dll 中的注册表操作包装函数使用 LSTATUS 作为返回值。为保持统一,本博客统一使用 DWORD 作为 Windows 错误代码数据类型,这是因为 GetLastError 的返回值类型是 DWORD。
作为 Windows 开发人员,注册表是必须要了解的,读写注册表也是很平常的事情。然而,现实中也发现好多程序员对注册表的有些细节并不了解,尤其是在 64 位系统上重定向,以及 NT 6.0 开始推出的注册表虚拟化。
MSDN 上的说法是:注册表虚拟化是一种应用程序兼容技术,让那些可能带来全局影响的注册表写入操作重定向到每个用户的位置。这个读取或者写入重定向对于程序而言都是透明的。该技术从 Windows Vista 开始支持。(原文:Registry virtualization is an application compatibility technology that enables registry write operations that have global impact to be redirected to per-user locations. This redirection is transparent to applications reading from or writing to the registry. It is supported starting with Windows Vista.)
看的出来微软推出这个技术的目的。准确的来说就是,因为向 HKEY_LOCAL_MACHINE(以下简称 HKLM)写入注册表,是会影响到电脑上的所有用户,为了避免这种全局的影响,微软针对其写入操作进行了重定向。究其根本原因,就是 Windows XP 上并没有 UAC,任何程序都可以随意写入 HKLM。然而,从 Windows Vista 开始引入 UAC 之后,微软当然不允许低权限程序来随意操作 HKLM 了,但这样的话又可能会权限问题写入失败,就有可能导致程序运行出错,所以,为了早期的程序能正常运行且不影响现有注册表,微软引入了这个技术,以保证老的程序不会因为权限问题导致注册表写入失败。那么如何避免重定向呢?微软说要嵌入 manifest 并指定应用程序的执行权限,否则程序的注册表读写操作将注册表虚拟化技术重定向到其他位置。manifest 文件在 Visual Studio 中被称为清单文件。关于清单文件,将在其他文章进行讨论,在此我们这里只讨论执行权限级别设置,即 requestedExecutionLevel 这个节点。
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
其中 level 属性值 asInvoker,还可以是 highestAvailable 或 requireAdministrator。意义如下:
- asInvoker
以和调用该程序的进程同样的权限级别执行。也可以在右键菜单中选择使用管理员权限执行,但程序不会主动请求管理员权限,即便当前用户具备以管理员执行的条件。 - highestAvailable
以当前用户可以获得的最高权限来执行。即当前用户具备以管理员执行的条件时,会请求管理员权限,这种情况下和 requireAdministrator 一样。如果当前用户不具备管理员权限,则类似于 asInvoker 的情况。 - requireAdministrator
始终请求管理员权限。如果当前用户不具备管理员权限,则程序无法执行。
如果程序并没有嵌入清单文件,或者嵌入的清单文件并没有指定执行权限,那么程序的注册表写入将会被重定向,而不是返回失败。如下面的代码:
BOOL WINAPI RegWriteStringTest(void)
{
HKEY hKey = NULL;
DWORD dwError = RegCreateKeyEx(HKEY_LOCAL_MACHINE, _T("SOFTWARE\\KeyName"), 0, NULL, 0, KEY_WRITE, NULL, &hKey, NULL);
if (dwError == NO_ERROR)
{
TCHAR szValue[] = _T("ValueName");
TCHAR szData[] = _T("ValueData");
DWORD dwSize = lstrlen(szData) * sizeof(TCHAR) + sizeof(TCHAR);
dwError = RegSetValueEx(hKey, szValue, 0, REG_SZ, (BYTE *)szData, dwSize);
RegCloseKey(hKey);
if (dwError == NO_ERROR)
{
return TRUE;
}
}
return FALSE;
}
在程序未嵌入清单文件或者其中不包含权限信息时,且程序未以管理员权限执行的情况下,期望的返回值是 ERROR_ACCESS_DENIED,实际的返回值却是 NO_ERROR,调用 RegSetValueEx 也同样会成功。然而,打开注册表编辑器在 HKLM\SOFTWARE\KeyName 下查看,却发现并没有写入任何信息。使用 RegSnap 建立执行前后两个注册表快照,对比之后发现,注册表的写入被重定向到:
HKEY_CURRENT_USER\Software\Classes\VirtualStore\MACHINE\SOFTWARE\KeyName
当读取的时候,也是从上述位置读取,因此实际上也返回成功。就会造成一种假象:注册表系列 API 有 BUG,明明没有写入任何值,结果却返回成功,而且看似根本没写进去的值还能再次读取成功。
关于注册表虚拟化的更多信息,请访问:
https://msdn.microsoft.com/en-us/library/aa965884.aspx
在 64 位系统上,32 位程序读写部分注册表路径时会被系统重定向,这有些类似于读写 System32 文件夹的处理方式。比如,写入 HKLM\Software\KeyName,却发现实际写入到 HKLM\Software\Wow6432Node\KeyName,读取亦是如此。现实中发现,很多的程序员在检测一个程序在 HKLM 键下面的注册表信息,通常会针对 HKLM\Software 和 HKLM\Software\Wow6432Node 分别检查,实际上这样检查毫无效果。对于 32 位程序而言,访问 HKLM\Software 时,系统底层会重定向到 HKLM\Software\Wow6432Node,并不能得到真正的 HKLM\Software 下面的信息,即便再访问一次 HKLM\Software\Wow6432Node,经测试也是访问 Wow6432Node 下面的值,和直接访问 HKLM\Software 并没有任何区别。如果你仔细阅读 MSDN 上关于注册表重定向和访问权限等资料,会发现微软提供了两个特殊的注册表权限位:KEY_WOW64_32KEY、KEY_WOW64_64KEY,来控制访问权限。所以,当使用 RegOpenKeyEx 或 RegCreateKeyEx 访问注册表的 HKCR 或 HKLM\Software 下的路径,不需要显式指定 Wow6432Node,而是应当通过其权限位,如 KEY_READ,和上述二者之一进行组合来控制具体的访问位置。如果开发者显示指定 HKLM\Software\Wow6432Node,则程序在任何情况下都是访问这个路径。但是在 32 位系统中,这个路径默认并不存在,如果强行创建,依然没有任何意义。为了保持统一以及遵循 API 的规范,我们应该做到不显式指定 Wow6432Node 子键。如果不通过权限位进行访问视图控制,可能会造成代码逻辑混乱,如访问不同的注册表路径实际上底层逻辑相同,或者同样的代码编译为 32 位或 64 位后逻辑不一致等等。所以,如果要检测 32 位和 64 位注册表 HKLM\SOFTWARE\KeyName 下是否存在 ValueName,规范的代码如下:
BOOL WINAPI RegCheckValueTest(void)
{
DWORD dwWowFlags[] = { KEY_WOW64_32KEY, KEY_WOW64_64KEY };
DWORD dwWowCount = ARRAYSIZE(dwWowFlags);
for (size_t i = 0; i < dwWowCount; i++)
{
HKEY hKey = NULL;
DWORD dwAccess = KEY_READ | dwWowFlags[i];
DWORD dwError = RegOpenKeyEx(HKEY_LOCAL_MACHINE, _T("SOFTWARE\\KeyName"), dwAccess, &hKey);
if (dwError == NO_ERROR)
{
dwError = RegQueryValueEx(hkeySub, _T("ValueName"), NULL, NULL, NULL, NULL);
RegCloseKey(hKey);
if (dwError == NO_ERROR)
{
return TRUE;
}
}
}
return FALSE;
}
在不同 CPU 位数的系统上,32 位和 64 位程序分别使用不同的权限位组合访问 HKLM\Software 时,系统底层实际访问的注册表位置如下表所示。
系统架构 | 程序架构 | 显式访问路径 | 实际访问路径 | 备注 | ||
权限位不含 KEY_WOW64_* | 权限位包含 KEY_WOW64_32KEY | 权限位包含 KEY_WOW64_64KEY | ||||
32 位系统 | 32 位程序 | HKLM\Software | HKLM\Software | 原因:在 32 位系统上不存在不同访问视图 影响:参数 KEY_WOW64_* 被系统忽略 | ||
64 位系统 | 32 位程序 | HKLM\Software | HKLM\Software\Wow6432Node | HKLM\Software\Wow6432Node | HKLM\Software | |
64 位程序 | HKLM\Software | HKLM\Software | ||||
32 位程序 | HKLM\Software\Wow6432Node | HKLM\Software\Wow6432Node | 原因:在路径中显式指定了 Wow6432Node 节点 影响:参数 KEY_WOW64_* 被系统忽略 | |||
64 位程序 |
可见,32 位程序访问注册表 HKLM\Software 路径时,默认会被重定向到 HKLM\Software\Wow6432Node,如果权限位指定 KEY_WOW64_64KEY 时则访问 HKLM\Software。64 位程序访问注册表 HKLM\Software 路径时,默认会访问 HKLM\Software,如果权限位指定 KEY_WOW64_32KEY 时则访问 HKLM\Software\Wow6432Node。当然,前提是程序并没有受到注册表虚拟化影响,否则会被写入到以下注册表位置:
HKEY_CURRENT_USER\Software\Classes\VirtualStore\MACHINE\SOFTWARE HKEY_CURRENT_USER\Software\Classes\VirtualStore\MACHINE\SOFTWARE\Wow6432Node
实际观察发现 HKCU\SOFTWARE\Wow6432Node 下面只有极少量的数据,因此 HKCU\SOFTWARE\Wow6432Node 下面(包括其他从此处映射的键)的注册表键通常可以忽略。这可以说明,注册表针对 32 和 64 位的重定向仅针对 HKLM(包括其他从此处映射的键)有效,如果要访问 HKCU 下面的节点,通常无需考虑重定向的问题。而在 32 位系统上,不存在注册表重定向的问题。
关于注册表重定向的更多信息,请访问:
https://msdn.microsoft.com/en-us/library/aa384253.aspx
https://msdn.microsoft.com/en-us/library/aa384129.aspx