64 位 Windows 平台開發要點之注冊表虛擬化和重定向


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM