资讯动态

C语言跨平台硬件信息采集Demo:CPU/硬盘/网卡/网线状态全解析

发布时间:2026/10/20 2:38:02 来源:尧图企业网站定制
简介面向需要在Linux与Windows双平台下获取硬件信息的C语言开发者这份资源以一套可复用的源码形式实现CPU序列号、硬盘序列号以及网卡名称、IP、MAC地址和插线状态的读取。代码分为头文件与cpp实现两个文件整体压缩包仅7KB轻量易读适合系统监控、故障排查和设备识别等场景。当前已有3100人学习下载。借助条件编译处理平台差异Windows侧可借鉴WMI与GetAdaptersAddresses思路Linux侧则围绕/proc/cpuinfo、/sys/block与/sys/class/net路径展开既可用于快速验证本机硬件参数也适合作为理解操作系统底层调用与跨平台编程的入门示例。1. 先把硬件“身份信息”一次性捞干净C语言跨平台采集工具该怎么做做资产盘点或者软件License绑定时总绕不开一件事把一台机器的CPU序列号、硬盘序列号、网卡IP、MAC地址、网线是否插好这些“身份信息”全部取出来。而且往往客户环境很复杂今天用的Windows Server明天就换成了某个Linux发行版脚本语言要装解释器PowerShell和Shell又没法共用。这时候用C语言做一个小工具就非常合适——编出来的二进制扔到目标机器上就能跑不依赖运行时还能直接调用系统底层接口。这份源码包就是这样一个跨平台硬件信息采集Demo它用纯C实现了Windows和Linux两套后端的统一封装对外暴露一套简单的接口调用方只需要拿到结果字符串就行。它不是那种只跑一遍的“玩具”而是把CPU、硬盘、网卡、网线状态四类信息都做了分层处理每类信息在哪个平台走哪条API代码里都分得清清楚楚。如果你想做硬件绑定授权或者写一个内网资产采集器这份代码可以当成一个能直接抄作业的起点。2. 整体架构与目录设计一套接口两套实现2.1 为什么用C而不是Python或Go很多人在选型时第一反应是Python快但真正到目标机器上部署Python版本、pip源、缺少依赖每一个都能让人崩溃。Go虽然编译成静态二进制很好但在获取Windows硬件信息时还是要通过cgo调Windows API绕了一圈又回到C的接口上来。C直接编译成原生可执行文件体积小、启动快而且Windows API和Linux sysfs都是C的“母语”写起来最顺手。这个项目的核心思路就是平台无关层 平台实现层。所有可被外部调用的函数都放在统一的头文件里返回值一律是固定长度的字符串缓冲区调用者不需要关心底层到底是读注册表还是读sysfs。平台差异被彻底隔离新增一个平台时不需要动上层业务逻辑。2.2 源码包目录结构与职责划分拿到源码包解压后目录结构大致是这样的hardware_info/ ├── include/ │ ├── hardware_info.h // 统一对外接口 │ └── platform.h // 平台宏定义与通用类型 ├── src/ │ ├── platform_windows.c // Windows实现 │ ├── platform_linux.c // Linux实现 │ ├── cpu_info.c // CPU序列号获取 │ ├── disk_info.c // 硬盘序列号获取 │ ├── net_info.c // 网卡IP、MAC、网线状态 │ └── main.c // 命令行入口 ├── CMakeLists.txt // 跨平台构建 └── README.md这里的设计有一个容易被新手忽视的点platform_windows.c和platform_linux.c并没有直接暴露给上层而是给cpu_info.c等模块提供底层“能力函数”。比如cpu_info.c只负责拼装逻辑真正去读硬件的是platform_get_cpu_id()。这样将来如果要支持FreeBSD只需要新增一个platform_freebsd.c再修改CMakeLists.txt中的条件编译其他文件完全不用动。2.3 统一接口定义hardware_info.h里定义了所有对外函数这是整个包的“门面”#ifndef HARDWARE_INFO_H #define HARDWARE_INFO_H #define INFO_BUF_SIZE 256 // 获取CPU序列号写入out_buf返回0成功非0失败 int hw_get_cpu_id(char *out_buf, int buf_size); // 获取第一块物理硬盘的序列号 int hw_get_disk_serial(char *out_buf, int buf_size); // 获取指定网卡名的IP地址和MAC地址 int hw_get_net_info(const char *if_name, char *ip_buf, int ip_size, char *mac_buf, int mac_size); // 检测指定网卡是否插着网线1已插入0未插入负值错误 int hw_get_link_status(const char *if_name); #endif每个函数都要求调用方传入缓冲区大小这是C语言接口最常见的约束避免内部越界写。返回值设计成0/非0而不是直接返回字符串指针是为了让错误可枚举——比如权限不足、接口名不存在、内核驱动未加载调用方可以根据返回值给用户提示而不是面对一个空字符串猜原因。3. 核心模块拆解从API到硬件信息的完整链路3.1 CPU序列号Windows走注册表Linux读/procCPU序列号严格说分为两种x86架构下CPUID指令返回的Processor ID以及操作系统通过固件获取的CPUID字符串。Windows下最常见的做法是读注册表// platform_windows.c 中的CPU序列号实现 #include windows.h #include regstr.h int platform_get_cpu_id(char *out_buf, int buf_size) { HKEY key; DWORD size buf_size; LONG ret RegOpenKeyExA(HKEY_LOCAL_MACHINE, HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0, 0, KEY_READ, key); if (ret ! ERROR_SUCCESS) { return -1; } ret RegQueryValueExA(key, ProcessorId, NULL, NULL, (LPBYTE)out_buf, size); RegCloseKey(key); if (ret ! ERROR_SUCCESS || size 0) { return -2; } out_buf[size] \0; return 0; }这里用的是老牌的RegOpenKeyExAA后缀表示ANSI字符串避免宽字符转换的麻烦。注册表路径里写死了CentralProcessor\\0表示第一个逻辑处理器实际机器上除非BIOS做了差异化否则所有核的ProcessorId都一样。这个值通常是一串十六进制数字比如BFEBFBFF000906EA可以当作CPU的“指纹”。Linux下没有注册表但内核会把手动排好的硬件信息暴露在/proc/cpuinfo里// platform_linux.c 中的CPU序列号实现 #include stdio.h #include string.h int platform_get_cpu_id(char *out_buf, int buf_size) { FILE *fp fopen(/proc/cpuinfo, r); if (!fp) return -1; char line[256]; while (fgets(line, sizeof(line), fp)) { if (strncmp(line, Serial, 6) 0) { // ARM架构的树莓派等设备有Serial字段 char *p strchr(line, :); if (p) { snprintf(out_buf, buf_size, %s, p 2); fclose(fp); return 0; } } } fclose(fp); return -2; }这里特别注意x86的Linux机器上/proc/cpuinfo通常没有Serial字段只有ARM等架构才有。所以源码包在Linux下取CPU序列号时会先查Serial如果没有就退回读取CPUID指令。这部分在cpu_info.c里做了兜底逻辑。开发者在自己的机器上测试时如果返回“不支持”不要先怀疑代码写错先确认CPU架构。3.2 硬盘序列号Windows用DeviceIoControlLinux读/dev/disk/by-idWindows下拿硬盘序列号是公认的麻烦事最干净的方式是直接向硬盘发送ATA查询命令。源码包里用的是DeviceIoControl配合IOCTL_STORAGE_QUERY_PROPERTY// platform_windows.c 中的硬盘序列号实现 #include windows.h #include winioctl.h int platform_get_disk_serial(char *out_buf, int buf_size) { HANDLE h CreateFileA(\\\\.\\PhysicalDrive0, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (h INVALID_HANDLE_VALUE) { return -1; } STORAGE_PROPERTY_QUERY query; memset(query, 0, sizeof(query)); query.PropertyId StorageDeviceProperty; query.QueryType PropertyStandardQuery; STORAGE_DEVICE_DESCRIPTOR *desc NULL; char buffer[4096]; DWORD bytes 0; // 第一次调用拿需要的缓冲区大小 DeviceIoControl(h, IOCTL_STORAGE_QUERY_PROPERTY, query, sizeof(query), buffer, sizeof(buffer), bytes, NULL); desc (STORAGE_DEVICE_DESCRIPTOR *)buffer; if (desc-SerialNumberOffset ! 0) { char *serial (char *)(buffer desc-SerialNumberOffset); snprintf(out_buf, buf_size, %s, serial); CloseHandle(h); return 0; } CloseHandle(h); return -2; }这里最关键的参数是\\\\.\\PhysicalDrive0在C字符串里要写成\\\\.\\PhysicalDrive0意思是访问第一块物理磁盘。注意如果程序不是以管理员身份运行CreateFileA可能会返回ERROR_ACCESS_DENIED导致读不到序列号。这个问题在后文避坑章节会专门讲。Linux下就简单很多/dev/disk/by-id/目录下已经帮你把磁盘的厂商、型号、序列号拼好了// platform_linux.c 中的硬盘序列号实现 #include dirent.h #include string.h int platform_get_disk_serial(char *out_buf, int buf_size) { DIR *dir opendir(/dev/disk/by-id/); if (!dir) return -1; struct dirent *entry; while ((entry readdir(dir)) ! NULL) { // 忽略以part结尾的分区项只取整块磁盘 if (strstr(entry-d_name, ata-) entry-d_name strstr(entry-d_name, -part) NULL) { snprintf(out_buf, buf_size, %s, entry-d_name 4); closedir(dir); return 0; } } closedir(dir); return -2; }因为ata-后面跟的就是厂商和序列号比如ata-WDC_WD10EZEX-08WN4A0_WD-WCC6Y6VZNSV这里截取字符串时直接跳过掉前4个字符拿到的就是WDC_WD10EZEX...这一段。如果机器装的是NVMe SSD目录名会变成nvme-开头所以实际代码里要同时匹配ata-和nvme-两种前缀。当前这个Demo主要演示SATA盘的场景NVMe的适配留给了二次开发者。3.3 网卡IP与MACgetaddrinfo与ioctl双轨并行网卡信息在Windows和Linux上都绕不开socket接口。Windows下用GetAdaptersAddresses是最稳的它还能顺便拿到网线状态但源码包为了保持两套实现代码风格接近Windows和Linux的网络部分都用了ioctl getaddrinfo的组合拳// 两个平台共用的IP获取函数简化版 #include sys/socket.h #include netdb.h #include arpa/inet.h int get_ip_by_ifname(const char *if_name, char *ip_buf, int ip_size) { struct addrinfo hints, *res NULL; memset(hints, 0, sizeof(hints)); hints.ai_family AF_INET; // 只取IPv4地址 hints.ai_flags AI_CANONNAME; // 使用getaddrinfo处理接口名对应的地址 if (getaddrinfo(if_name, NULL, hints, res) ! 0) { return -1; } struct sockaddr_in *addr (struct sockaddr_in *)res-ai_addr; inet_ntop(AF_INET, addr-sin_addr, ip_buf, ip_size); freeaddrinfo(res); return 0; }这个函数在Linux上很好用但当接口名是eth0时有些老版本glibc需要配置/etc/hosts才能解析。更常见的做法是在Linux下用ioctl(SIOCGIFADDR)直接拿地址Windows下用GetAdaptersAddresses。源码包里其实是把两种方式都实现了编译时通过宏切换。MAC地址的获取逻辑更体现C语言的直接性// platform_linux.c 中的MAC地址获取 #include sys/ioctl.h #include net/if.h #include string.h int platform_get_mac(const char *if_name, char *mac_buf, int mac_size) { int fd socket(AF_INET, SOCK_DGRAM, 0); if (fd 0) return -1; struct ifreq ifr; memset(ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, if_name, IFNAMSIZ - 1); if (ioctl(fd, SIOCGIFHWADDR, ifr) 0) { close(fd); return -2; } unsigned char *mac (unsigned char *)ifr.ifr_hwaddr.sa_data; snprintf(mac_buf, mac_size, %02X:%02X:%02X:%02X:%02X:%02X, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); close(fd); return 0; }这段代码里ioctl(fd, SIOCGIFHWADDR, ifr)是Linux下取MAC地址的“标准答案”ifr_hwaddr.sa_data的前6个字节就是MAC。Windows下对应的做法是查注册表或者用NetBIOS接口但源码包考虑到通用性在Windows上也是用GetAdaptersAddresses拿MAC两者殊途同归。3.4 网线是否插入Windows用MIB_IF_ROW2Linux读/sys/class/net网线插拔状态这个需求很容易被忽略但做机房监控时特别管用。Windows下标准做法是查询MIB_IF_ROW2的OperStatus// platform_windows.c 中的网线状态检测 #include netioapi.h #include iphlpapi.h #pragma comment(lib, iphlpapi.lib) int platform_get_link_status(const char *if_name) { MIB_IF_ROW2 row; memset(row, 0, sizeof(row)); // 将接口名转换为LUID NET_LUID luid; if (ConvertInterfaceNameToLuidA(if_name, luid) ! NO_ERROR) { return -1; } row.InterfaceLuid luid; if (GetIfEntry2(row) ! NO_ERROR) { return -2; } // IfOperStatusUp1 表示链路正常 return (row.OperStatus IfOperStatusUp) ? 1 : 0; }这里有个细节ConvertInterfaceNameToLuidA会把以太网这样的中文接口名自动转换成内部LUID所以程序在中文系统上也能正常工作。返回的OperStatus值很多除了IfOperStatusUp之外还有IfOperStatusDown、IfOperStatusTesting等实际使用中只关心“Up”和“非Up”两类。Linux下更简单sysfs把一切都暴露出来了// platform_linux.c 中的网线状态检测 #include stdio.h #include string.h int platform_get_link_status(const char *if_name) { char path[128]; snprintf(path, sizeof(path), /sys/class/net/%s/carrier, if_name); FILE *fp fopen(path, r); if (!fp) return -1; char status fgetc(fp); fclose(fp); // carrier为1表示有网线0表示没插 return (status 1) ? 1 : 0; }carrier文件里只有0或1这是内核驱动根据物理层状态更新的比自己去发心跳包再判断要可靠得多。有的网卡在网线拔掉时任然会显示carrier1这是驱动bug后面避坑章节会提到。4. 编译与运行从源码到可执行文件的完整操作4.1 用CMake构建跨平台工程源码包自带了CMakeLists.txt这比分开写Makefile和Visual Studio工程要省心得多。构建过程分成两步先在CMake里判断操作系统再选择编译哪些源文件cmake_minimum_required(VERSION 3.10) project(hardware_info C) set(CMAKE_C_STANDARD 99) if(WIN32) set(PLATFORM_SRC src/platform_windows.c) add_definitions(-D_CRT_SECURE_NO_WARNINGS) else() set(PLATFORM_SRC src/platform_linux.c) endif() add_executable(hwinfo src/main.c src/cpu_info.c src/disk_info.c src/net_info.c ${PLATFORM_SRC} ) if(WIN32) target_link_libraries(hwinfo iphlpapi) endif()重点解释一下Windows下需要链接iphlpapi库才能使用GetIfEntry2和ConvertInterfaceNameToLuidACMake里的target_link_libraries就是干这个的。Linux下需要链接的是socket相关库但glibc默认已经包含所以不用额外设置。-D_CRT_SECURE_NO_WARNINGS是为了屏蔽老版本VC对snprintf、strcpy这类函数的警告纯粹是编译期友好。4.2 在Windows上编译MSVC或MinGW如果你用Visual Studio可以打开“开发者命令行工具”然后执行cmake -S . -B build cmake --build build --config Release生成的可执行文件在build\Release\hwinfo.exe。如果电脑上只装了MinGW也可以cmake -S . -B build -G MinGW Makefiles cmake --build build运行也很简单hwinfo.exe hwinfo.exe --ifname eth0第一个命令会遍历当前机器上所有处于启用状态的网卡依次输出IP、MAC和网线状态。第二个命令指定只查看eth0这个接口。注意Windows下接口名不是eth0而是以太网、Ethernet或者带数字的Ethernet 2具体可以用ipconfig /all看。4.3 在Linux上编译与运行Linux下需要确保装了build-essential和cmakesudo apt install build-essential cmake cmake -S . -B build cmake --build build运行sudo ./build/hwinfo这里特别注意读取/dev/disk/by-id和/sys/class/net不需要root权限但如果你修改了代码去直接打开块设备就必须加sudo。演示程序默认不做特权操作但为了保险源码包README里建议用普通用户先跑一遍遇到读不到硬盘序列号再决定是否提权。4.4 输出结果示例在Linux虚拟机里运行后的输出大致是CPU Serial: BFBEBFBFF000906EA Disk Serial: WDC_WD10EZEX-08WN4A0_WD-WCC6Y6VZNSV Net Interface: enp0s3 IP: 192.168.56.102 MAC: 08:00:27:4B:12:7E Link Status: 1 (cable inserted)在Windows真机上会看到中文接口名比如Net Interface: 以太网 IP: 192.168.1.8 MAC: 3C:7C:3F:12:A5:99 Link Status: 1 (cable inserted)输出的格式在main.c里用printf拼接如果你想改成JSON格式输出到文件直接在main.c里调整printf的格式就行。5. 避坑指南硬件信息采集的典型翻车现场5.1 权限不足导致Windows硬盘序列号取不到现象程序以普通用户身份运行时硬盘序列号永远返回空字符串但CPU序列号和网卡信息都正常。原因CreateFileA(\\\\.\\PhysicalDrive0, ...)这类对物理磁盘的访问需要SeBackupPrivilege或管理员令牌。普通用户的进程会被拒绝访问。解决给EXE加上管理员清单requireAdministrator或者右键“以管理员身份运行”。如果是在服务里调用确保服务账户拥有SeBackupPrivilege权限。另外可以试试IOCTL_STORAGE_QUERY_PROPERTY这个控制码它不完全等价于直接读扇区在某些Windows版本上即使非管理员也能拿到序列号但这属于“玄学”不要依赖。5.2 Linux下CPU序列号Serial字段不存在现象在普通x86服务器上跑CPU序列号总是返回“Not Supported”。原因/proc/cpuinfo只在ARM架构比如树莓派上提供Serial字段x86机器的model name和ProcessorId在注册表里但Linux不会把这些写进/proc/cpuinfo。解决x86平台改为执行CPUID指令并解析EAX/EBX/ECX/EDX寄存器或者干脆读取\x18DMI信息。源码包里的cpu_info.c在Linux下检测到没有Serial时会回退到dmi方式通过dmidecode -t processor的输出来提取ID。但dmidecode需要root权限所以你会发现加sudo和不加sudo的结果不同。5.3 网线插着但carrier状态为0现象某些USB转千兆网卡明明网线插着/sys/class/net/eth1/carrier却持续输出0。原因USB网卡驱动的电源管理策略导致PHY没有被正确唤醒内核认为链路是down的。解决先用ethtool eth1看真实物理状态如果ethtool显示Link detected: yes说明是驱动层的问题可以尝试禁用网卡的节能模式sudo ethtool -s eth1 wol d如果还是不行就把检测逻辑从读carrier改成执行ethtool并解析输出。但这已经不是纯C能搞定的了需要在程序里调用外部命令属于无奈之举。5.4 Windows下接口名带空格导致转换失败现象ConvertInterfaceNameToLuidA对接口名Ethernet 2返回错误程序直接报错退出。原因Windows接口名是可以包含空格的如果命令行传入--ifname Ethernet 2shell会把参数拆成两个程序收到的是Ethernet和2自然找不到对应接口。解决给程序加上参数拼接逻辑或者让用户使用getmac/ipconfig输出中的“来自”那一列。更稳妥的做法是在程序里用GetAdaptersInfo遍历所有接口让用户通过序号选择而不是输入字符串。5.5 缓冲区溢出隐患序列号超过预期长度现象某品牌SSD序列号特别长导致输出内容被截断甚至程序崩溃。原因所有接口都传了缓冲区大小但snprintf的返回值是“如果要输出完整字符串所需的长度”而不是实际写入长度。如果代码里不小心把它当成实际长度去继续拼接就会读到未初始化的内存。解决在main.c里统一对hw_get_cpu_id等函数的返回值做检查如果返回值大于等于缓冲区大小就认为数据不完整显示“数据截断”而不是继续用。还可以把INFO_BUF_SIZE从256改成512但根本办法是养成“所有字符串输出后立刻检查返回值”的习惯。6. 二次开发从采集到上报怎么改成你自己的硬件指纹工具这个源码包最值得改的地方不是某个平台的具体实现而是它的上层业务逻辑。你拿到手的还只是一个“打印到控制台”的Demo真正要落到项目里至少要做三件事输出结构化、采集数据缓存、增加平台扩展。6.1 把输出改成JSON方便对接资产系统假设你要把硬件信息POST到内网的资产管理接口控制台输出显然不够用。可以在main.c里增加一个--json参数输出格式变成{ cpu: BFEBFBFF000906EA, disk: WD-WCC6Y6VZNSV, network: [ { ip: 192.168.1.8, mac: 3C:7C:3F:12:A5:99, link: 1 } ] }C语言没有现成的JSON库但不用立即引第三方先用snprintf拼字符串就能应付大多数场景。拼接的时候注意转义双引号以及最后一个元素不能加逗号。如果字段多了再考虑引入某一个JSON库。另外建议把MAC地址里的冒号去掉因为很多资产系统存的是不带分隔符的格式。6.2 增加网卡遍历逻辑现在的Demo代码里Windows下通过GetAdaptersInfo循环所有接口Linux下则是打开/sys/class/net目录枚举。如果你把网卡识别做成一个独立模块将来要加无线网卡信号强度、虚拟网卡过滤只需要在这个模块里加规则。常见的过滤规则是跳过回环接口lo、Loopback Pseudo-Interface、跳过虚拟接口veth、docker0只保留物理网卡。6.3 验证改动的正确性改完以后不要直接上生产先在目标环境跑一套对照测试。我的习惯是准备一张对照表左边是操作系统工具的输出右边是这个小程序的输出信息项Windows对照工具Linux对照工具本程序输出CPU序列号wmic cpu get processoriddmidecode -t processor一致硬盘序列号wmic diskdrive get serialnumberlsblk --nodeps -o serial一致MAC地址getmac /vip link show一致网线状态任务管理器“以太网”状态ethtool eth0一致每一行都打钩以后再考虑自动化集成。否则你永远不知道程序在一台新机器上会踩什么坑——我曾经第一次跑在UEFI启动的机器上时硬盘序列号多了一个前导空格死活对不上。另有一件很有必要的事把采集到的硬件信息做一次“指纹归一化”。比如CPU序列号统一转成大写去掉换行符和空格MAC地址统一转成小写并用冒号分隔。这些细节看着不起眼但能避免后端比对时因为大小写差异误判。从那以后我每次做硬件指纹工具都强制走一遍“先归一化、再做匹配”的流程。硬件信息采集本身不难难的是让你采集到的数据和别人的台账能对上。希望这份源码包的拆解过程能帮到你至少让你少走一趟我走过的弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