1. 程式人生 > >獲取Windows系統版本

獲取Windows系統版本

(本文在引用的原文的基礎上,添加了另外兩個獲取系統版本的方法,見文中內容)

一、前言

本文並不是討論windows作業系統的版本來歷和特點,也不是討論為什麼沒有Win9,而是從程式設計師角度討論下Windows獲取系統版本的方法和遇到的一些問題。在Win8和Win10出來之後,在獲取系統版本時,可能很多人都碰到了類似的問題,為什麼以前工作得很好的API,突然開始說謊了?

我們一般怎麼獲取系統版本

我想用的最多的可能就是這兩個API了吧。

DWORD WINAPI GetVersion (VOID);
BOOL WINAPI GetVersionExW(__inout LPOSVERSIONINFOW lpVersionInformation);

其實GetVersion和GetVersionExW的實現是類似的,內部都是呼叫的NtCurrentPeb這個函式,還有一個GetVersionExA內部則是呼叫的GetVersionExW來實現。

GetVersionExW大概是這麼實現的(這只是Windows2000的原始碼,後面的新系統,OSVERSIONINFOW這個結構多了幾倍的成員)。

WINBASEAPI BOOL WINAPI GetVersionExW(
    LPOSVERSIONINFOW lpVersionInformation)
{
    PPEB Peb;

    if (lpVersionInformation->dwOSVersionInfoSize != sizeof( *lpVersionInformation )) {
        SetLastError( ERROR_INSUFFICIENT_BUFFER );
       return FALSE;
        }

    Peb = NtCurrentPeb();
    lpVersionInformation->dwMajorVersion = Peb->OSMajorVersion;
    lpVersionInformation->dwMinorVersion = Peb->OSMinorVersion;
    lpVersionInformation->dwBuildNumber  =Peb->OSBuildNumber;
    lpVersionInformation->dwPlatformId  = Peb->OSPlatformId;
    wcscpy(lpVersionInformation->szCSDVersion,BaseCSDVersion );
    return TRUE;
}

其中BaseCSDVersion是個全域性變數,存放的是系統SP的字串資訊,在DLL初始化的時候就已經賦值了,由BaseDllInitialize來初始化。重點看下NtCurrentPeb這個函式,其實很顯然,GetVersionExW就是從PEB裡面去拷貝版本資訊。NtCurrentPeb是一個呼叫比較頻繁的函式,它返回當前程序的PEB結構地址,也就是通過fs暫存器去定位PEB,然後在GetVersionExW裡面把PEB裡面的系統版本資訊拷貝給GetVersionExW的傳出引數,也就是上面的OSMajorVersion等成員。

二、現在為什麼不行了

但是從Windows8.1出來之後,GetVersionExW這個API被微軟明文給廢棄了,這個坑下得可夠大的(參考[1])。也就是說從Windows8.1開始之後(包括Windows10),這個API常規情況下就是返回6.2了。
“In Windows 8.1, the GetVersion(Ex)APIs have been deprecated. That means that while you can still call the APIs,if your app does not specifically target Windows 8.1, you will getWindows 8 versioning (6.2.0.0).”

但是此時你去檢視應用軟體PEB的資訊,發現PEB裡面的系統版本還是正確的,在Windows10下面除錯了一下,發現但是GetVersionExW確實返回的是6.2,但是PEB裡面的版本則是6.4。也就是說微軟更改了這個API的實現。

去除錯微軟對這個API做了什麼改動意義不大,反正現在的結果就是這個API返回的值不對了,API也開始說謊了~不過在[1]裡面,微軟同時給出一個解決方案,嗯,一邊跟你說,這個API已經被廢棄了,一邊又說還是可以用的,這不是坑爹是什麼……解決方案是什麼呢?修改manifest檔案。加一段compatibility節點。

<?xml version="1.0"encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0"xmlns="urn:schemas-microsoft-com:asm.v1"xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
    <description> my appexe </description>
    <trustInfoxmlns="urn:schemas-microsoft-com:asm.v3">
       <security>
           <requestedPrivileges>
               <requestedExecutionLevel
                   level="asInvoker"
                   uiAccess="false"
               />    
           </requestedPrivileges>
       </security>
    </trustInfo>

   <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
       <application>
           <!-- Windows 8.1 -->
           <supportedOSId="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
           <!-- Windows Vista -->
           <supportedOSId="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
           <!-- Windows 7 -->
           <supportedOSId="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
           <!-- Windows 8 -->
           <supportedOSId="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
       </application>
    </compatibility>
