本文并不是讨论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、还有一个我个人比较喜欢的方法是使用一个未文档化的函数来获取系统版本,也就是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; // eax@5 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一致。
效果展示
分别在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