1+1=10

扬长避短 vs 取长补短

获取Windows下的硬件信息

在Windows下获取硬件(比如CPU、主板、硬盘、显卡、网卡...)的信息,主要是通过WMI实现。

本文涉及

  • WMIC 工具使用【废弃】
  • Get-WmiObject 使用 【废弃】
  • Get-CimInstance 使用
  • python下wmi 使用
  • C++下 wmi 使用
  • Qt下通过ActiveQt使用wmi 【本文重点】
  • __cpuid 使用

术语

  • WMI:WIndows管理规范(Windows Management Instrumentation)。WMI是Windows操作系统提供的一种管理接口,从Windows 2000系统开始出现,它允许开发人员通过编程语言或脚本来访问和管理Windows系统的各种资源和服务。WMI通过提供COM/DCOM接口,使得开发人员可以从应用程序或脚本中直接调用WMI提供的功能来获取系统信息。WMI基于WBEM和CIM实现。
  • WBEM:基于Web的企业管理(Web Based Enterprise Management)。1996年由BMC、Cisco、Compaq、Intel和Microsoft发起。
  • CIM:公共信息模型(Common Information Model),是一个可扩展并面向对象的数据模型,含有与企业各个部门有关的信息。CIM也是由分布式管理任务组 (DMTF) 维护的跨平台标准。

WMI

WMI提供COM/DCOM接口,可以通过C/C++客户端、VBScript或JScript脚本、Powershell 等.NET客户端 访问。

早期,WMI架构中是没有.Net的:

wmi-architecture-from-msdn-0

后来,.Net占据重要位置(感觉COM和.Net混杂挺乱):

wmi-architecture-from-msdn

注:可关注wbemtest 这个测试工具。

wmic

  • https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/wmic

这个东西出镜率挺高,但是它已经 被废弃了。因为功能太强,容易被黑客利用。

The WMI command-line (WMIC) utility is deprecated as of Windows 10, version 21H1, and as of the 21H1 semi-annual channel release of Windows Server. 

只是wmic这个命令行工具被废弃了,不是WMI功能被废弃了。

常用命令

  • 查看cpu: wmic cpu list brief
  • 查看物理内存: wmic memphysical list brief
  • 查看逻辑内存: wmic memlogical list brief
  • 查看缓存内存: wmic memcache list brief
  • 查看虚拟内存: wmic pagefile list brief
  • 查看网卡: wmic nic list brief

具体一点的:

  • 获取cpu名称:wmic cpu get Name
  • 获取cpu核心数:wmic cpu get NumberOfCores
  • 获取cpu线程数:wmic cpu get NumberOfLogicalProcessors
  • 获取cpu序列号:wmic cpu get processorid
  • 获取主板序列号:wmic baseboard get serialnumber
  • 获取BIOS序列号:wmic bios get serialnumber
  • 获取硬盘序列号:wmic diskdrive where index=0 get serialnumber

Get-WmiObject

  • https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-wmiobject?view=powershell-5.1

这是Windows Powershell中的 cmdlet,在Windows Powershell 3.0中,已经被废弃,建议使用Get-CimInstance。而到了Powershell 6.0 (pwsh)中,它更是直接被移除了,比wmic还彻底。

一些命令:

  • 获取cpu信息:Get-WmiObject Win32_Processor
  • 获取BIOS信息:Win32_Bios

关于pwsh 与 powershell

https://github.com/powershell/powershell

从PowerShell 6开始,可执行程序的名字改为pwsh, 它使用 .NET Core,可以运行在Windows、Linux和macOS系统。

之前版本的PowerShell 只运行在Windows平台,叫做Window PowerShell,可执行程序的名字却为 powershell.exe

Get-CimInstance

  • https://learn.microsoft.com/en-us/powershell/scripting/learn/ps101/07-working-with-wmi?view=powershell-7.4
  • https://learn.microsoft.com/en-us/powershell/scripting/samples/getting-wmi-objects--get-ciminstance-?view=powershell-7.4