</assembly>

主要就是compatibility部分了,如果你已經有manifest檔案了,只需要新增compatibility部分即可。對了Windows10怎麼辦?貌似[1]裡面還沒有說啊,別急,用

<!-- Windows 10 -->
<supportedOSId="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>

就好了,怎麼知道的?請叫我雷鋒!在Windows10下面測試一把的結果如圖。

三、相容模式的影響

還有一個可能的情況會造成GetVersionExW返回的系統版本和實際的系統版本不一樣。這個與Windows8.1,Windows10沒有什麼關係。純粹是為了相容考慮,在設定相容模式之後,GetVersionExW返回的是相容的目標版本的系統版本。啟動除錯去檢視應用程式的PEB是不是被修改過了,結果發現,並沒有修改過PEB。那麼問題來了,為什麼GetVersionExW的值發生變化了呢?

直接除錯GetVersionExW發現,在設定相容模式之後,微軟使用IATHook的方式,Hook了一堆的(嗯,不是1-2個,而是一堆)系統API,其中GetVersionExW就被AcLayers.dll裡面的一個函式給Hook了,然後Hook函式裡面返回了相容系統版本號。

四、怎樣判斷相容模式

一般來說,應用程式不需要判斷當前是否處於相容模式下執行,微軟實現這個機制的目的本意就是想對應用程式透明。主要是很多“古老的”程式內部嚴格限定只能在某個具體的系統下執行,譬如限定在WindowsXP SP3下執行(因為當時微軟的系統最高版本可能就是XP),這樣當用戶作業系統升級之後,譬如升級到了Windows7,這個時候問題來了!本來一般情況下微軟的系統是可以前向相容的,結果應用程式自己主動不相容,發現不是XP,主動退出,導致使用者用不了了,因此微軟發明了一個相容模式,高版本的系統可以模擬一個低版本的系統執行環境,這樣就解決大量的類似問題。

在相容模式下,當應用程式呼叫GetVersionExW等API時,返回的是相容的目標系統的系統版本,當然這只是相容模式技術解決的一個問題而已,但是是較重要的一個問題(相容模式還解決了很多其它問題)。

一般的應用程式不需要關心這個相容模式。但是某些特殊的應用程式卻恰恰需要,應用程式可能會根據不同的系統版本做不同的事情,而一個可能性是使用者誤把應用程式設定為某個低版本作業系統相容執行,導致整個程式執行反而異常。

舉個例子,像系統補丁修復程式,一般來說漏洞補丁都是和系統版本一一對應,如果程式使用GetVersionExW來獲取系統版本,那麼程式執行在Windows7下面,因為相容模式的影響,導致補丁修復程式推送了一大批WindowsXP下面的補丁,想想這個場景,也是有點尷尬的。

從大部分的使用場景上面來說,放棄使用GetVersionExW也許是一個更好的選擇。通過其它方式拿到更精確的系統版本,不用考慮相容模式的副作用,也不用擔心Windows8(主要是指Window8.1和Window10)以上的系統獲取到錯誤的系統版本。

那麼怎麼判斷當成程式正在相容模式執行呢?方法應該有很多,比較簡單的方法,[4]裡面介紹過一種,不過這種方法要注意,在Windows8.1之後,它可能給出錯誤的結果,要按照上面提到的辦法,讓GetVersionExW返回正確的值。

另外一種更好的方法是判斷登錄檔裡面的應用程式相容模式記錄列表,當把一個應用程式設定為相容模式或者管理員許可權啟動之後,系統會在HKEY_CURRENT_USER\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers下面記錄相應的資訊,如果想所有使用者起效,則修改HKEY_LOCAL_MACHINE\\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers即可。我們可以試試設定之後的效果。我在Windows7SP1下面隨意設定了幾個。

可以很清楚的看到兩個程式被設定為相容WIN7RTM執行和相容WINXPSP3執行,如果你去掉這兩個登錄檔值,則應用程式就不再以相容模式執行,因此實際上可以檢測這個位置判斷哪些程式被設定為相容模式執行,甚至可以通過刪除這裡的內容,去掉某些應用程式的相容模式設定。同時可以發現的是微軟用很容易識別的字串來描述相容的目標系統,更多的相容描述字串可以參考[3],另外要注意的是從Windows8開始,這些字串前面多了一個波浪線和空格(~ ),譬如相容WINXPSP3,在Windows10下面是~ WINXPSP3。

