为什么旧代码坏了?
that other answer 中的代码适用于旧版本的 Windows。具体来说,它可以顺利处理到 Windows 8(版本 6.2)。但正如您所注意到的,Windows 8.1(版本 6.3)和 Windows 10(版本 10.0)开始出现问题。代码看起来应该可以工作,但它正在为 Windows 8 之后的任何版本获取版本 6.2。
这样做的原因是 Microsoft 已决定更改 Windows 向应用程序报告其版本号的方式。为了防止旧程序错误地决定不在这些最新版本的 Windows 上运行,操作系统已将其版本号“峰值”为 6.2。虽然 Windows 8.1 和 10 仍分别具有内部版本号 6.3 和 10.0,但它们继续向旧应用程序报告其版本号为 6.2。从本质上讲,这个想法是“你无法处理真相”,所以它会被扣留给你。在后台,您的应用程序和系统之间存在compatibility shims,负责在您调用这些 API 函数时伪造版本号。
这些特殊的兼容性填充程序最初是在 Windows 8.1 中引入的,并且影响了几个版本信息检索 API。在 Windows 10 中,兼容性填充程序开始影响几乎所有版本号检索方式,包括尝试直接从系统文件中读取版本号。
事实上,这些旧版本的信息检索 API(如其他答案使用的 GetVersionEx 函数)已被 Microsoft 正式“弃用”。在新代码中,您应该使用 Version Helper functions 来确定 Windows 的底层版本。但是这些函数存在两个问题:
其中有一大堆——一个用于检测 Windows 的每个版本,包括“点”版本——并且它们不是从任何系统 DLL 导出的。相反,它们是在随 Windows SDK 分发的 C/C++ 头文件中定义的内联函数。这对 C 和 C++ 程序员来说非常有用,但是一个不起眼的 VB 6 程序员该怎么做呢?您不能从 VB 6 中调用任何这些“帮助”函数。
即使您可以从 VB 6 调用它们,Windows 10 也扩展了兼容性垫片的范围(如上所述),因此即使是 IsWindows8Point1OrGreater 和 IsWindows10OrGreater函数会骗你。
兼容性清单
理想解决方案,以及链接的 SDK 文档所暗示的解决方案,是在应用程序的 EXE 中嵌入一个带有兼容性信息的清单。 Manifest files 最初是在 Windows XP 中引入的,作为将元数据与应用程序捆绑在一起的一种方式,随着每个新版本的 Windows,可以包含在清单文件中的信息量都在增加。
清单文件的相关部分是一个名为compatibility 的部分。它可能看起来像这样(清单只是一个遵循特定格式的 XML 文件):
<!-- Declare support for various versions of Windows -->
<ms_compatibility:compatibility xmlns:ms_compatibility="urn:schemas-microsoft-com:compatibility.v1" xmlns="urn:schemas-microsoft-com:compatibility.v1">
<ms_compatibility:application>
<!-- Windows Vista/Server 2008 -->
<ms_compatibility:supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />
<!-- Windows 7/Server 2008 R2 -->
<ms_compatibility:supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows 8/Server 2012 -->
<ms_compatibility:supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 8.1/Server 2012 R2 -->
<ms_compatibility:supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!-- Windows 10 -->
<ms_compatibility:supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</ms_compatibility:application>
</ms_compatibility:compatibility>
它的工作方式是每个版本的 Windows(从 Vista 开始)都有一个 GUID,如果您的清单包含该 GUID 作为 supportedOS,那么系统知道您在 之后编写了应用程序该版本已发布。因此,假设您已准备好处理其重大更改和新功能,因此兼容性填充程序不会应用于您的应用程序。当然包括the original code使用的GetVersionEx函数。
如果您是一位尽职尽责的 Windows 开发人员,您很有可能已经在您的 VB 6 应用中嵌入了清单。您需要一个清单来获得主题控制(通过明确选择加入 ComCtl32.dll 的第 6 版),防止 UAC 虚拟化(通过仅请求 asInvoker 权限),甚至可能防止 DPI 虚拟化(通过将自己标记为高-DPI 感知)。您可以找到lots of information online,了解应用程序清单中的这些设置和其他设置如何工作。
如果您已在应用中嵌入清单文件,则只需将 Windows 8.1 和 Windows 10 GUID 添加到现有清单中即可。这将消除操作系统版本的谎言。
如果您还没有嵌入清单文件,那么您还有一些工作要做。 VB 6 是在清单被构思出来的几年前发布的,因此,IDE 没有任何内置工具来处理它们。你必须自己处理它们。见here for tips on embedding a manifest file in VB 6。总而言之,它们只是文本文件,因此您可以在记事本中创建一个并使用mt.exe(Windows SDK 的一部分)将其嵌入到您的 EXE 中。自动化此过程有多种可能性,或者您可以在完成构建后手动执行。
另一种解决方案
如果您不想对清单大惊小怪,还有另一种解决方案。它只涉及向您的 VB 6 项目添加代码,不需要任何类型的清单即可工作。
您可以调用另一个鲜为人知的 API 函数来检索 true 操作系统版本。它实际上是 GetVersionEx 和 VerifyVersionInfo 函数调用的内部内核模式函数。但是当您直接调用它时,您避免了通常会应用的兼容性填充程序,这意味着您可以获得真实的、未经过滤的版本信息。
这个函数被称为RtlGetVersion,正如链接文档所暗示的,它是一个供驱动程序使用的运行时例程。但是由于 VB 6 能够动态调用原生 API 函数的魔力,我们可以在我们的应用程序中使用它。以下模块展示了它的使用方法:
'==================================================================================
' RealWinVer.bas by Cody Gray, 2016
'
' (Freely available for use and modification, provided that credit is given to the
' original author. Including a comment in the code with my name and/or a link to
' this Stack Overflow answer is sufficient.)
'==================================================================================
Option Explicit
''''''''''''''''''''''''''''''''''''''''''''''''''
' Windows SDK Constants, Types, & Functions
''''''''''''''''''''''''''''''''''''''''''''''''''
Private Const cbCSDVersion As Long = 128 * 2
Private Const STATUS_SUCCESS As Long = 0
Private Const VER_PLATFORM_WIN32s As Long = 0
Private Const VER_PLATFORM_WIN32_WINDOWS As Long = 1
Private Const VER_PLATFORM_WIN32_NT As Long = 2
Private Const VER_NT_WORKSTATION As Byte = 1
Private Const VER_NT_DOMAIN_CONTROLLER As Byte = 2
Private Const VER_NT_SERVER As Byte = 3
Private Const VER_SUITE_PERSONAL As Integer = &H200
Private Type RTL_OSVERSIONINFOEXW
dwOSVersionInfoSize As Long
dwMajorVersion As Long
dwMinorVersion As Long
dwBuildNumber As Long
dwPlatformId As Long
szCSDVersion As String * cbCSDVersion
wServicePackMajor As Integer
wServicePackMinor As Integer
wSuiteMask As Integer
wProductType As Byte
wReserved As Byte
End Type
Private Declare Function RtlGetVersion Lib "ntdll" _
(lpVersionInformation As RTL_OSVERSIONINFOEXW) As Long
''''''''''''''''''''''''''''''''''''''''''''''''''
' Internal Helper Functions
''''''''''''''''''''''''''''''''''''''''''''''''''
Private Function IsWinServerVersion(ByRef ver As RTL_OSVERSIONINFOEXW) As Boolean
' There are three documented values for "wProductType".
' Two of the values mean that the OS is a server versions,
' while the other value signifies a home/workstation version.
Debug.Assert ver.wProductType = VER_NT_WORKSTATION Or _
ver.wProductType = VER_NT_DOMAIN_CONTROLLER Or _
ver.wProductType = VER_NT_SERVER
IsWinServerVersion = (ver.wProductType <> VER_NT_WORKSTATION)
End Function
Private Function GetWinVerNumber(ByRef ver As RTL_OSVERSIONINFOEXW) As String
Debug.Assert ver.dwPlatformId = VER_PLATFORM_WIN32_NT
GetWinVerNumber = ver.dwMajorVersion & "." & _
ver.dwMinorVersion & "." & _
ver.dwBuildNumber
End Function
Private Function GetWinSPVerNumber(ByRef ver As RTL_OSVERSIONINFOEXW) As String
Debug.Assert ver.dwPlatformId = VER_PLATFORM_WIN32_NT
If (ver.wServicePackMajor > 0) Then
If (ver.wServicePackMinor > 0) Then
GetWinSPVerNumber = "SP" & CStr(ver.wServicePackMajor) & "." & CStr(ver.wServicePackMinor)
Exit Function
Else
GetWinSPVerNumber = "SP" & CStr(ver.wServicePackMajor)
Exit Function
End If
End If
End Function
Private Function GetWinVerName(ByRef ver As RTL_OSVERSIONINFOEXW) As String
Debug.Assert ver.dwPlatformId = VER_PLATFORM_WIN32_NT
Select Case ver.dwMajorVersion
Case 3
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows NT 3.5 Server"
Exit Function
Else
GetWinVerName = "Windows NT 3.5 Workstation"
Exit Function
End If
Case 4
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows NT 4.0 Server"
Exit Function
Else
GetWinVerName = "Windows NT 4.0 Workstation"
Exit Function
End If
Case 5
Select Case ver.dwMinorVersion
Case 0
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows 2000 Server"
Exit Function
Else
GetWinVerName = "Windows 2000 Workstation"
Exit Function
End If
Case 1
If (ver.wSuiteMask And VER_SUITE_PERSONAL) Then
GetWinVerName = "Windows XP Home Edition"
Exit Function
Else
GetWinVerName = "Windows XP Professional"
Exit Function
End If
Case 2
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2003"
Exit Function
Else
GetWinVerName = "Windows XP 64-bit Edition"
Exit Function
End If
Case Else
Debug.Assert False
End Select
Case 6
Select Case ver.dwMinorVersion
Case 0
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2008"
Exit Function
Else
GetWinVerName = "Windows Vista"
Exit Function
End If
Case 1
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2008 R2"
Exit Function
Else
GetWinVerName = "Windows 7"
Exit Function
End If
Case 2
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2012"
Exit Function
Else
GetWinVerName = "Windows 8"
Exit Function
End If
Case 3
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2012 R2"
Exit Function
Else
GetWinVerName = "Windows 8.1"
Exit Function
End If
Case Else
Debug.Assert False
End Select
Case 10
If IsWinServerVersion(ver) Then
GetWinVerName = "Windows Server 2016"
Exit Function
Else
GetWinVerName = "Windows 10"
Exit Function
End If
Case Else
Debug.Assert False
End Select
GetWinVerName = "Unrecognized Version"
End Function
''''''''''''''''''''''''''''''''''''''''''''''''''
' Public Functions
''''''''''''''''''''''''''''''''''''''''''''''''''
' Returns a string that contains the name of the underlying version of Windows,
' the major version of the most recently installed service pack, and the actual
' version number (in "Major.Minor.Build" format).
'
' For example: "Windows Server 2003 SP2 (v5.2.3790)" or
' "Windows 10 (v10.0.14342)"
'
' This function returns the *real* Windows version, and works correctly on all
' operating systems, including Windows 10, regardless of whether or not the
' application includes a manifest. It calls the native NT version-info function
' directly in order to bypass compatibility shims that would otherwise lie to
' you about the real version number.
Public Function GetActualWindowsVersion() As String
Dim ver As RTL_OSVERSIONINFOEXW
ver.dwOSVersionInfoSize = Len(ver)
If (RtlGetVersion(ver) <> STATUS_SUCCESS) Then
GetActualWindowsVersion = "Failed to retrieve Windows version"
End If
' The following version-parsing logic assumes that the operating system
' is some version of Windows NT. This assumption will be true if you
' are running any version of Windows released in the past 15 years,
' including several that were released before that.
Debug.Assert ver.dwPlatformId = VER_PLATFORM_WIN32_NT
GetActualWindowsVersion = GetWinVerName(ver) & " " & GetWinSPVerNumber(ver) & _
" (v" & GetWinVerNumber(ver) & ")"
End Function
预期的公共接口是一个名为GetActualWindowsVersion 的函数,它返回一个字符串,其中包含实际 底层Windows 版本的名称。例如,它可能会返回 “Windows Server 2003 SP2 (v5.2.3790)” 或 “Windows 10 (v10.0.14342)”。
(完全在 Windows 10 上测试并运行!)
模块的公共函数调用了几个内部帮助函数,它们从the native RTL_OSVERSIONINFOEXW data structure 中解析信息,稍微简化了代码。如果您想花时间修改代码以提取它,则此结构中还有更多可用信息。例如,有一个包含标志的wSuiteMask 成员,其存在表示某些功能或产品类型。 GetWinVerName 帮助函数中显示了如何使用此信息的示例,其中检查 VER_SUITE_PERSONAL 标志以查看它是 Windows XP Home 还是 Pro。
最后的想法
对于这个问题还有其他几种“解决方案”在网上流传。我建议避免这些。
一个流行的建议是尝试从注册表中读取版本号。这是一个可怕的想法。注册表既不打算作为程序的公共接口,也不记录为程序的公共接口。这意味着此类代码依赖于随时可能更改的实现细节,让您回到损坏的情况——这正是我们首先要解决的问题!查询注册表永远不会比调用记录在案的 API 函数更有优势。
另一个经常被建议的选项是使用WMI 来检索操作系统版本信息。这比 Registry 更好,因为它实际上是一个文档化的公共接口,但它仍然不是一个理想的解决方案。一方面,WMI 是一个非常严重的依赖项。并非所有系统都会运行 WMI,因此您需要确保它已启用,否则您的代码将无法运行。如果这是您唯一需要使用 WMI 的事情,那么它会非常慢,因为您必须先等待 WMI 启动并运行。此外,从 VB 6 以编程方式查询 WMI 很困难。我们没有那些 PowerShell 人那么容易!但是,如果您仍然使用 WMI,这将是获取人类可读的操作系统版本字符串的便捷方法。你可以通过查询Win32_OperatingSystem.Name来做到这一点。
我什至见过像reading the version from the process's PEB block 这样的其他黑客攻击!当然,那是针对 Delphi,而不是 VB 6,而且由于 VB 6 中没有内联汇编,我什至不确定您是否能想出一个 VB 6 等价物。但即使在 Delphi 中,这也是一个非常糟糕的主意,因为它也依赖于实现细节。只是……不要。