CIM是Powershell 3.0 引入的。至少目前没过时

  • 获取主板信息:
Get-CimInstance Win32_BaseBoard
  • 获取内存信息
Get-CimInstance Win32_PhysicalMemory
  • 获取CPU信息
Get-CimInstance Win32_Processor
  • 获取操作系统信息
Get-CimInstance Win32_OperatingSystem

注意:要查看具体有什么class可用,使用

Get-CIMClass

这个表很长,可以使用通配符过滤,比如:

 Get-CIMClass Win32*Processor

另外,微软官方上有列表: * Computer System Hardware Classes

Python

python有 wmi 的第三方包,它依赖于 pywin32包。用起来挺简单:

import wmi

c = wmi.WMI()

# information about the cpu
for cpu in c.Win32_Processor():
    print("Processor ID: %s" % cpu.ProcessorId.strip())
    print("Process Name: %s" % cpu.Name.strip())

# information about the baseboard
for board_id in c.Win32_BaseBoard():
    print("Board ID: %s" % board_id.SerialNumber.strip())

# information about the BIOS
for bios_id in c.Win32_BIOS():
    print("BIOS Serial Number: %s" % bios_id.SerialNumber.strip())

# get disk information
for disk in c.Win32_DiskDrive():
    print("Disk Serial Number: %s" % disk.SerialNumber.strip())
    print("Disk Drive Caption: %s" % disk.Caption)
    print("Disk Drive Size: %s" % disk.Size)

# get physical memory information
for memModule in c.Win32_PhysicalMemory():
    print("Memory Capacity: %.fMB" % (int(memModule.Capacity)))

# get information about the OS
for os in c.Win32_OperatingSystem():
    print("Windows Directory: %s" % os.WindowsDirectory)
    print("Operating System: %s" % os.Caption)
    print("OS Manufacturer: %s" % os.Manufacturer)
    print("OS Configuration: %s" % os.BuildNumber)
    print("OS Serial Number: %s" % os.SerialNumber)
    print("OS Version: %s" % os.Version)

C++

如何在C、C++中通过WMI获取硬件信息?

要使用 C++ 创建 WMI 应用程序:必须初始化 COM、访问和设置 WMI 协议以及执行手动清理。 MSDN上有说明和举例。

  • https://learn.microsoft.com/en-us/windows/win32/wmisdk/example--getting-wmi-data-from-the-local-computer

官方例子有中有点小问题,去掉using namespace或者将其起到最后。不然byte类型(C++17引入)会有二义性。

#define _WIN32_DCOM
#include <iostream>
using namespace std;
#include <comdef.h>
#include <Wbemidl.h>

#pragma comment(lib, "wbemuuid.lib")

主要步骤:

  • 通过 CoInitializeEx 初始化COM参数
  • 通过CoInitializeSecurity初始化COM进程安全性
  • 通过CoCreateInstance获取WMI初始定位符
  • 通过IWbemLocator::ConnectServer获取本地root\cimv2命名空间的IWebemServices指针
  • 使用IWbemServices指针向WMI发出请求。这个请求是真正需要的东西
  • 获取并显式查询数据
  • 清理等其他操作

Github上有不少C++的对wmi的封装

  • https://github.com/sondernextdoor/WindowsHardwareInfo
  • https://github.com/bozali/wmi
  • https://github.com/Thomas-Sparber/wmi
  • https://github.com/sonodima/wmipp

Qt

Qt是C++的库,所以前面的C++中的东西都可以直接用。只是需要注意Qt中使用COM的一些坑...

Qt官方的qtsystems模块仓库中有具体封装wmi的例子(没有用ActiveQt):

  • https://github.com/qt/qtsystems/blob/dev/src/systeminfo/windows/qwmihelper_win.cpp