五、判斷系統版本更好的辦法

GetVersionExW既然被微軟廢棄了,再使用總覺得拔涼拔涼的,有什麼更好的判斷系統版本的方法嗎?答案是肯定的!下面給出幾種實踐中用過的方法。

1、首先從原理上來說,GetVersionExW是讀取的PEB裡面的版本資訊,其實我們自己也可以讀取PEB嘛,只是麻煩一點。這個就不給例子了。有興趣可以自己實現一下。

2、微軟在[1]裡面其實推薦過一批更好的API([7]),號稱介面名更人性化,從名字上面看確實含義更清晰了,不過使用起來是否方便就仁者見仁智者見智了,隨意羅列幾個,不過這套API宣告在<VersionHelpers.h>裡面,比較新的SDK才有。

VERSIONHELPERAPI IsWindows7OrGreater()

VERSIONHELPERAPIIsWindows7SP1OrGreater()

VERSIONHELPERAPI IsWindows8OrGreater()

VERSIONHELPERAPI IsWindows8_1OrGreater()

VERSIONHELPERAPI IsWindowsServer()

3、使用VerifyVersionInfo來進行版本判斷(參考[8]),這個API宣告在winbase.h裡面,從Windows2000系統就已經開始提供了,但是我們可能很少使用,說實話,使用起來不是特別方便。我們先看看是怎麼使用的,它本質是進行版本比較。

BOOL WINAPI VerifyVersionInfo(
 _In_  LPOSVERSIONINFOEX lpVersionInfo,
 _In_  DWORD dwTypeMask,
 _In_  DWORDLONG dwlConditionMask
);

這個函式的原型裡面第一個引數是熟悉的OSVERSIONINFOEX,但是這裡是做為傳入引數使用,第二個引數dwTypeMask用於指定要比較哪些項,可以比較主版本,次版本,Build號等等,可以使用位組合。第三個引數則是比較的方法,是>、=還是<,或者>=,<=等等,可以通過VER_SET_CONDITION來設定,可以進行各種組合來判斷,還是比較靈活的。看兩個例子吧。

BOOL IsWinVerGreaterThan(DWORDdwMajorVersion, DWORD dwMinorVersion)
{
    OSVERSIONINFOEXW osvi = {0};
    DWORDLONG dwlConditionMask = 0;

    ZeroMemory(&osvi, sizeof(osvi));
    osvi.dwOSVersionInfoSize= sizeof(osvi);
    osvi.dwMajorVersion= dwMajorVersion;
    osvi.dwMinorVersion= dwMinorVersion;

    // 主版本號判斷
    VER_SET_CONDITION(dwlConditionMask, VER_MAJORVERSION, VER_GREATER);
    if (::VerifyVersionInfoW(&osvi, VER_MAJORVERSION, dwlConditionMask))
       return TRUE;

    // 次版本號判斷
    VER_SET_CONDITION(dwlConditionMask, VER_MAJORVERSION, VER_EQUAL);
    VER_SET_CONDITION(dwlConditionMask, VER_MINORVERSION, VER_GREATER);

    return ::VerifyVersionInfo(&osvi, VER_MAJORVERSION | VER_MINORVERSION, dwlConditionMask);
}

 
//-------------------------------------------------------------------------
// 函式    : IsWinVerEqualTo
// 功能    : 判斷是否=某個特定的系統版本
// 返回值  : BOOL
// 引數    : DWORD dwMajorVersion
// 引數    : DWORD dwMinorVersion
// 附註    :
//-------------------------------------------------------------------------
BOOL IsWinVerEqualTo(DWORDdwMajorVersion, DWORD dwMinorVersion)
{
    OSVERSIONINFOEXW osvi = {0};
    DWORDLONG dwlConditionMask = 0;

    // 1、初始化系統版本資訊資料結構
    ZeroMemory(&osvi, sizeof(osvi));
    osvi.dwOSVersionInfoSize= sizeof(osvi);
    osvi.dwMajorVersion= dwMajorVersion;
    osvi.dwMinorVersion= dwMinorVersion;

    // 2、初始化條件掩碼
    VER_SET_CONDITION(dwlConditionMask, VER_MAJORVERSION, VER_EQUAL);
    VER_SET_CONDITION(dwlConditionMask, VER_MINORVERSION, VER_EQUAL);
  
   return ::VerifyVersionInfoW(&osvi, VER_MAJORVERSION | VER_MINORVERSION, dwlConditionMask);
}

