ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于VC++的Windows硬件信息采集工具:WMI、API与实战

基于VC++的Windows硬件信息采集工具:WMI、API与实战 简介一套面向VC开发者的Windows系统硬件信息获取示例工程适合正在编写系统监控、性能分析或自动化运维工具的程序员学习。压缩包共23个文件约40KB核心为4个C源文件与5个头文件并附带完整的工程配置、对话框界面资源、使用说明文档等结构清晰可直接打开编译和二次修改。已有336人学习/下载适合作为入门参考。示例围绕GetSystemInfo、GlobalMemoryStatusEx、GetDiskFreeSpaceEx、GetPerformanceInfo等Windows API展开演示如何获得CPU类型、物理内存、磁盘剩余空间等关键参数并通过MFC对话框完整展示信息获取与界面绑定的实现流程同时涉及注册表查询、错误处理等系统编程细节。读者可在此基础上扩展WMI调用或第三方库进一步获取主板型号、设备序列号等深层硬件信息为构建更庞大的系统监控与性能分析工具打下扎实基础。 最近在整理内部工具库的时候重新把一个用VC编写的Windows硬件信息采集小工程翻了出来顺手把代码和文档打包成了zip给同事用。这个工具的功能不复杂在一台Windows机器上把CPU、内存、硬盘、显卡、主板、BIOS这些硬件信息一次性读出来输出成文本或者Json方便后续做资产登记、软件授权绑定、远程故障排查。开发这类工具时网上能搜到一堆半成品但真正能在目标机器上顺畅跑起来、不乱码、不崩、不卡死的其实不多。这篇文章我就把这个zip工程里的设计思路、核心实现、踩过的坑完整展开说一下给准备写Windows硬件信息采集、系统诊断工具、软件授权校验的朋友做一个参考。1. 项目整体设计与技术选型1.1 为什么用VC而不用脚本或C#.NET先回答一个肯定会被问到的问题都什么年代了读个硬件信息还用VC因为使用场景决定了技术选型。这个工具做出来是要在各类Windows机器上跑的尤其是那些不能保证联网、不能保证有最新.NET运行库的办公电脑和工控机。C#确实写起来舒服但部署时要考虑目标机的.NET Framework版本问题PowerShell也能做但脚本分发麻烦还要处理执行策略限制。VC编译出来的程序是一个独立exe选对运行库之后基本不需要预装环境拿U盘拷过去就能跑这是最关键的。另外Windows本身的系统API和WMI接口都是原生COM接口VC/C是访问它们最直接的语言不会隔一层托管运行时造成额外的类型转换问题。内部工具场景下二进制体积和兼容性是第一位的所以选VC是务实的选择不是情怀。1.2 三条获取硬件信息的技术路线对比在Windows上取硬件信息归纳起来就三条路Windows API、WMI、注册表/设备信息文件。我刚做第一版的时候三条路都试了一遍各有优缺点。Windows API走的是系统底层接口比如GetSystemInfo获取CPU架构和逻辑处理器个数、GlobalMemoryStatusEx获取物理内存大小、GetAdaptersInfo获取网卡MAC地址。优点是调用快、稳定性高、几乎没有权限要求缺点是能拿到的字段有限像CPU的型号名称、主板的厂商型号这类信息API直接获取不到必须绕道。WMI是Windows管理规范通过COM接口访问root\cimv2命名空间下的各个类。它是一套非常完整的内存数据库Windows自己维护的硬件信息基本都能查到Win32_Processor、Win32_BaseBoard、Win32_VideoController这些类都是现成的。缺点是WMI查询需要初始化COM首次查询可能遇到几秒延迟一旦WMI服务异常就会整个卡住。注册表方式主要看HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\System和HKEY_LOCAL_MACHINE\HARDWARE\DEVICEMAP两个位置能读到CentralProcessor、BIOS等信息。特点是读取快但注册表在Win10/11中有很多字段是动态生成的系统未初始化完整时会拿到空值且部分键值被系统保护程序以普通权限访问会失败。三种路线的实际差别我整理成了下面这个表方便对比技术路线查询速度信息完整度典型用途主要风险Windows API极快基本毫秒级低只有基础计数和容量类逻辑核心数、内存总量、MAC地址依赖系统版本老接口在Win10可能废弃WMI慢首个查询可能超过1秒高硬件型号、厂商、序列号等都有CPU、内存条、显卡、主板、BIOS依赖Winmgmt服务权限不足时部分属性为空注册表快中能拿到固件描述类信息CPU描述、BIOS版本、部分设备状态键值格式不稳定易受损坏影响最终我的方案是“API拿基础、WMI拿细节、注册表兜底”先用API获取核心数和系统内存总量再用WMI补充CPU型号、显卡型号、主板信息、磁盘型号注册表只作为取数失败后的降级方案。这个组合在实际使用中稳定性最好即使WMI服务出问题工具也能退回到仅显示基础信息不至于完全不可用。2. 核心流程与关键接口说明2.1 整体流程拆解整个程序的核心流程可以归纳为初始化COM环境 - 连接WMI服务 - 按需查询各类硬件信息 - 解析并输出结果 - 释放资源。第一步初始化COM非常关键很多人写的代码运行时第一次调用就被挂住原因就是COM没初始化。调用CoInitializeEx时要注意线程模式在主线程里用COINIT_APARTMENTTHREADED如果在工作线程里执行查询则用COINIT_MULTITHREADED。这个模式选错WMI连接在某些机器上会出现RPC_E_CHANGED_MODE错误。第二步连接WMI服务通过IWbemLocator接口的ConnectServer方法指向root\cimv2命名空间。如果目标机器上WMI服务被禁用这里会返回失败代码里必须做判断并给出降级方案。第三步是逐步查询Win32_Processor、Win32_BaseBoard、Win32_BIOS、Win32_ComputerSystemProduct、Win32_VideoController、Win32_DiskDrive、Win32_PhysicalMemory等类用IWbemServices的ExecQuery方法。第四步解析返回的IEnumWbemClassObject枚举器逐个读取属性值。这里涉及的VARIANT类型转换要注意BSTR字符串记得用SysAllocString和SysFreeString配套管理避免内存泄漏。最后一步是逆序释放COM资源包括IWbemLocator、IWbemServices、枚举器最后CoUninitialize。2.2 常用WMI类与实际能拿到的字段WMI里和硬件相关的类非常庞杂没必要全查实际用到的就那么几个。我整理了高频使用的类及其常用属性给新入门的朋友做个速查WMI类名用途常用属性备注Win32_ProcessorCPU信息Name、NumberOfCores、NumberOfLogicalProcessors、ProcessorId、MaxClockSpeedProcessorId在虚拟机上取到的值可能统一需单独处理Win32_BaseBoard主板信息Manufacturer、Product、SerialNumber品牌机SerialNumber常常被固件隐藏Win32_BIOSBIOS信息Manufacturer、SMBIOSBIOSVersion、ReleaseDate、SerialNumberReleaseDate是UTC时间转本地时要加8小时Win32_ComputerSystemProduct整机信息Vendor、Name、Version、IdentifyingNumber查整机序列号最可靠的类Win32_VideoController显卡信息Name、AdapterRAM、DriverVersion、VideoModeDescriptionAdapterRAM对显存大于4GB的显卡经常溢出值会显示负数Win32_DiskDrive物理硬盘Model、SerialNumber、Size、InterfaceTypeSize的单位是字节要除以1GB转GB显示Win32_PhysicalMemory物理内存条Capacity、Speed、Manufacturer、PartNumber部分主板固件不上报PartNumber值会为空Win32_NetworkAdapter网卡信息MACAddress、AdapterType、NetConnectionID只取物理网卡过滤掉虚拟网卡判断依据是PhysicalAdapter属性每个类查出来之后得到的都是IEnumWbemClassObject枚举器要遍历取第一个对象因为硬件组件通常只有一个实例。如果是Win32_PhysicalMemory这种可能有多根内存条的就要遍历全部对象并累加Capacity。2.3 ANSI和Unicode函数选择这个坑一定要避开VC写Windows程序绕不开字符集问题。现代Windows系统默认走UnicodeWMI返回的BSTR也是宽字符所以工程字符集必须设置为“使用Unicode字符集”。为什么强调这点因为一旦用了ANSI函数处理WMI返回的字符串中文硬件型号十有八九会变成乱码。网上很多老代码还在用char*配合printf输出这在控制台程序里勉强能看但一旦涉及到文件输出或者GUI显示乱码问题会非常严重。处理办法是统一用wchar_t和宽字符函数。具体写代码时我习惯直接用WideCharToMultiByte做一次统一转换把宽字符串转成UTF-8字节再输出到文件这样生成的文本文件在Notepad和VS Code里都不会出现乱码。别再用系统默认的本地代码页在简体中文系统上写出来的ANSI文本拿到繁体系统或者英文系统上就是一堆问号跨语言环境的兼容性很差。3. 代码实现与编译注意事项3.1 工程配置要点新建一个Visual Studio控制台应用程序项目按下面这张清单配置基本不会踩坑平台工具集选当前已安装的VS版本即可老项目注意别选错比如开发环境是VS2019目标机器是Win7工具集用v142字符集选“使用Unicode字符集”坚决不用多字节字符集运行库在Release版本下选“多线程(/MT)”这样程序不需要依赖目标机器上的VC运行库这个点下面细说C语言标准选C14不用太高太新的特性因为内部工具要保证在最大范围的编译器上能重新编译。依赖的库主要有Ole32.lib、OleAut32.lib、WbemUuid.lib。这三个是WMI编程必需的属性页的“链接器-输入-附加依赖项”里加进去不然后期编译会报一堆unresolved external symbol。3.2 核心代码片段解读下面这一段是我获取CPU信息并输出到控制台的简化实现保留了主体结构方便直接参考。#include windows.h #include wbemidl.h #include comdef.h #include iostream #pragma comment(lib, wbemuuid.lib) bool QueryCpuInfo() { HRESULT hres; // 1. 初始化COM hres CoInitializeEx(0, COINIT_MULTITHREADED); if (FAILED(hres)) { std::wcerr LCOM初始化失败: 0x std::hex hres std::endl; return false; } // 2. 创建WMI定位器 IWbemLocator* pLoc nullptr; hres CoCreateInstance(CLSID_WbemLocator, 0, CLSCTX_INPROC_SERVER, IID_IWbemLocator, (LPVOID*)pLoc); if (FAILED(hres)) { CoUninitialize(); return false; } // 3. 连接WMI IWbemServices* pSvc nullptr; hres pLoc-ConnectServer(_bstr_t(LROOT\\CIMV2), NULL, NULL, 0, NULL, 0, 0, pSvc); if (FAILED(hres)) { pLoc-Release(); CoUninitialize(); return false; } // 4. 执行查询 IEnumWbemClassObject* pEnumerator nullptr; hres pSvc-ExecQuery(bstr_t(LWQL), bstr_t(LSELECT * FROM Win32_Processor), WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, NULL, pEnumerator); if (FAILED(hres)) { pSvc-Release(); pLoc-Release(); CoUninitialize(); return false; } // 5. 遍历结果 IWbemClassObject* pObj nullptr; ULONG uReturn 0; while (pEnumerator) { hres pEnumerator-Next(WBEM_INFINITE, 1, pObj, uReturn); if (uReturn 0) break; VARIANT vtProp; // 读取CPU名称 if (SUCCEEDED(pObj-Get(LName, 0, vtProp, 0, 0))) { if (vtProp.vt VT_BSTR) { std::wcout LCPU: vtProp.bstrVal std::endl; } VariantClear(vtProp); } // 读取物理核心数和逻辑核心数 if (SUCCEEDED(pObj-Get(LNumberOfCores, 0, vtProp, 0, 0))) { std::wcout L物理核心数: vtProp.uintVal std::endl; VariantClear(vtProp); } if (SUCCEEDED(pObj-Get(LNumberOfLogicalProcessors, 0, vtProp, 0, 0))) { std::wcout L逻辑核心数: vtProp.uintVal std::endl; VariantClear(vtProp); } pObj-Release(); } // 6. 释放资源 pEnumerator-Release(); pSvc-Release(); pLoc-Release(); CoUninitialize(); return true; }判断属性值类型时VARIANT的vt字段是关键。大部分数值字段是VT_UI4或VT_UINT字符串字段是VT_BSTR但偶尔会遇到NULL值读取前先判断vt ! VT_NULL避免拿空指针操作造成崩溃。获取其他硬件信息时把SQL语句换成对应的类名属性名相应调整即可整体流程不用动。我会把每个硬件类封装成独立的查询函数比如QueryCpuInfo、QueryMotherBoardInfo、QueryDiskInfo这样主函数里依次调用单个查询失败不会影响其他模块。3.3 静态链接VC运行库告别“缺少VCRUNTIME140.dll”编译阶段一个经常被忽略的细节是运行库选择。VS默认的Release配置用的是“多线程DLL(/MD)”意味着编出来的exe依赖目标机器上的VC可再发行运行库。如果目标机器没装或者装的版本不对双击程序就会弹出“由于找不到VCRUNTIME140.dll无法继续执行代码”。对于内部工具最省心的办法是改静态链接项目属性 - C/C - 代码生成 - 运行库改成“多线程(/MT)”这样CRT运行库直接打进exe里目标机器上什么环境都不用装。代价是exe体积会大一些以我的实际经验看一个简单的控制台程序体积大概增加200到300KB对于几百KB的硬件采集工具来说完全能接受。顺带说一句如果你的工具要配合其他程序调用最好同时加上InitCommonControlsEx之类的初始化避免在Win7上显示界面时出现控件样式异常。4. 常见问题与排查技巧实录4.1 WMI查询失败或长时间卡住的排查思路WMI查询在大多数机器上是稳定的但总有例外。运维同事之前反馈说有台机器工具跑起来鼠标转圈几十秒才出结果最后发现Winmgmt服务之前被优化软件关掉了程序一直在等待WMI响应。遇到这类情况先手动在命令行执行wbemtest确认能否连接到root\cimv2。如果wbemtest连不上基本就是WMI Service异常可以命令行输入net start winmgmt重新启动服务或者用管理员身份运行“sfc /scannow”。代码层面则必须在ExecQuery调用后设置合理的等待逻辑查询放后台线程超时后主动放弃避免采集工具本身变成故障排查的负担。4.2 硬件型号为空、序列号显示“To be filled by O.E.M.”这种情况很常见尤其是品牌机和组装机用了一块工包主板时。原因是主板固件写入的SMBIOS信息不完整或者厂商故意隐藏了部分字段。如果你写的工具只做显示用途直接输出“未知/不可用”即可如果做软件授权绑定一定要过滤这类占位字符串否则两个不同品牌的机器会因为都显示“To be filled by O.E.M.”而生成相同的机器码授权校验直接失效。过滤规则我一般这样做字符串长度为0、包含“To be filled”、“Default string”、“Not Specified”等关键字的统统视为无效。对授权场景最好把CPU的ProcessorId、网卡MAC、磁盘序列号这三个字段做哈希组合匹配概率和抗碰撞能力都比单用一个序列号强得多。4.3 注册表设备信息损坏导致读取异常或设备不可用热搜词里有一条“摄像头由于其配置信息(注册表中的)不完整或已损坏Windows无法启动这个硬件设备”这个错误在设备管理器的属性页里很常见。本质是系统在设备枚举时读到的注册表配置键值已经损坏导致设备驱动加载失败摄像头这类即插即用设备经常中招。我写的采集程序一般不会直接去读设备实例的注册表枚举键因为一旦某个分支损坏RegQueryValueEx可能会返回异常数据甚至引起程序等待。正确的处理方式是走WMI的设备类查询比如Win32_PnPEntity只取ConfigManagerErrorCode字段用它来判断设备状态。若要修复摄像头这类设备比较稳妥的操作是设备管理器里先卸载设备再扫描检测硬件改动让系统重新生成注册表项还不行就卸载驱动后重新安装。4.4 多块显卡、多块硬盘导致的信息重复带核显和独显双显卡的机器Win32_VideoController会返回两个对象有多个硬盘的机器Win32_DiskDrive也会返回多个对象。很多初写者在遍历时只取第一个导致出报表时漏掉独显或第二块硬盘。处理方法是遍历全部对象并在输出时标明序号。显卡判断哪块是独显可以参考PNPDeviceID里是否包含“PCI\VEN_”和DeviceID字段的厂商代号硬盘则直接用Index字段区分磁盘顺序。如果只关心主硬盘做机器码可以优先选择InterfaceType为SCSI/ATA/NVMe且Size最大的那块这个规则在绝大多数台式机和笔记本上都成立。4.5 常见问题速查表现象可能原因排查方法解决方案程序缺失VCRUNTIME140.dll目标机没有VC运行库检查VC运行库是否安装编译时使用MT静态链接或在目标机安装VC运行库WMI返回空字符串SMBIOS信息缺失用wbemtest手动验证过滤占位符改用其他类兜底中文型号乱码使用了ANSI函数处理BSTR检查代码中是否用printf输出全面切换到Unicode和WideCharToMultiByte显卡显存显示负数AdapterRAM字段溢出查看AdapterRAM是不是超过4GB改用Win32_VideoController的VideoModeDescription程序在部分Win7上无法运行缺少UCRT或者操作系统不方便检查系统升级补齐系统组件编译时目标平台选择Win7并在程序清单声明兼容摄像头等关联设备初始化异常注册表配置分支损坏设备管理器中查看错误代码释放设备驱动并重新扫描必要时重装驱动采集进程偶尔CPU占用高首次调用COM/WMI初始化耗时观察任务管理器进程占用将查询操作放到后台线程设置超时保护结果缓存常用字段5. 从硬件信息采集到设备指纹的几个扩展点5.1 用多个字段生成稳定的机器码拿到硬件信息之后最常做的事就是生成设备唯一标识。有些人单独取CPU的ProcessorId但虚拟机环境下这个值会重复有人取主板序列号结果品牌机的序列号字段被厂商刻意隐藏。我最终用的是CPU ProcessorId、主板ManufacturerProduct、物理网卡MAC、主硬盘序列号这四个字段拼接后做一次SHA256散列取前24位作为设备的默认指纹。这个方案的稳定性和散列冲突概率在实际运行中表现都还不错。处理仓库里的机器时发现同样配置的整批办公电脑CPU和主板字段可能完全一样但硬盘序列号和网卡MAC不同所以必须保证这两个字段能可靠读到。如果读不到降级策略是拼接系统安装ID和内存容量值虽然区分度差点但至少不会因为字段为空直接生成全零指纹。5.2 采集结果落盘与无界面调用把工具做成命令行无界面模式配合资产管理系统做批量采集会非常方便。主函数里加一个命令行参数解析不传参时默认输出到控制台传“-o result.json”时输出Json文件传“-s”时静默执行并返回退出码。这样在域环境里可以通过计划任务批量分发把生成的json文件集中收集到共享目录作为IT资产的底层数据来源。需要额外注意Json格式里的转义问题。WMI返回的字符串里偶尔包含双引号、反斜杠输出时要做好转义。为了省事我直接用第三方Json库而不是手写拼接字符串不然批量采集数据里出现非法Json后期清洗数据的成本会高得多。5.3 扩展温度、风扇等传感信息如果想采集CPU温度、风扇转速常规WMI类基本无能为力。原因是温度数据不做进SMBIOS而是由主板上的SuperIO芯片掌握必须通过IO端口读写芯片寄存器才能拿到。OpenHardwareMonitor和LibreHardwareMonitor是这类方案的开源实现核心思路是用Ring0驱动或者直接IO访问获取硬件传感器数据。在我自己的工具里这部分没有默认开启因为涉及Ring0驱动安装会触发杀毒软件误报也增加了工具的权限要求。只有在需要做硬件健康监控的指定设备上我才会集成LibreHardwareMonitor的驱动方案普通资产登记场景下完全没有必要。6. 写在最后的一些实操体会这个工具从最初只能输出CPU和内存的小程序到现在能采集整套硬件信息前后迭代了不少版本。我最大的体会是Windows硬件信息采集并不是一个从API文档到代码的直线过程而是一个不断和真实硬件环境博弈的过程。举一个最典型的例子同样一段WMI代码在一台联想台式机上能读到完整的主板序列号在另一台同型号机器上就显示“Unknown”两台机器连固件版本都完全一致。后来才意识到不同批次的板卡在产线上写入SMBIOS时的数据完整度是有差异的代码不能假设字段一定存在。所以如果你准备自己写类似工具我建议在架构上多做一层容错把WMI查询结果统一封装成“读取成功/失败/为空”三种状态上层逻辑再统一处理而不是让每一处都散落着对字段是否为空的判断。再分享一个部署层面的小技巧因为内部工具经常要分发到不同电脑跑我会在release目录里放一个config.ini里面配置是否输出Json、是否静默模式、结果文件路径。这样一个exe就能适配“装机检测要弹窗显示”和“后台批量采集要静默运行”两种完全不同的场景省得维护两个版本。本文还有配套的精品资源点击获取
返回列表