Qt 与 COM初始化

注意:Qt在Windows下为了支持剪切板,所以QGuiApplication初始化时已经为主线程调用了 OleInitialize() 函数,一旦再次调用CoInitializeEx() 且使用不用的并发标志,会造成崩溃等问题。

由于 OLE 技术不是线程安全的, 因此 OleInitialize 函数使用 COINIT_APARTMENTTHREADED 标志调用 CoInitializeEx

MSDN中的Initializing the COM Library中,

  • 提及CoInitializeEx时,说每个用到COM的线程都要调用它:
Each thread that uses a COM interface must make a separate call to this function.
  • 提及主线程时,说到主线程用COINIT_APARTMENTTHREADED
Generally, a thread that creates a window should use the COINIT_APARTMENTTHREADED flag, and other threads should use COINIT_MULTITHREADED. 

MSDN中的 CoInitializeEx 函数 (combaseapi.h) 中,

  • 提及 CoInitializeEx 可以多次调用,只要并发标志相同即可。
对于使用 COM 库的每个线程,CoInitializeEx 必须至少调用一次,并且通常只调用一次。 只要同一线程传递相同的并发标志,就允许同一线程多次调用 CoInitializeEx ,但后续的有效调用将返回S_FALSE。 若要在线程上正常关闭 COM 库,每次成功调用 CoInitialize 或 CoInitializeEx(包括返回S_FALSE的任何调用)都必须通过对 CoUninitialize 的相应调用进行平衡。

QAxObject

Qt中有ActiveQt模块,所以可以减少一些工作。

注:执行Qt自带的dumpdoc可以生成一个帮助文档。

dumpdoc WbemScripting.SWbemLocator -o help.html

对于这个例子,由于涉及好多个QAxObject,我们可以需要对每个对象,都生成一个文档,这个最好在代码中进行,具体示例见下一节。

使用ActiveQt模组,我们可以写一个 WmiHelper 类:

// demo file by dbzhang800 2023
#include <QApplication>
#include <QDebug>
#include <QUuid>
#include <QThread>
#include <QFile>
#include <QAxObject>
#include <Windows.h>

class WmiHelper : public QObject
{
public:
    WmiHelper(QObject *parent=nullptr)
        :QObject(parent)
    {
        // try to make Qt's GUI Thread happy.
        if (CoInitializeEx(0, COINIT_MULTITHREADED) == RPC_E_CHANGED_MODE)
            CoInitializeEx(0, COINIT_APARTMENTTHREADED);
        wbemLocator = new QAxObject("WbemScripting.SWbemLocator");
        wmiService = wbemLocator->querySubObject(
            "ConnectServer(QString&,QString&)", ".", "ROOT\\CIMV2");
    }

    ~WmiHelper()
    {
        delete wbemLocator;
        CoUninitialize();
    }

    QString queryInfo(const QString &className, const QString &name)
    {
        QString info;

        QAxObject *resultList = wmiService->querySubObject("ExecQuery(QString&))",
                                   QString("SELECT %1 FROM %2").arg(name, className));
        auto count = resultList->property("Count").toInt();
        for (int i=0; i<count; ++i) {
            QAxObject *item = resultList->querySubObject("ItemIndex(int)", i);
            auto latin1 = name.toLatin1();
            info = item->dynamicCall(latin1.data()).toString();
        }
        resultList->clear();
        return info;
    }

private:
    QAxObject *wbemLocator {nullptr};
    QAxObject *wmiService {nullptr};
};

然后我们可以在Qt的各处直接使用了(Gui线程,QCoreApplication线程,其他次线程):