封裝一下使用就更方便了,譬如要判斷當前是Window7,用IsWinVerEqualTo(6,1)即可。或者你不想暴露一些“噁心”的MagicNumber,可以再封裝一個IsWindows7()嘛。

4、[獲取作業系統版本方法1]還有一個我個人比較喜歡的方法是使用一個未文件化的函式來獲取系統版本,也就是RtlGetNtVersionNumbers,這個是NTDLL裡面的一個未文件化函式。但是這個函式微軟把它匯出了,因此我們就有辦法使用了。

使用方法:

//-------------------------------------------------------------------------
// 函式    : GetNtVersionNumbers
// 功能    : 呼叫RtlGetNtVersionNumbers獲取系統版本資訊
// 返回值  : BOOL
// 引數    : DWORD& dwMajorVer 主版本
// 引數    : DWORD& dwMinorVer 次版本
// 引數    : DWORD& dwBuildNumber build號
// 附註    :
//-------------------------------------------------------------------------
BOOL GetNtVersionNumbers(DWORD&dwMajorVer, DWORD& dwMinorVer,DWORD& dwBuildNumber)
{
    BOOL bRet= FALSE;
    HMODULE hModNtdll= NULL;
    if (hModNtdll= ::LoadLibraryW(L"ntdll.dll"))
    {
        typedef void (WINAPI *pfRTLGETNTVERSIONNUMBERS)(DWORD*,DWORD*, DWORD*);
        pfRTLGETNTVERSIONNUMBERS pfRtlGetNtVersionNumbers;
        pfRtlGetNtVersionNumbers = (pfRTLGETNTVERSIONNUMBERS)::GetProcAddress(hModNtdll, "RtlGetNtVersionNumbers");
        if (pfRtlGetNtVersionNumbers)
        {
           pfRtlGetNtVersionNumbers(&dwMajorVer, &dwMinorVer,&dwBuildNumber);
           dwBuildNumber&= 0x0ffff;
           bRet = TRUE;
        }

        ::FreeLibrary(hModNtdll);
        hModNtdll = NULL;
    }

    return bRet;
}

使用未文件化的函式要注意的一個點是,需要分析清楚函式的傳入引數的型別,否則傳錯了型別,如果型別大小不一樣,輕則函數出錯,重則程式崩潰(尤其是傳出引數)。我們可以看下RtlGetNtVersionNumbers這個函式是怎麼實現的(除錯用的ntdll.dll的版本是6.1.7601.18247,其它系統的也差不多的,僅僅是Hardcode的數字不一樣),下面是它的實現偽碼(IDA生成)。

int __stdcall RtlGetNtVersionNumbers(int a1, int a2, int a3)
{
 int result; // [email protected]

 if ( a1 )
    *(_DWORD *)a1 = 6;

 if ( a2 )
    *(_DWORD *)a2 = 1;

 result = a3;

 if ( a3 )
    *(_DWORD *)a3 = 0xF0001DB1u;

 return result;
}

我只能說微軟,你幹得漂亮!直接Hardcode處理,簡單幹淨!

5、還有一種方法是直接去獲取NTDLL這個系統關鍵檔案(其它的檔案也可行,但是實踐證明NTDLL最好)的檔案版本號,一般來說,該檔案的版本基本上就是系統的版本。像[4]裡面用到的判斷相容的方法就是通過對比GetVersionEx的返回值和關鍵系統檔案的版本,來判斷是否當前應用程式處理相容模式下

注:建議不要使用RtlGetVersion來進行版本判斷。Windows2003之前它的行為在相容模式下和GetVersionExW不一致,Vista之後在相容模式下它的行為和GetVersionExW一致。

