Практика
W8 или не W8 – вот в чем вопрос

:: Меню ::
:: На главную ::
:: FAQ ::
:: Заметки ::
:: Практика ::
:: Win API ::
:: Проекты ::
:: Скачать ::
:: Секреты ::
:: Ссылки ::

:: Сервис ::
:: Написать ::

:: MVP ::

:: RSS ::

Яндекс.Метрика


Сегодня хотелось бы поговорить о граблях, о которые набило шишку огромное (судя по интернет форумам) количество разработчиков. Имя им – GetVersion и GetVersionEx. Я был сильно удивлен, увидев, что выдают эти функции в Windows 8.1. Так, на планшете под управлением Windows 8.1 x86, вместо ожидаемого значения 6.3 я увидел значение 6.1, а на виртуальной машине под управлением Windows 8.1 x64 функция “порадовала” значением 6.2.

Объяснение такому поведению нашлось довольно быстро, в описании этих функций в MSDN. Вот что там написано:

With the release of Windows 8.1, the behavior of the GetVersion(Ex) API has changed in the value it will return for the operating system version. The value returned by the GetVersion(Ex) function now depends on how the application is manifested.

Applications not manifested for Windows 8.1 or Windows 10 will return the Windows 8 OS version value (6.2). Once an application is manifested for a given operating system version, GetVersion(Ex) will always return the version that the application is manifested for in future releases. To manifest your applications for Windows 8.1 or Windows 10, refer to Targeting your application for Windows.

В вольном переводе это означает то, что разработчик должен задекларировать совместимость приложения с операционными системами семейства Windows от версии 8 и выше (об этом хорошо написано в блоге GunSmoker-а).

К сожалению, мои попытки подружить приложение с манифестом и заставить функции GetVersion и GetVersionEx выдавать правильный результат не увенчались успехом. Ниже привожу содержание файла манифеста, с которым я эксперементировал.

<?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">

   <dependency>
      <dependentAssembly>
         <assemblyIdentity
            version="6.0.0.0"
            publicKeyToken="6595b64144ccf1df"
            name="Microsoft.Windows.Common-Controls"
            type="win32"
            processorArchitecture="*"
         />
      </dependentAssembly>
   </dependency>

   <description>Здесь - описание вашего приложения.</description>

   <trustInfo xmlns="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>
         <!-- This Id value indicates the application supports
              Windows Vista and Windows Server 2008 functionality -->
         <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/> 
         <!-- This Id value indicates the application supports
              Windows 7 and Windows Server 2008 R2 functionality -->
         <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
         <!-- This Id value indicates the application supports
              Windows 8 and Windows Server 2012 functionality -->
         <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
         <!-- This Id value indicates the application supports
              Windows 8.1 and Windows Server 2012 R2 functionality -->
         <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
         <!-- This Id value indicates the application supports
              Windows 10 functionality -->
         <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
      </application>
   </compatibility>
   
</assembly>

Было бы неплохо, если бы в настройках проекта появились соответствующие опциональные настройки для автоматической генерации корректного манифеста (имеющихся настроек явно недостаточно). Надеюсь когда-нибудь разработчики это сделают.


Тем не менее, этот подход является работоспособным, подтверждение чему я нашел и интернете и прикладываю в конце статьи (GetVersionEx.zip).

Также в MSDN есть ссылка на Version Helper functions, но поскольку корректность работы некоторых функций также зависит от манифеста (например IsWindows8Point1OrGreater и IsWindows10OrGreater), я не стал экспериментировать с ними.

В поисках ответа на вопрос, каким же еще образом можно определить точную версию Windows, удалось найти еще несколько решений.

Первое решение, которое у меня не вызывает особого доверия – определять версию Windows по версии библиотеки kernel32.dll, которая совпадает с версией Windows. На моем компьютере с Windows 7 это выглядит так:


Также для решения задачи можно прибегнуть к помощи WMI.

uses
  ActiveX, ComObj{, Variants};
 
procedure TForm1.Button1Click(Sender: TObject);
const
  WbemUser = '';
  WbemPassword = '';
  WbemComputer = 'localhost';
  wbemFlagForwardOnly = $00000020;
var
  FSWbemLocator: OLEVariant;
  FWMIService: OLEVariant;
  FWbemObjectSet: OLEVariant;
  FWbemObject: OLEVariant;
  oEnum: IEnumvariant;
  iValue: LongWord;
begin
   try
      FSWbemLocator := CreateOleObject('WbemScripting.SWbemLocator');
      FWMIService := FSWbemLocator.ConnectServer(WbemComputer, 'root\CIMV2', WbemUser, WbemPassword);
      FWbemObjectSet := FWMIService.ExecQuery('SELECT * FROM Win32_OperatingSystem', 'WQL', wbemFlagForwardOnly);
      oEnum := IUnknown(FWbemObjectSet._NewEnum) as IEnumVariant;
      while oEnum.Next(1, FWbemObject, iValue) = 0 do
      begin
         ShowMessage(Format('Версия: %s',[String(FWbemObject.Version)]));
         FWbemObject := Unassigned;
      end;
   except
      on E: EOleException do
         ShowMessage(Format('EOleException %s (%x)', [E.Message, E.ErrorCode]));
      on E: Exception do
         ShowMessage(E.Classname + ':' + E.Message);
   end;
end;
 
//initialization
//  CoInitialize(nil);
 
//finalization
//  CoUninitialize;

Можно прибегнуть к помощи функции NetServerGetInfo, возвращающей текущие сведения о конфигурации для указанного сервера (если параметр ServerName равен nil, используется локальный компьютер).

