转自:http://www.cnblogs.com/jiake/p/6093780.html
关于 GetVersion 系列接口
关于如何获取 Windows 系统版本号的话题,网上已经有了太多的帖子。但个人觉得总结的都不尽全面,或者没有给出比较稳定的解决方案。
众所周知,获取 Windows 系统版本的 API 是 GetVersion 和 GetVersionEx。这两个 API 的使用也都相当简单,一直被广泛使用 (下文中,我们将这两个统称为 GetVersion 系列)。后来在 Windows XP 中微软引入了应用程序兼容模式,可以选择兼容之前老的系统。可能很多人并不知道其具体的实现原理,以及能造成如何影响,微软官网开始也未对此详细说明。但直到后来,随着 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 走到今天这条路,微软也是没办法,这个行为一开始被修改就注定了今天被抛弃的结果。
官方推荐的备用方案
此外,微软也提供其他的几个 API 用来判断 (不能获取) 系统版本是否为特定版本,只是鲜为人知,使用频率较低。从 GetVersion 系列被抛弃开始,这些 API 才在 MSDN 被列出在 GetVersionEx 的说明页面,作为其他的备选方案。
IsOS
这个 API 是判断特定版本的,但是最高支持也就到 Windows 2003。此后,微软 MSDN 页面未对参数进行更新。
VerifyVersionInfo
该 API 要求配合 VerSetConditionMask 使用,才能进行后续判断。微软已经将 VerifyVersionInfo 封装为更易使用的函数。使用这些函数需包含 VersionHelpers.h 头文件,只有安装了较新版本的 Visual Studio 或 Windows SDK 才支持。
这组函数依然只能够用来判断 (不能获取) 系统版本。而且,根据 MSDN 的说明,其中的部分依然受到清单文件影响,但未测试。
NetWkstaGetInfo
这个 API 也是微软官方推荐的获取系统版本号的替代品之一。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#include
#include
#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;
}
|
经测试,获取的版本正常,不受兼容性和清单文件的影响,但在 dll 中调用会失败。
非官方备用方案
这里提供一些在网上搜索到的其他方案。由于部分使用了系统内部接口甚至数据结构,不保证后续依然有效。
查询 kernel32.dll 版本
通常情况下 kernel32.dll 的版本号和系统是同步的,但如果微软哪天不遵守这个约定,这个方法就不好用了。有的程序则是查询 ntoskrnl.exe 的版本信息,原理类似。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
#include
#include
#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 都拿不到正确结果了,微软还有什么能相信?好吧,你不给我正确结果,我就直接分析二进制总行了吧!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
#include
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 系统变更数据结构,造成任何蓝屏死机问题,本人概不负责。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
|
#include
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 中动态加载,本人就不列出详细代码,仅以静态调用作为示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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 上获取的结果是 6.2,仍可能受到清单文件的影响。
RtlGetNtVersionNumbers
同上,从 ntdll.dll 中加载。此接口系高手反编译所得,微软并未放出任何文档,请谨慎使用。Windows 2000 不支持,Windows XP 起支持。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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,目前看来是不会出问题的。