6、[獲取作業系統版本方法2]還可以通過呼叫已經文件化的NetWkstaGetInfo API函式來獲取,具體說明可以檢視MSDN。程式碼如下:

		DWORD dwLevel = 100;
		LPWKSTA_INFO_100 lpWkStaInfo100 = NULL;
		NET_API_STATUS statusRet = NetWkstaGetInfo( NULL, dwLevel, (LPBYTE*)&lpWkStaInfo100 );
		if ( statusRet == NERR_Success )
		{
			os.dwMajorVersion = lpWkStaInfo100->wki100_ver_major;
			os.dwMinorVersion = lpWkStaInfo100->wki100_ver_minor;
		}

		// lpWkStaInfo100結構體指向的記憶體是NetWkstaGetInfo函式內部分配的,所以
        // 此處需要釋放一下
		if ( lpWkStaInfo100 != NULL )
		{
			NetApiBufferFree( lpWkStaInfo100 );
		}

檢視MSDN中該函式的出處:

所以在使用之前需要包含標頭檔案,引入lib庫:

#include <lm.h>  // 引用NetWkstaGetInfo API函式

#pragma comment( lib, "Netapi32.lib" )  // 引用NetWkstaGetInfo API函式

7、[獲取作業系統版本方法3]還可以通過獲取windows系統核心dll檔案的版本號來判斷,應該是沒問題的。程式碼如下:

BOOL32 Sys_IsWin8()
{
	BOOL32 bWin8 = FALSE;
	HINSTANCE	libHandle;
	libHandle = LoadLibraryW( L"kernel32.dll");
	wchar_t loadedPath[ MAX_OSPATH ];
	if ( libHandle ) {
		// since we can't have LoadLibrary load only from the specified path, check it did the right thing
		GetModuleFileNameW( libHandle, loadedPath, sizeof( loadedPath ) - 1 );
		MPrintf( "LoadLibrary '%s' wants to load '%s'\n", "kernel32.dll", loadedPath );
	}
	else
	{
		return bWin8;
	}
	DWORD dwHandle;
	u32 dwSize = GetFileVersionInfoSize((LPCWSTR)loadedPath, &dwHandle);
	wchar_t* pVersionData =  new wchar_t[dwSize];
	if(GetFileVersionInfo((LPCWSTR)loadedPath,dwHandle, dwSize,(void**)pVersionData))
	{
		UINT dwQuerySize = 0;
		VS_FIXEDFILEINFO *pTransTable ;
		u32 dwRet = VerQueryValueW(pVersionData, L"\\",(LPVOID*)&pTransTable, &dwQuerySize);
		if (dwRet)
		{
			u32 dwWinMajor = HIWORD(pTransTable->dwFileVersionMS);//major version number  
			u32 dwWinMinor = LOWORD(pTransTable->dwFileVersionMS); //minor version number   /*win8:2,win8.1:3*/
			if ((dwWinMajor == 6 && dwWinMinor == 2) || (dwWinMajor == 6 && dwWinMinor == 3))
			{
				bWin8 = TRUE;
			}
		}
	}	
	SAFE_DELETE_ARRAY(pVersionData)
	return bWin8;
}

8、[注意win8的兩個版本]win8有兩個版本:8.0版本對應6.2,8.1版本對應6.3,所以在判斷是否是win8版本時要使用這兩個版本,具體版本如下所示:

8.0版本的截圖:

8.1版本的截圖:

六、效果展示

分別在WindowsXP,Windows7,Windows10下面測試了這些方法。注意左邊的是常規模式執行,右邊的是相容模式執行。

參考文獻

[1] Operating system version changes inWindows 8.1 and Windows Server 2012 R2 http://msdn.microsoft.com/en-us/library/windows/desktop/dn302074(v=vs.85).aspx 

[2] GetVersionExhttp://msdn.microsoft.com/en-us/library/ms724451(VS.85).aspx

[3] Running an Application asAdministrator or in Compatibility Mode http://www.verboon.info/2011/03/running-an-application-as-administrator-or-in-compatibility-mode/

[4] http://blog.csdn.NET/magictong/article/details/5829065怎樣判定應用程式自身執行在“相容模式”下?

[5] http://blogs.msdn.com/b/chuckw/archive/2013/09/10/manifest-madness.aspxManifestMadness

[6] OSVERSIONINFOEX structure http://msdn.microsoft.com/en-us/library/ms724833(v=vs.85).aspx

[7] Version Helper functions http://msdn.microsoft.com/en-us/library/windows/desktop/dn424972(v=vs.85).aspx

[8] VerifyVersionInfofunction http://msdn.microsoft.com/en-us/ms725492(VS.85).aspx