type
  NET_API_STATUS = DWORD;

  _SERVER_INFO_101 = record
    sv101_platform_id: DWORD;
    sv101_name: LPWSTR;
    sv101_version_major: DWORD;
    sv101_version_minor: DWORD;
    sv101_type: DWORD;
    sv101_comment: LPWSTR;
  end;
  SERVER_INFO_101 = _SERVER_INFO_101;
  PSERVER_INFO_101 = ^SERVER_INFO_101;
  LPSERVER_INFO_101 = PSERVER_INFO_101;

  function NetServerGetInfo(servername: LPWSTR; level: DWORD; var bufptr): NET_API_STATUS;
     stdcall; external 'Netapi32.dll';
  function NetApiBufferFree(Buffer: LPVOID): NET_API_STATUS; stdcall; external 'Netapi32.dll';

const
  MAJOR_VERSION_MASK = $0F;

procedure TForm1.Button1Click(Sender: TObject);
var
  Buffer: PSERVER_INFO_101;
begin
   if NetServerGetInfo(nil, 101, Buffer) = NO_ERROR then
   try
      // Выдает данные реальной ОС из режима совместимости
      ShowMessage(Format('NetServerGetInfo: %d.%d',
         [Buffer.sv101_version_major and MAJOR_VERSION_MASK, Buffer.sv101_version_minor]));
   finally
      NetApiBufferFree(Buffer);
   end;
end;

Или воспользоваться функцией NetWkstaGetInfo:

type
  WKSTA_INFO_100 = record
    wki100_platform_id: DWORD;
    wki100_computername: LPWSTR;
    wki100_langroup: LPWSTR;
    wki100_ver_major: DWORD;
    wki100_ver_minor: DWORD;
  end;
  LPWKSTA_INFO_100 = ^WKSTA_INFO_100;

  function NetWkstaGetInfo(ServerName: LPWSTR; Level: DWORD;
    BufPtr: Pointer): Longint; stdcall;
    external 'netapi32.dll' Name 'NetWkstaGetInfo';

function GetWindowsVersion: string;
var
  PBuf: LPWKSTA_INFO_100;
  Res: LongInt;
begin
   Result := '';
   Res := NetWkstaGetInfo(nil, 100, @PBuf);
   if Succeeded(Res) then
      Result := Format('%d.%d', [PBuf^.wki100_ver_major, PBuf^.wki100_ver_minor]);
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
   ShowMessage(GetWindowsVersion);
end;

Однако мне больше всего нравится способ с использованием PEB — структуры процесса в windows, заполняемой загрузчиком на этапе создания процесса, которая содержит информацию об окружении, загруженных модулях (LDR_DATA), базовой информации по текущему модулю и другие критичные данные, необходимые для функционирования процесса. Помимо всего прочего в PEB содержится и версия Windows, которую и нужно получить.

Адрес PEB - константа для всех процессов в системе. На 32 битных Windows указатель на PEB хранится в сегментном регистре FS:[0x30], а на 64 битных Windows в сегментном регистре GS:[0x60]. Получаем версию Windows из PEB:

function GetVersionPEB: Word;
asm
   {$IFDEF WIN32}
   mov edx, fs:[30h]
   mov eax, [edx+0A4h]
   shl eax, 8
   mov  al, [edx+0A8h]
   {$ELSE IFDEF WIN64}
   mov rdx, qword ptr GS:[abs $60]
   mov eax, [rdx+118h]
   shl eax, 8
   mov  al, [rdx+11Ch]
   {$ENDIF}
end;

Теперь выделить из результата мажорный и минорный номер – дело техники. Некоторые из этих техник хотелось бы продемонстрировать.

procedure TForm1.Button1Click(Sender: TObject);
var
  WinVer: Word;
  Major, Minor: Byte;
begin
   WinVer := GetVersionPEB;
   Major := HiByte(WinVer);
   Minor := LoByte(WinVer);

   ShowMessage(IntToStr(Major) + '.' + IntToStr(Minor));
end;

Это, пожалуй, самый очевидный вариант, не нуждающийся в комментариях. А вот следующие будут интереснее.

procedure TForm1.Button2Click(Sender: TObject);
type
  TWinVer = record
    case Integer of
      0: (Version: Word);
      1: (Minor, Major: Byte);
  end;
var
  WinVer: TWinVer;
begin
   WinVer.Version := GetVersionPEB;

   ShowMessage(IntToStr(WinVer.Major) + '.' + IntToStr(WinVer.Minor));
end;

В этом примере используется запись с вариантами, состоящей из необязательной фиксированной и вариантной частей. Вариантная часть напоминает условный оператор case. Между словами case и of записывается поле признака. Оно определяет, какой из вариантов в данный момент будет активизирован. Поле признака должно быть равно одному из расположенных следом значений, каждому из которых сопоставляется вариант записи. Он заключается в круглые скобки и отделяется от своего значения двоеточием. Вариантная часть не имеет отдельного end (как этого бывает у оператора case), слово end завершает и вариантную часть, и всю запись.

procedure TForm1.Button3Click(Sender: TObject);
type
  TWinVer = record
    Minor, Major: Byte;
  end;
var
  Ver: Word;
  WinVer: TWinVer absolute Ver;
begin
   Ver := GetVersionPEB;

   ShowMessage(IntToStr(WinVer.Major) + '.' + IntToStr(WinVer.Minor));
end;

Здесь использовано ключевое слово absolute, указывающее компилятору рассматривать одну или несколько переменных как существующие в одном месте в памяти (вроде вариантных записей), когда одна переменная перекрывает другую.

Вот теперь то мы точно будем знать с какой версией Windows имеем дело. Я же на этом заканчиваю. Всем творческих успехов!

.: GetVersionEx.zip :.
.: Пример к данной статье :.


При использовании материала - ссылка на сайт обязательна