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 这个节点。
1 2 3 4 5 6 7 |
|
其中 level 属性值 asInvoker,还可以是 highestAvailable 或 requireAdministrator。意义如下:
- asInvoker
以和调用该程序的进程同样的权限级别执行。也可以在右键菜单中选择使用管理员权限执行,但程序不会主动请求管理员权限,即便当前用户具备以管理员执行的条件。 - highestAvailable
以当前用户可以获得的最高权限来执行。即当前用户具备以管理员执行的条件时,会请求管理员权限,这种情况下和 requireAdministrator 一样。如果当前用户不具备管理员权限,则类似于 asInvoker 的情况。 - requireAdministrator
始终请求管理员权限。如果当前用户不具备管理员权限,则程序无法执行。
如果程序并没有嵌入清单文件,或者嵌入的清单文件并没有指定执行权限,那么程序的注册表写入将会被重定向,而不是返回失败。如下面的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
在程序未嵌入清单文件或者其中不包含权限信息时,且程序未以管理员权限执行的情况下,期望的返回值是 ERROR_ACCESS_DENIED,实际的返回值却是 NO_ERROR,调用 RegSetValueEx 也同样会成功。然而,打开注册表编辑器在 HKLM\SOFTWARE\TestKey 下查看,却发现并没有写入任何信息。使用 RegSnap 建立执行前后两个注册表快照,对比之后发现,注册表的写入被重定向到:
1 |
|
其中 <User_SID> 为当前用户的 SID 字符串,在本机甚至整个互联网都是唯一的。不同的系统或不同的用户,该 SID 字符串通常都不相同。当读取的时候,也是从上述位置读取,因此实际上也返回成功。就会造成一种假象:注册表系列 API 有 BUG,明明没有写入任何值,结果却返回成功,而且看似根本没写进去的值还能再次读取成功。
关于注册表虚拟化的更多信息,请访问:
https://msdn.microsoft.com/en-us/library/aa965884.aspx
在 64 位系统上,32 位程序读写部分注册表路径时,会被系统所重定向,这有些类似于读写 System32 文件夹的处理方式。比如,写入 HKLM\Software\TestKey,却发现实际写入到 HKLM\Software\Wow6432Node\TestKey,读取亦是如此。现实中发现,很多的程序员在检测一个程序在 HKLM 键下面的注册表信息,通常会针对 HKLM\Software 和 HKLM\Software\Wow6432Node 分别检查,实际上这样检查毫无效果。对于 32 位程序而言,访问 HKLM\Software 时,系统底层会重定向到 HKLM\Software\Wow6432Node,并不能得到真正的 HKLM\Software 下面的信息,即便再访问一次 HKLM\Software\Wow6432Node,和直接访问 HKLM\Software 并没有任何区别。如果你仔细阅读 MSDN 上关于注册表重定向和访问权限等资料,会发现微软提供了两个特殊的注册表权限位:KEY_WOW64_32KEY、KEY_WOW64_64KEY,来控制访问权限。当使用 RegOpenKeyEx 或 RegCreateKeyEx 访问注册表的时候,不需要在子键路径中显式的指定 Wow6432Node,而是应当通过其权限位,如 KEY_READ,和上述二者之一进行组合来控制具体的访问位置。如检测 32 位和 64 位注册表 HKLM\SOFTWARE\TestKey 是否存在 TestValue,正确的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
在 64 位系统上,32 位和 64 位程序分别使用不同的权限位组合访问 HKLM\Software 时,系统底层实际访问的注册表位置对比:
不包含 KEY_WOW64_*** | 包含 KEY_WOW64_32KEY | 包含 KEY_WOW64_64KEY | |
32 位程序 | HKLM\Software\Wow6432Node | HKLM\Software\Wow6432Node | HKLM\Software |
64 位程序 | HKLM\Software | HKLM\Software\Wow6432Node | HKLM\Software |
即:32 位程序访问注册表 HKLM\Software 路径时,默认会被系统重定向到 HKLM\Software\Wow6432Node,而在权限位显式指定 KEY_WOW64_64KEY 时则访问 HKLM\Software;64 位程序访问注册表 HKLM\Software 路径时,默认会被系统会访问 HKLM\Software,而在权限位显式指定 KEY_WOW64_32KEY 时则访问 HKLM\Software\Wow6432Node。当然,前提是程序并没有受到注册表虚拟化影响,否则会被写入到以下注册表位置:
1 2 |
|
而由于注册表的 HKCR 又来自于 HKCU\Software\Classes 和 HKLM\Software\Classes(也包括 64 位系统上的 32 位注册表 HKLM\Software\Wow6432Node\Classes)。故,理论上,上面的键值也可以写成:
1 2 |
|
理论如此,不过实际观察发现在 HKCU\SOFTWARE\Wow6432Node 下面只有极少量的注册表信息,而 HKCU 又映射自 HKEY_USERS\<User_SID>,同时 HKEY_USERS\<User_SID>\SOFTWARE\Wow6432Node 下面也只有极少量的注册表信息,没有 Classes 子健,因此 HKCU\SOFTWARE\Wow6432Node 下面(包括其他从此处映射的键)的注册表键通常可以忽略。这说明,注册表针对 32 和 64 位的重定向仅针对 HKLM(包括其他从此处映射的键)有效,如果要访问 HKCU 下面的节点,通常无需考虑重定向的问题。由于 HKEY_USERS\<User_SID> 这个键路径包含了用户 SID,不同用户甚至都不一样,直接用 RegOpenKeyEx 来操作显然是办不到的。微软提供了另外一个函数 RegOpenCurrentUser,来操作当前执行程序用户权限的注册表。通常用户都是使用都是当前登录用户权限启动注册表编辑器,那么其中的 HKCU 也是当前登录用户的注册表。例如,当前登录用户为非管理员账户,但程序使用管理员权限启动,则该程序使用 RegOpenCurrentUser 访问的是管理员的注册表,这和通过管理员权限直接启动注册表编辑器是一样的。示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
在 32 位系统上,不存在这些问题。