也谈如何获取真实正确的 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,目前看来是不会出问题的。

上一篇:打FFT时中发现的卡常技巧


下一篇:APP缓存数据线程安全问题