int main(int argc, char *argv[])
{
  QGuiApplication a(argc, argv);

  WmiHelper wmiHelper;
  qDebug() << "cpu: " << wmiHelper.queryInfo("Win32_Processor", "ProcessorId");
  qDebug() << "bios: " << wmiHelper.queryInfo("Win32_BIOS", "SerialNumber");
  qDebug() << "baseboard: " << wmiHelper.queryInfo("Win32_BaseBoard", "SerialNumber");
  qDebug() << "disk: " << wmiHelper.queryInfo("Win32_DiskDrive", "SerialNumber");
  qDebug() << "nic: " << wmiHelper.queryInfo("Win32_NetworkAdapter", "MACAddress");
  qDebug() << "os: " << wmiHelper.queryInfo("Win32_OperatingSystem", "SerialNumber");
  qDebug() << "cpu name: " << wmiHelper.queryInfo("Win32_Processor", "Name");
  qDebug() << "os name: " << wmiHelper.queryInfo("Win32_OperatingSystem", "Caption");

  return a.exec();
}

generateDocumentation

适当使用 QString QAxBase::generateDocumentation() 生成帮助文档,对我们使用ActiveQt有帮助,比如前面的代码,可以加上:

  auto writeDoc = [](const QString &fileName, const QByteArray &content){
      QFile f(fileName);
      f.open(QFile::WriteOnly);
      f.write(content);
      f.close();
  };

  wbemLocator = new QAxObject("WbemScripting.SWbemLocator");
  writeDoc("1.html", wbemLocator->generateDocumentation().toUtf8());
  wmiService = wbemLocator->querySubObject(
      "ConnectServer(QString&,QString&)", ".", "ROOT\\CIMV2");
  writeDoc("2.html", wmiService->generateDocumentation().toUtf8());

得到:

activeqt-iswbemlocator

以及

activeqt-iswbemservicesex

activeqt-iswbemobjectset

__cpuid, __cpuidex

除了WMI,CPU的ID可以通过 __cpuid__cpuidex 来获取。

#include <intrin.h>

#include <iostream>
#include <array>
#include <vector>
#include <sstream>
#include <iomanip>

int main()
{
    std::array<int, 4> cpuInfo;

    // Calling __cpuid with 0x0 as the function_id argument
    // gets the number of the highest valid function ID.
    __cpuid(cpuInfo.data(), 0x0);
    const auto nIds = cpuInfo[0];

    std::vector<std::array<int, 4>> cpuInfos;
    for (int i=0; i <= nIds; ++i)
    {
        __cpuidex(cpuInfo.data(), i, 0);
        cpuInfos.push_back(cpuInfo);
    }

    // vender name in data[0]: 1, 3, 2
    std::string vendor(12, '0');
    *reinterpret_cast<int*>(vendor.data()) = cpuInfos[0][1];
    *reinterpret_cast<int*>(vendor.data() + 4) = cpuInfos[0][3];
    *reinterpret_cast<int*>(vendor.data() + 8) = cpuInfos[0][2];
    std::cout << "CPU Vendor is : " << vendor << std::endl;

    // serial number in data[1]: 3, 0
    std::stringstream stream;
    for (int i=3; i>=0; i-=3)
        stream << std::setfill('0') << std::setw(8) << std::uppercase << std::hex << cpuInfos[1][i];
    std::cout << "ProcessorId is : " << stream.str() <<  std::endl;

    return 0;
}

运行结果

CPU Vendor is : GenuineIntel
ProcessorId is : BFEBFBFF806C1

参考

  • https://devblogs.microsoft.com/scripting/powertip-list-all-available-cim-classes-by-using-powershell/
  • http://timgolden.me.uk/python/wmi/index.html
  • https://pypi.org/project/WMI/
  • https://www.codeproject.com/Articles/10539/Making-WMI-Queries-In-C
  • https://github.com/qt/qtsystems/tree/dev/src/systeminfo
  • https://github.com/jiangzhw/MachineCodeCal
  • https://learn.microsoft.com/en-us/cpp/intrinsics/cpuid-cpuidex?view=msvc-170
  • https://en.wikipedia.org/wiki/CPUID

Tools Qt

Comments