也談如何獲取真實正確的 Windows 系統版本號


  • 關於 GetVersion 系列接口

    關於如何獲取 Windows 系統版本號的話題,網上已經有了太多的帖子。但個人覺得總結的都不盡全面,或者沒有給出比較穩定的解決方案。

    眾所周知,獲取 Windows 系統版本的 API 是 GetVersionGetVersionEx。這兩個 API 的使用也都相當簡單,一直被廣泛使用(下文中我們將其統稱為 GetVersion 系列)。后來在 Windows XP 中微軟引入了應用程序兼容模式,可以選擇以兼容之前 Windows 系統版本的模式運行程序。可能很多人並不知道其具體的實現原理,以及能造成如何影響,微軟官網開始也未對此詳細說明。但直到后來,隨着 Windows Vista、Windows 7 等發布,開始有人反映這個 API 並不能獲取到真正的系統版本號。我也開始嘗試測試各種獲取系統版本信息的 API,才慢慢發現應用兼容性設置其中一個影響是讓用戶無法獲取正確的系統版本號。即,將程序設置為兼容舊的操作系統,則 GetVersion 系列接口獲取到的版本就是該兼容系統的版本,也就是說結果與當前系統實際版本不符,是錯誤的。

    與此同時,Windows XP 中也引入了主題,引入了 manifest 文件(Visual Studio 中稱之為清單文件)的概念。關於這個清單文件,我在這篇文章中討論程序執行權限也提到過,此文暫不詳述。清單文件中的設置會影響到程序的某些行為,比如是否使用主題、是否以管理員權限執行、程序支持的操作系統列表、是否受 DPI 縮放影響等。這里我們只討論其中「支持的操作系統列表」這部分,因為這部分現在也會影響到在新版操作系統中調用 GetVersion 系列的結果。

    雖然微軟的官網對於該系列 API 的行為進行了說明,但畢竟實踐才是唯一標准。為了搞清楚各個系統版本的 GetVersion 系列接口結果行為有何不同,我詳細測試后,將其整理如下:

    是否嵌入清單 程序無清單文件或清單未指定支持當前系統 程序有清單文件且清單指定支持當前系統
    兼容模式設置 未設置兼容模式 設置兼容模式 未設置兼容模式 設置兼容模式
    Windows 2000 5.0 5.0[1] 5.0[2] 5.0[2]
    Windows XP (x86) 5.1 兼容模式設置的兼容系統版本 5.1[3] 5.1[3]
    Windows XP (x64) 5.2 兼容模式設置的兼容系統版本 5.2[3] 5.2[3]
    Windows Vista 6.0 兼容模式設置的兼容系統版本 6.0 兼容模式設置的兼容系統版本
    Windows 7 6.1 兼容模式設置的兼容系統版本 6.1 兼容模式設置的兼容系統版本
    Windows 8 6.2 兼容模式設置的最高兼容系統版本,最高 6.2。 6.2 兼容模式設置的最高兼容系統版本,最高 6.2。
    Windows 8.1 6.2 兼容模式設置的最高兼容系統版本,最高 6.2。 6.3 兼容模式設置的最高兼容系統版本,最高 6.3。
    Windows 10 6.2 兼容模式設置的最高兼容系統版本,最高 6.2。 10.0 兼容模式設置的最高兼容系統版本,最高 10.0。
    [1] Windows 2000 不支持兼容模式,因此結果不受影響。
    [2] Windows 2000 不支持清單文件,因此結果不受影響。
    [3] Windows XP 不支持清單文件中指定的支持操作系統列表。

    可以看得出來,結果慘不忍睹,這還怎么能讓人放心使用?實際上 GetVersion 系列接口的行為變更從 XP 時代就有,然而微軟開始並沒有在 MSDN 上給出相關說明,也沒有多少人留意。起初微軟是為設置應用程序以兼容模式運行,將其 hook 並返回錯誤的結果來實現讓老程序在新的操作系統上以舊版本操作系統的「兼容模式」運行。然而,這個帶來了更大的麻煩,再加上 manifest 的引入,使得這個 API 完全被微軟玩壞。到 Windows 8 時代,該頁面才注明該 API 已被廢棄,並且給出其他的解決方案。只能說,這兩個 API 走到今天這條路,微軟也是自食其果,其返回值從一開始被 hook 修改就注定了今天被拋棄的結果。

  • 官方推薦的備用方案

    此外,微軟也提供其他的幾個 API 用來判斷(不能獲取)系統版本是否為特定版本,只是鮮為人知,使用頻率較低。從 GetVersion 系列被拋棄開始,這些 API 才在 MSDN 被列出在 GetVersionEx 的說明頁面,作為其他的備選方案。

    • IsOS

      這個 API 是判斷特定版本的,但是最高支持也就到 Windows 2003。此后,微軟 MSDN 頁面未對參數進行更新。

    • VerifyVersionInfo

      該 API 需配合 VerSetConditionMask 預先設置條件和邏輯,再進行后續判斷。微軟已經將 VerifyVersionInfo 封裝為如下更易使用的函數。使用這些函數需包含 VersionHelpers.h 頭文件,較新版本的 Visual Studio 或 Windows SDK 中提供此頭文件。

      這組函數依然只能夠用來判斷而不能獲取系統版本。而且,根據 MSDN 的說明,其中的部分依然受到清單文件影響,但未測試。

    • NetWkstaGetInfo

      這個 API 也是微軟官方推薦的獲取系統版本號的替代方案之一。

      #include <windows.h>
      #include <lm.h>
      #pragma comment(lib, "netapi32.lib")
      
      DWORD PASCAL GetVersion( void )
      {
      	DWORD dwVersion = 0;
      	WKSTA_INFO_100 *wkstaInfo = NULL;
      	NET_API_STATUS netStatus = NetWkstaGetInfo(NULL, 100, (BYTE **)&wkstaInfo);
      	if (netStatus == NERR_Success)
      	{
      		DWORD dwMajVer = wkstaInfo->wki100_ver_major;
      		DWORD dwMinVer = wkstaInfo->wki100_ver_minor;
      		dwVersion = (DWORD)MAKELONG(dwMinVer, dwMajVer);
      		NetApiBufferFree(wkstaInfo);
      	}
      	return dwVersion;
      }

      經測試,獲取的系統主次版本號正確,不受兼容性設置和清單文件的影響,但無法獲取 build 版本。某些場景下在 dll 中調用會失敗,原因未知,使用時需要注意。

  • 非官方備用方案

    這里提供一些在網上搜索到的其他方案。由於部分使用了系統內部接口甚至數據結構,不保證后續依然有效。

    • 查詢 kernel32.dll 版本

      通常情況下 kernel32.dll 的版本號和系統是同步的,但如果微軟哪天不遵守這個約定,這個方法就不好用了。有的程序則是查詢 ntoskrnl.exe 的版本信息,原理類似。

      #include <windows.h>
      #include <shlwapi.h>
      #pragma comment(lib, "shlwapi.lib")
      #pragma comment(lib, "version.lib")
      
      DWORD PASCAL GetKernelVersion( void )
      {
      	DWORD dwVersion = 0;
      	WCHAR szDLLName[MAX_PATH] = { 0 };
      	HRESULT hr = SHGetFolderPathW(NULL, CSIDL_SYSTEM, NULL, SHGFP_TYPE_CURRENT, szDLLName);
      	if ((hr == S_OK) && PathAppendW(szDLLName, L"kernel32.dll"))
      	{
      		DWORD dwVerInfoSize = GetFileVersionInfoSizeW(szDLLName, NULL);
      		if (dwVerInfoSize > 0)
      		{
      			HANDLE hHeap = GetProcessHeap();
      			LPVOID pvVerInfoData = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, dwVerInfoSize);
      			if (pvVerInfoData != NULL)
      			{
      				if (GetFileVersionInfoW(szDLLName, 0, dwVerInfoSize, pvVerInfoData))
      				{
      					UINT ulLength = 0;
      					VS_FIXEDFILEINFO *pvffi = NULL;
      					if (VerQueryValueW(pvVerInfoData, L"\\", (LPVOID *)&pvffi, &ulLength))
      					{
      						dwVersion = pvffi->dwFileVersionMS;
      					}
      				}
      				HeapFree(hHeap, 0, pvVerInfoData);
      			}
      		}
      	}
      	return dwVersion;
      }

      很不幸,經測試,如果程序沒有嵌入清單文件,在 Windows 8.1 或 Windows 10,這個方法獲取的結果也是 6.2,也就是說仍然受到清單文件的影響,有可能得到錯誤的結果。

    • 讀取 kernel32.dll 版本

      什么,還有個讀取?那么查詢和讀取有什么分別?沒看到上面最后一行嗎,連獲取文件版本信息的 API 都拿不到正確結果了,微軟還有什么能相信?好吧,你不給我正確結果,我就直接分析二進制總行了吧!

      #include <windows.h>
      
      DWORD PASCAL ReadKernelVersion( void )
      {
      	DWORD dwVersion = 0;
      	HMODULE hinstDLL = LoadLibraryExW(L"kernel32.dll", NULL, LOAD_LIBRARY_AS_DATAFILE);
      	if (hinstDLL != NULL)
      	{
      		HRSRC hResInfo = FindResource(hinstDLL, MAKEINTRESOURCE(VS_VERSION_INFO), RT_VERSION);
      		if (hResInfo != NULL)
      		{
      			HGLOBAL hResData = LoadResource(hinstDLL, hResInfo);
      			if (hResData != NULL)
      			{
      				static const WCHAR wszVerInfo[] = L"VS_VERSION_INFO";
      				struct VS_VERSIONINFO {
      					WORD wLength;
      					WORD wValueLength;
      					WORD wType;
      					WCHAR szKey[ARRAYSIZE(wszVerInfo)];
      					VS_FIXEDFILEINFO Value;
      					WORD Children[];
      				} *lpVI = (struct VS_VERSIONINFO *)LockResource(hResData);
      				if ( (lpVI != NULL) && (lstrcmpiW(lpVI->szKey, wszVerInfo) == 0) && (lpVI->wValueLength > 0) )
      				{
      					dwVersion = lpVI->Value.dwFileVersionMS;
      				}
      			}
      		}
      		FreeLibrary(hinstDLL);
      	}
      	return dwVersion;
      }

      很高興的告訴大家,這個結果即使在 Windows 8.1 或 Windows 10 上,也都依然是正確的。

    • 讀取 PEB 數據結構

      PEB 結構是 Windows 系統的內部接口,讀取的數據是最底層的,但是也正因為是內部結構,微軟隨時有可能變動。下面的結構體只是簡略定義,對不需要或者不重點關注的成員進行了省略或者使用了 PVOID 指針來代替。務必注意,此方法僅供參考,如后期 Windows 系統變更數據結構,造成任何藍屏死機問題,本人概不負責。

      #include <windows.h>
      
      typedef struct _PEB {
      	BOOLEAN InheritedAddressSpace;
      	BOOLEAN ReadImageFileExecOptions;
      	BOOLEAN BeingDebugged;
      	BOOLEAN BitField;
      	HANDLE Mutant;
      	PVOID ImageBaseAddress;
      	PVOID Ldr;
      	PVOID ProcessParameters;
      	PVOID SubSystemData;
      	PVOID ProcessHeap;
      	PVOID FastPebLock;
      	PVOID AtlThunkSListPtr;
      	PVOID SparePtr2;
      	ULONG EnvironmentUpdateCount;
      	PVOID KernelCallbackTable;
      	ULONG SystemReserved[1];
      	ULONG SpareUlong;
      	PVOID FreeList;
      	ULONG TlsExpansionCounter;
      	PVOID TlsBitmap;
      	ULONG TlsBitmapBits[2];
      	PVOID ReadOnlySharedMemoryBase;
      	PVOID ReadOnlySharedMemoryHeap;
      	PVOID *ReadOnlyStaticServerData;
      	PVOID AnsiCodePageData;
      	PVOID OemCodePageData;
      	PVOID UnicodeCaseTableData;
      	ULONG NumberOfProcessors;
      	ULONG NtGlobalFlag;
      	LARGE_INTEGER CriticalSectionTimeout;
      	SIZE_T HeapSegmentReserve;
      	SIZE_T HeapSegmentCommit;
      	SIZE_T HeapDeCommitTotalFreeThreshold;
      	SIZE_T HeapDeCommitFreeBlockThreshold;
      	ULONG NumberOfHeaps;
      	ULONG MaximumNumberOfHeaps;
      	PVOID *ProcessHeaps;
      	PVOID GdiSharedHandleTable;
      	PVOID ProcessStarterHelper;
      	ULONG GdiDCAttributeList;
      	PVOID LoaderLock;
      	ULONG OSMajorVersion;
      	ULONG OSMinorVersion;
      	USHORT OSBuildNumber;
      	USHORT OSCSDVersion;
      	ULONG OSPlatformId;
      } PEB, *PPEB;
      
      typedef struct _TEB {
      	NT_TIB NtTib;
      	PVOID EnvironmentPointer;
      	struct {
      		HANDLE UniqueProcess;
      		HANDLE UniqueThread;
      	} ClientId;
      	PVOID ActiveRpcHandle;
      	PVOID ThreadLocalStoragePointer;
      	PEB *ProcessEnvironmentBlock;
      } TEB, *PTEB;
      
      DWORD PASCAL GetVersionPEB( void )
      {
      	DWORD dwVersion = 0;
      	TEB *lpTeb = NtCurrentTeb();
      	if (lpTeb != NULL)
      	{
      		PEB *lpPeb = lpTeb->ProcessEnvironmentBlock;
      		if (lpPeb != NULL)
      		{
      			DWORD dwMajVer = lpPeb->OSMajorVersion;
      			DWORD dwMinVer = lpPeb->OSMinorVersion;
      			dwVersion = (DWORD)MAKELONG(dwMinVer, dwMajVer);
      		}
      	}
      	return dwVersion;
      }

      再次很高興的告訴你,這個結果截止 Windows 10,也都能獲取到正確的版本號。

    • RtlGetVersion

      使用時通常都是從 ntdll.dll 中動態加載,本人就不列出詳細代碼,僅以靜態調用作為示例。

      NTSTATUS NTAPI RtlGetVersion(
      	RTL_OSVERSIONINFOW *lpVersionInformation
      );
      
      DWORD PASCAL GetVersionRtl( void )
      {
      	DWORD dwVersion = 0;
      	RTL_OSVERSIONINFOEXW osvi = { 0 };
      	osvi.dwOSVersionInfoSize = sizeof(osvi);
      	NTSTATUS status = RtlGetVersion((RTL_OSVERSIONINFOW *)&osvi);
      	if (status == STATUS_SUCCESS)
      	{
      		DWORD dwMajVer = osvi.dwMajorVersion;
      		DWORD dwMinVer = osvi.dwMinorVersion;
      		dwVersion = (DWORD)MAKELONG(dwMinVer, dwMajVer);
      	}
      	return dwVersion;
      }

      最新測試發現,Windows 10 上可以獲取到正確的版本號。但如果程序設置了兼容模式,仍會獲取得到錯誤結果,即兼容系統的版本號。

    • RtlGetNtVersionNumbers

      同上,從 ntdll.dll 中加載。此接口系高手反編譯所得,微軟並未放出任何文檔,請謹慎使用。Windows 2000 不支持,Windows XP 起支持。

      void NTAPI RtlGetNtVersionNumbers(
      	DWORD *lpdwMajorVersion,
      	DWORD *lpdwMinorVersion,
      	DWORD *lpdwBuildNumber
      );
      
      DWORD PASCAL GetVersionRtl( void )
      {
      	DWORD dwMajorVersion = 0;
      	DWORD dwMinorVersion = 0;
      	RtlGetNtVersionNumbers(&dwMajorVersion, &dwMinorVersion, NULL);
      	DWORD dwVersion = (DWORD)MAKELONG(dwMinorVersion, dwMajorVersion);
      	return dwVersion;
      }

      在 Windows 10 上獲取的結果是 10.0,目前看來是不會出問題的。


免責聲明!

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



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