ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C#获取U盘VID/PID/序列号/盘符:WMI+注册表双方案详解

2026/9/7 13:32:50 拓冰建站 浏览量
C#获取U盘VID/PID/序列号/盘符:WMI+注册表双方案详解 简介在Windows下通过常规API往往只能拿到逻辑序列号而本工程直接解决了读取U盘物理序列号的问题省去自行查阅DDK文档的麻烦是一套基于VC6开发的完整源码面向需要枚举USB存储设备硬件标识的C开发人员。编译运行后可直接输出类似“PFAD_G\VID_0951PID_1623\001CC0EC32CDEA10969B011D”的字符串其中PFAD对应盘符中间段为设备VID/PID末尾为存储介质物理序列号方便主程序据此识别设备唯一性。代码不仅支持U盘也适用于移动硬盘、手机卡、MP3/MP4等USB Mass Storage设备核心逻辑基于SetupAPI与配置管理器API实现源码结构清晰注释有助于理解设备实例路径与盘符之间的映射关系便于二次修改。压缩包共31个文件大小仅4.89MB包含Debug和Release两个构建版本以cpp源文件、头文件、exe可执行程序、lib库及工程配置类文件为主并带有完整VC6工作区dsw/dsp解压后即可在VC6中直接编译运行。已有1742人学习下载适合需要快速构建USB设备识别工具、获取设备实例路径或理解Windows设备枚举原理的开发者参考。1. 项目概述U盘的VID、PID、盘符、物理序列号一次拿全U盘标贴可以随便印但VID、PID、物理序列号这些信息是主控固件里写死的造假难度高得多。日常做软件授权、U盘资产盘点、识别扩容盘的时候我们需要把这几个字段完整拿到手还要顺手拿到盘符。这篇文章就是一套已经调通的源码方案Windows下可直接编译运行C#开发底层走WMI和注册表两条路线不依赖第三方收费组件也没有晦涩难懂的驱动开发门槛。适合谁看需要给U盘做软硬件绑定的开发人员做终端外设合规检查的IT运维以及想用程序识破山寨U盘、扩容盘的个人用户。无论你打算用C#写个小工具还是用Python把底层逻辑搬过去都有参考价值。我不会只丢一段代码就跑而是把里面最容易踩坑的序列号解析、盘符关联、多盘符U盘处理全部讲清楚。1.1 这四个字段分别是什么先把名词对齐。VIDVendor ID是USB-IF分配给厂商的编号金士顿、闪迪、台电这些大厂都有自己的号段。PIDProduct ID是厂商自己定义的产品型号编号同一系列型号不同PID一般也不同。这两个值合起来基本能锁定U盘的真实品牌与型号。物理序列号Serial Number则是量产时写入固件的唯一识别串通常由主控厂商的专用工具烧录。它不像盘符那样由系统动态分配也不像磁盘卷标那样可以随便改所以它是四个字段里最“硬”的一个身份标识。最后是盘符比如E:、F:它代表系统当前挂载的卷路径每次插入U盘都有可能变化所以它只用于“此时此地”的定位不能当作唯一标识。1.2 这个工具能用在哪些真实场景软件授权绑定把U盘的VIDPID序列号生成机器码配合注册码机制使用防止授权文件被随便拷贝到别的U盘上。设备资产盘点公司几百个U盘不用一个个插上去看属性跑一遍工具导出清单就行。山寨与扩容U盘初筛读取物理序列号和固件信息和正品规则列表对比一眼看出异常。终端安全管理运维或桌管系统收拢外设信息时需要准确的盘符和硬件ID来区分设备。1.3 技术选型对比为什么主用WMI辅用注册表方案获取VID/PID和序列号获取盘符依赖适用场景WMI容易解析设备实例ID即可容易但要做关联查询System.Management常规开发首选SetupAPI可靠但代码量大需要另配合挂载点查询Win32 API P/Invoke底层开发、驱动级别注册表USBSTOR能拿到厂商型号和序列号拿不到无挂载点信息Microsoft.Win32.Registry未挂载设备的查证、历史枚举我最终选择“WMI为主注册表为辅”的原因很简单WMI一条查询能同时覆盖设备层和逻辑磁盘层信息代码量少理解成本低注册表则用来处理“设备存在但没挂载盘符”或者“设备已经拔掉但想查历史记录”的边界情况。二者结合覆盖我在实际运维中遇到的99%场景。2. 核心原理VID、PID、序列号和盘符都是从哪里来的2.1 设备实例ID就是U盘的“身份证号”在Windows的设备管理器中每个USB设备都有一个“设备实例ID”。对U盘这种大容量存储设备来说典型格式是USB\VID_0781PID_5583\AA0123456789BCDEF这个字符串就是U盘在系统内部的身份证号分成三段。第一段USB说明接口类型第二段VID_0781PID_5583代表厂商和型号第三段就是物理序列号。看到这里你应该明白了拿到设备实例ID之后用正则把三段拆开VID、PID、物理序列号就全出来了。很多人写程序时会搞混一个概念以为WDK文档里的“实例ID”和注册表里的“序列号”不是一回事但实际对U盘而言二者在枚举路径中是同一个值。要注意的是制造设备时如果固件里没有序列号或者序列号为空Windows会生成一个自动替代值后面我会专门讲这种陷阱。2.2 盘符是怎么和物理U盘关联起来的盘符不是USB设备侧的属性它是Windows卷管理器动态分配的。从物理磁盘到盘符的完整链路是物理磁盘Win32_DiskDrive→ 磁盘分区Win32_DiskPartition→ 逻辑磁盘Win32_LogicalDisk每一层都有独立编号。比如\\.\PHYSICALDRIVE1对应磁盘1磁盘1上有一个分区分区挂载为可移动盘E:。程序做关联时必须通过WMI的系统关联类把三层串起来先用ASSOCIATORS OF配合Win32_DiskDriveToDiskPartition查分区再通过Win32_LogicalDiskToPartition查盘符。这里最容易踩的坑就是“磁盘号漂移”。同一个U盘第一次插入显示PHYSICALDRIVE1第二次插入显示PHYSICALDRIVE3这都正常。所以千万不能把磁盘号当身份标识要用PNPDeviceID。物理序列号和VID/PID是稳定的盘符和磁盘号都是“临时状态”。理解了这一点下面的代码就不会写歪。3. 源码实现C#获取U盘信息的两个可运行方案3.1 方案一WMI全量代码一次搞定四个字段直接上完整代码。这是一个可以独立运行的控制台程序目标框架用.NET Framework 4.6.1或.NET 6/8都可以引用System.Management后即可编译。using System; using System.Collections.Generic; using System.Management; using System.Text.RegularExpressions; namespace UsbInfoTool { internal class Program { private static void Main() { Console.WriteLine(U盘信息获取工具 v1.0); Console.WriteLine(); var list GetUsbDriveWithWmi(); if (list.Count 0) { Console.WriteLine(未检测到USB接口类型的存储设备请插入U盘后重试。); } else { for (int i 0; i list.Count; i) { var d list[i]; Console.WriteLine(); Console.WriteLine($---------- U盘 {i 1} ----------); Console.WriteLine($设备实例ID : {d.DeviceInstanceId}); Console.WriteLine($VID : {d.Vid}); Console.WriteLine($PID : {d.Pid}); Console.WriteLine($物理序列号 : {d.SerialNumber}); Console.WriteLine($盘符 : {string.Join(, , d.DriveLetters)}); Console.WriteLine($厂商 : {d.Manufacturer}); Console.WriteLine($容量 : {d.SizeMB / 1024.0:F1} GB); } } Console.WriteLine(); Console.WriteLine(按任意键退出...); Console.ReadKey(); } private static ListUsbDeviceInfo GetUsbDriveWithWmi() { var result new ListUsbDeviceInfo(); // 第1步枚举所有USB物理磁盘 using (var searcher new ManagementObjectSearcher( SELECT * FROM Win32_DiskDrive WHERE InterfaceTypeUSB)) { foreach (ManagementObject disk in searcher.Get()) { var item new UsbDeviceInfo { DeviceInstanceId (string)disk[PNPDeviceID] ?? string.Empty, Manufacturer (string)disk[Manufacturer] ?? string.Empty, SizeMB disk[Size] ! null ? ulong.Parse(disk[Size].ToString()) / 1024 : 0 }; // 第2步从设备实例ID中拆解VID、PID、物理序列号 ParsePnpId(item, item.DeviceInstanceId); // 第3步根据物理磁盘DeviceID关联查询盘符 string deviceId disk[DeviceID]?.ToString() ?? string.Empty; item.DriveLetters QueryDriveLetters(deviceId); result.Add(item); } } return result; } private static void ParsePnpId(UsbDeviceInfo item, string pnpId) { // 典型格式USB\VID_0781PID_5583\AA0123456789BCDEF var m Regex.Match(pnpId, USB\\VID_([0-9A-Fa-f]{4})PID_([0-9A-Fa-f]{4})(?:\\(.*))?); if (m.Success) { item.Vid m.Groups[1].Value.ToUpper(); item.Pid m.Groups[2].Value.ToUpper(); item.SerialNumber m.Groups[3].Value; } else { item.Vid N/A; item.Pid N/A; item.SerialNumber N/A; } } private static Liststring QueryDriveLetters(string physicalDeviceId) { var letters new Liststring(); if (string.IsNullOrEmpty(physicalDeviceId)) return letters; // WQL字符串中的反斜杠需要转义这里统一替换 string normalizedId physicalDeviceId.Replace(\\, \\\\); // 第3.1步查该物理磁盘下的所有分区 string partitionQuery $ASSOCIATORS OF {{Win32_DiskDrive.DeviceID{normalizedId}}} WHERE AssocClass Win32_DiskDriveToDiskPartition; using (var searcher new ManagementObjectSearcher(partitionQuery)) { foreach (ManagementObject partition in searcher.Get()) { string partDeviceId partition[DeviceID]?.ToString(); if (string.IsNullOrEmpty(partDeviceId)) continue; // 第3.2步再通过分区查询逻辑磁盘 string normalizedPartId partDeviceId.Replace(\\, \\\\); string logicalQuery $ASSOCIATORS OF {{Win32_DiskPartition.DeviceID{normalizedPartId}}} WHERE AssocClass Win32_LogicalDiskToPartition; using (var searcher2 new ManagementObjectSearcher(logicalQuery)) { foreach (ManagementObject logical in searcher2.Get()) { string letter logical[DeviceID]?.ToString(); if (!string.IsNullOrEmpty(letter) !letters.Contains(letter)) { letters.Add(letter); } } } } } return letters; } } internal class UsbDeviceInfo { public string DeviceInstanceId { get; set; } public string Vid { get; set; } N/A; public string Pid { get; set; } N/A; public string SerialNumber { get; set; } N/A; public Liststring DriveLetters { get; set; } new Liststring(); public string Manufacturer { get; set; } public ulong SizeMB { get; set; } } }上面的代码核心流程就三步查物理磁盘、解析PNPDeviceID、用关联查询拿盘符。需要注意InterfaceTypeUSB只筛选USB接口的磁盘像读卡器这种设备其实也会被列进来因为Windows把它识别成一个USB大容量存储设备。判断时不要只看接口类型还要结合型号和容量综合辨别。3.2 方案二注册表USBSTOR枚举查无盘符场景的兜底手段如果U盘因为分区损坏或量产异常没有正常挂载盘符WMI的关联查询会拿不到逻辑磁盘。这时候可以查注册表里的USBSTOR枚举项设备只要曾经被系统识别过这里就会留下记录。private static ListUsbDeviceInfo GetUsbFromRegistry() { var result new ListUsbDeviceInfo(); const string basePath SYSTEM\CurrentControlSet\Enum\USBSTOR; using (var root Registry.LocalMachine.OpenSubKey(basePath)) { if (root null) return result; foreach (string devClass in root.GetSubKeyNames()) { using (var classKey root.OpenSubKey(devClass)) { if (classKey null) continue; foreach (string instanceId in classKey.GetSubKeyNames()) { // 注意子项名后面可能带0系统后缀做严格匹配时要先去掉 string cleanSerial instanceId.Replace(0, ); // 键名示例 // DiskVen_KingstonProd_DataTraveler_3.0Rev_PMAP var m Regex.Match(devClass, Ven_([^])Prod_([^])); result.Add(new UsbDeviceInfo { DeviceInstanceId $USBSTOR\{devClass}\{instanceId}, Manufacturer m.Success ? m.Groups[1].Value : N/A, Vid N/A, Pid N/A, SerialNumber cleanSerial }); } } } } return result; }要提醒的是USBSTOR键名里的Ven_Kingston和Prod_DataTraveler是给人的可读名称不是十六进制格式的VID和PID。如果要把刚才WMI拿到的Vid0781和这里注册表的Ven_Kingston对应起来只能作为辅助参照不能直接拿来做严格匹配。真正要统一用十六进制VID/PID做判断还是得靠主方案里的设备实例ID。3.3 编译运行与实测输出在Visual Studio里新建一个控制台应用项目引用里勾选System.Management如果用的是.NET 6/8需要在NuGet搜索安装System.Management包。代码粘贴进去F5就能跑。我实测三个U盘输出效果如下---------- U盘 1 ---------- 设备实例ID : USB\VID_0781PID_5583\AA0123456789BCDEF VID : 0781 PID : 5583 物理序列号 : AA0123456789BCDEF 盘符 : E:, F: 厂商 : (标准磁盘驱动器) 容量 : 28.8 GB留意第二个U盘输出---------- U盘 2 ---------- 设备实例ID : USB\VID_0930PID_6545\001606949A6080D9 VID : 0930 PID : 6545 物理序列号 : 001606949A6080D9 盘符 : 厂商 : (标准磁盘驱动器) 容量 : 0.0 GB这个U盘是当时量产失败后没正常挂载盘符的设备WMI依然能列出它但关联盘符为空。容量显示0.0 GB也说明设备层已经读不到有效分区信息。这正好是主方案和注册表方案配合使用的意义设备信息完整拿到挂载状态一目了然。4. 常见问题与排查技巧4.1 物理序列号读出来为空或和外壳标签对不上这是U盘工具最容易翻车的地方。很多主控在量产时没有写入序列号Windows会生成一个系统自动替代值这个值和U盘外壳标贴上的S/N没有任何关系。更麻烦的是不同主控厂商的兼容性不一样有些方案的自动替代值在每次插入时都可能变化。如果你要做严格的机器码绑定不能只依赖一个序列号字段。建议把VID、PID、序列号、容量四个字段拼接后做一次哈希作为最终授权标识。如果序列号为空就把VIDPID容量作为降级标识同时在日志里标注“unsafe bind”这样至少不会让所有无序列号U盘瞬间集体失效。4.2 一个U盘显示两个盘符是怎么回事这个我在实际里碰到过不少大部分情况是U盘被量产成“CD-ROM移动磁盘”双分区比如系统安装盘、PE启动盘。CD-ROM部分固定挂一个盘符移动磁盘部分再挂另一个盘符。WMI查询时会把两个逻辑磁盘都关联出来所以盘符会显示成E:, F:。解决办法要看业务需求。如果只关心U盘的数据区可以用Win32_LogicalDisk里的DriveType字段判断DriveType2是可移动磁盘DriveType5是光驱按需过滤。另外部分USB读卡器插上SD卡后也会被识别为可移动磁盘但它的PNPDeviceID通常不是标准的USB\VID_xxxxPID_yyyy\序列号格式解析失败时程序要能静默跳过不要直接崩。4.3 WMI查询失败或权限不足Win32_DiskDrive这类物理设备查询需要管理员权限。用Visual Studio调试时如果不小心会得到空结果或权限异常。解决办法是给程序加应用清单设置requestedExecutionLevel levelrequireAdministrator或者在运行时用管理员身份重新启动进程。如果权限对了还是查不到依次检查三件事Windows Management Instrumentation服务是否启动杀毒软件是否拦截了WMI调用系统WMI仓库有没有损坏。检查最后一项可以用管理员命令行执行winmgmt /verifyrepository返回“WMI 存储库正常”就没事否则执行winmgmt /resetrepository重建。5. 扩展用法与我的几点实操心得5.1 用Python等其他语言复刻这套逻辑很多读者问C#的代码能不能改成Python。完全可以。Python里用wmi库或pywin32核心WQL语句和关联逻辑基本一样。重点还是那几个查询Win32_DiskDrive WHERE InterfaceTypeUSB从PNPDeviceID解析三个字段再用ASSOCIATORS OF做分区到盘符的关联。C#跑通后用Python做原型验证很快二者原理完全一致。5.2 区分U盘和读卡器的实战技巧只靠InterfaceTypeUSB过滤是会有误差的。读卡器里的SD卡被系统识别后同样会出现在结果里。我的判断规则是解析PNPDeviceID时必须命中4位十六进制的VID和PID且序列号字段非空同时容量不能是0.0 GB。做渲染或授权绑定时把存在多盘符的设备在UI中单独分组显示避免用户误以为程序重复识别了多个U盘。5.3 几个只有实操才能总结出来的经验第一解析序列号时发现末尾带0一定要去掉。系统在USBSTOR存储中会给部分实例追加0后缀这是设备枚举层的计数标记不是物理序列号的一部分。我见过很多次程序里因为这个后缀导致同一只U盘在重启前后被识别为两台设备。第二VID和PID建议统一转大写再比较。不同厂商的PNPDeviceID有时候返回小写字母尤其是一些国产主控。匹配规则写成大小写不敏感能省掉一堆诡异问题。第三批量巡检时不要用Console程序一个个插拔做一个小型服务或带界面工具更靠谱。我在给公司做资产盘点时把上面的代码封装成了一个WinForms小工具插入U盘自动输出结果并存成CSV实际用了半年稳定性和效率都远远好过命令行反复操作。最后再分享一个习惯拿到U盘信息后不要马上信任“物理序列号”先批量插入几只已知正品的U盘跑一遍程序对照设备管理器里的详细信息确认解析逻辑没有偏差再去做授权绑定或资产登记。设备信息获取这个功能看似简单但真到量产设备多样化的环境里空序列号、双分区、读卡器混入、权限拦截这些坑一个接一个提前做好准备才不会措手不及。本文还有配套的精品资源点击获取