ARTICLE DETAIL

建站实战干货

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

C#获取U盘VID/PID/序列号/盘符:USB设备信息识别指南

2026/9/7 11:07:30 拓冰建站 浏览量
C#获取U盘VID/PID/序列号/盘符:USB设备信息识别指南 简介面向需要获取U盘、移动硬盘、手机卡、MP3播放器等可移动设备底层标识信息的开发者解决在Windows下读取盘符、厂商编号、产品编号与物理序列号的需求。源码采用VC6环境编写可直接生成类似“盘符G、厂商编号0951、产品编号1623、序列号001CC0EC32CDEA10969B011D”的完整信息串便于开发调试、设备管理以及自动化识别场景使用。压缩包共31个文件约4.89MB包含C/C源程序、VC6工程配置、编译好的可执行程序以及SetupAPI与配置管理器所需的静态库、头文件、调试符号和浏览信息文件Release与Debug目录分别提供发布版与调试版程序打开工程即可编译运行。已有1742人学习下载。通过这套源码可学习Windows下利用设备管理接口、配置管理接口及相关头文件查询可移动设备硬件信息的思路也能直接运行示例程序查看本机U盘、移动硬盘等设备的识别结果适合C/C开发者对照练习底层API调用与设备信息解析并在此基础上扩展更多设备类型与输出格式。 先说明一件事很多人一搜“U盘 VID PID”结果翻出来的全是自动控制领域的PID算法文章。这不是你搜错了而是“PID”这个缩写被工业控制用得太频繁。但今天聊的是USB世界的PID——Product ID也就是产品标识符。这篇文章我会一次性把U盘的VID、PID、盘符、物理序列号这四个信息讲透并给出一份C#源码直接在Windows上编译运行就能看到结果。这个工具是在我维护一批工控机时被逼出来的。当时要限制U盘访问但又不能靠盘符做判断因为盘符每次插入都可能变也不能靠卷序列号格式化一次就换一套。折腾了一圈真正稳定的方案是用“VID PID 物理序列号”做设备指纹。这个指纹能代表一块具体的U盘能区分同一型号下不同个体也能避免“插入设备管理器里看到一个名字实际却定位不到盘符”这种尴尬。下面我会从概念、原理、源码、踩坑、扩展这五条线展开代码直接用Visual Studio或.NET环境跑起来就能看效果。1. 为什么非要拿这四样先搞清楚需求的本质代码才不会白写1.1 场景一U盘白名单管控公司机房或工业现场经常有“只允许特定U盘拷数据”的诉求。但Windows不会自动告诉你“哪一块U盘是合规的”你需要自己定义合规。最常用的合规条件就是VID匹配厂商、PID匹配型号、物理序列号匹配唯一个体。至于盘符只是当前插入后的临时门牌号不属于设备固有属性。如果你只靠盘符做判断换一个USB口、多插几块U盘盘符可能就乱了。靠卷序列号更不行格式化一次就变。所以做白名单实质上是给每一块U盘发一张“身份证”这张身份证就是VIDPID物理序列号的三元组。1.2 场景二运维台账和设备追溯维护记录里要登记“哪些U盘接触过这台机器”不能只写“金士顿U盘一个”因为同型号的U盘可能有几十个。你需要唯一标识。物理序列号是最合适的追溯依据而VID/PID则能快速判断设备型号盘符用于确认当前行为路径。四者合在一起日志里就能精确描述“哪一厂商的哪一型号、哪一序列号的U盘在哪个盘符下被读取”。1.3 场景三U盘授权软件的底层依赖市面上不少U盘加密、U盘启动盘制作、U盘修复工具底层都会先读取设备标识。很多工具表面上显示“设备已锁定”实际上就是拿序列号做比对。如果序列号获取不可靠软件就会出现“换了台电脑识别不了授权U盘”这种问题。我的经验是先把这个获取程序跑通、把结果打印出来再决定后面做什么。基础信息都不准确上层逻辑全是空中楼阁。这也是这篇文章能当项目模板用的原因——它不是只解决某一个软件的需求而是解决了Windows下U盘信息获取的通用底座。2. 概念拆解VID、PID、盘符、物理序列号到底是什么2.1 VID/PIDUSB设备的厂商ID和产品IDVID是Vendor ID由USB-IFUSB Implementers Forum统一分配给厂商类似身份证里的地区码。PID是Product ID由厂商自己定义用于区分该厂商的不同产品型号。插上U盘后Windows通过USB设备描述符里的idVendor和idProduct字段识别设备。这两个值不是从U盘磁盘扇区里读出来的而是U盘主控芯片在USB枚举阶段上报给操作系统的。你可以在设备管理器里把U盘设备打开切到“详细信息”标签选“硬件ID”会看到类似USB\VID_0781PID_5583这样的字符串其中0781是SanDisk的VID5583是该型号的PID。注意U盘量产后VID/PID是可以被修改的。正规厂商不会随便改但山寨盘或修复工具可能改成一个“通用值”。这就意味着VID/PID只能做“粗粒度筛选”不能单独作为唯一凭证。2.2 盘符Windows挂载卷的“门牌号”盘符像给一栋楼分配的门牌号是Windows文件系统层的概念。物理U盘接入后主控被识别为存储设备系统再给它创建分区和卷然后分配盘符。盘符记录在注册表的MountPoints或DosDevices之下。盘符最大的问题是会变化。同一个U盘在这台电脑上是E:插到另一台电脑可能是F:即便在同一台电脑上如果你先插了别的U盘它也可能被挤到后面的字母。所以盘符是动态坐标只适合“当前会话里定位卷”不适合做长期标识。2.3 物理序列号U盘的硬件“身份证”物理序列号是U盘主控固件里存的那串字符理论上每个U盘出厂时都不一样。它在USB协议层面属于字符串描述符String Descriptor里的iSerialNumber字段在SCSI存储设备层面也可能通过INQUIRY命令返回。Windows里Win32_DiskDrive的SerialNumber字段以及在设备管理器“详细信息”里的“串行编号”属性显示的就是物理序列号。它不会因为格式化、换电脑、换盘符而改变是四样信息里最稳定的一个也是我做设备绑定时最依赖的字段。2.4 最容易混淆的卷序列号必须单独提醒Win32_LogicalDisk.VolumeSerialNumber是卷序列号不是物理序列号。它是由文件系统在格式化时生成的一个32位随机值每格式化一次就重新生成。很多人误把卷序列号当U盘ID结果做好的授权软件在一键格式化后全部失效。信息项来源层级是否唯一格式化后是否变化用途VIDUSB设备描述符厂商级不变识别厂商PIDUSB设备描述符型号级不变识别型号盘符文件系统挂载不唯一不定当前访问路径物理序列号主控固件/SCSI应答个体级不变唯一设备绑定卷序列号文件系统格式化不一定唯一会变尽量不要用于授权3. 获取路径Windows设备树和WMI关联关系3.1 一张设备树图看懂层级U盘插进电脑后Windows设备管理器里会出现一串设备节点大致是USB控制器 → USB Hub → USB设备节点USB\VID_xxxxPID_yyyy\序列号 → USB存储设备节点USBSTOR\DiskVen_xxxProd_xxxRev_xxx\序列号0 → 磁盘驱动器Win32_DiskDrive如\\.\PHYSICALDRIVE1 → 磁盘分区Win32_DiskPartition → 逻辑磁盘卷Win32_LogicalDisk对应E:这样的盘符。注意很多人直接拿Win32_DiskDrive的PNPDeviceID去解析VID/PID结果发现得到的是USBSTOR\DiskVen_SanDiskProd_Cruzer_BladeRev_1.00\...里面根本没有VID_和PID_。这是因为磁盘驱动器节点已经处于存储栈里USB设备节点才是带VID/PID的那层。3.2 为什么选择WMI而不是C SetupAPI获取这类信息有两条路底层用SetupAPI枚举设备信息集或者用WMI查Win32_*类。SetupAPI更接近内核能拿到很多底层信息但代码量大句柄管理复杂对新手不友好。WMI封装好了设备管理公共信息代码简洁只是性能上有一定开销但对这种“读取几个设备信息”的场景毫无压力。我用WMI有三个理由第一Win32_DiskDrive能直接筛出InterfaceType USB的设备省去遍历USB节点的麻烦第二GetRelated()方法能沿着设备树关联函数自动找到分区和逻辑盘第三后续做插拔监听、批量采集WMI事件模型也能复用。3.3 从磁盘到盘符的关联方式获取盘符不能直接查Win32_LogicalDisk然后拍脑袋对应因为系统里有可能是U盘、移动硬盘、本地硬盘混插。标准做法是三层跳跃第一步从Win32_DiskDrive取到Index属性比如1表示PHYSICALDRIVE1。第二步查Win32_DiskPartition中DiskIndex等于该值的所有分区。第三步在关联类Win32_LogicalDiskToPartition里找到该分区对应的Dependent属性也就是逻辑磁盘的DeviceID盘符就在里面。我代码里用的是WMI的GetRelated关联方法直接传入关联类名就行不用手写复杂的WQL字符串。3.4 物理序列号的两种兜底来源Win32_DiskDrive.SerialNumber读到的是WMI整理的序列号大多数情况下是正确的。但部分U盘主控返回的序列号带空格比如574D56 5349这种需要去掉空格再使用。如果SerialNumber字段返回空还有一个兜底办法从设备实例IDPNPDeviceID最后一段提取。USBSTOR节点的实例ID通常是序列号0这种格式去掉0后剩下的字符串就是序列号。这个方法不依赖WMI对SerialNumber的解析可靠性更高。我下面的源码里就是把两者结合起来用的。4. 可运行源码C#控制台版USB信息采集器4.1 环境准备这份代码面向Windows平台.NET Framework 4.6.1以上可以直接编译。如果你用的是.NET 5/6/7/8需要先安装NuGet包System.Management然后在项目文件里加上UseWindowsFormstrue/UseWindowsForms不一定需要但必须确认目标平台是Windows。开发环境推荐Visual Studio 2022创建一个控制台应用右键项目选择“添加引用”勾选System.Management。如果创建的是.NET Core风格的项目直接在NuGet里搜索System.Management安装。4.2 完整源码下面是我测试通过的完整代码复制到Program.cs即可运行。using System; using System.Collections.Generic; using System.Management; namespace UsbInfoTool { public class UsbDriveInfo { public string VID { get; set; } ; public string PID { get; set; } ; public string SerialNumber { get; set; } ; public string DriveLetters { get; set; } ; public string DeviceInstanceId { get; set; } ; } public static class UsbInfo { public static ListUsbDriveInfo GetUsbStorageDevices() { var result new ListUsbDriveInfo(); using (var searcher new ManagementObjectSearcher( SELECT * FROM Win32_DiskDrive WHERE InterfaceTypeUSB)) { foreach (ManagementObject disk in searcher.Get()) { string pnpDeviceId disk[PNPDeviceID]?.ToString() ?? ; string serial NormalizeSerial(disk[SerialNumber]?.ToString() ?? ); uint diskIndex Convert.ToUInt32(disk[Index]); var info new UsbDriveInfo { SerialNumber serial, DeviceInstanceId pnpDeviceId, DriveLetters string.Join(,, GetDriveLettersFromIndex(diskIndex)) }; var (vid, pid) GetVidPid(pnpDeviceId, serial); info.VID vid; info.PID pid; result.Add(info); } } return result; } private static string NormalizeSerial(string serial) { return string.IsNullOrEmpty(serial) ? : serial.Replace( , ); } private static Liststring GetDriveLettersFromIndex(uint diskIndex) { var letters new Liststring(); using (var partSearcher new ManagementObjectSearcher( SELECT * FROM Win32_DiskPartition WHERE DiskIndex diskIndex)) { foreach (ManagementObject partition in partSearcher.Get()) { using (ManagementObjectCollection logicalDisks partition.GetRelated( Win32_LogicalDisk, Win32_LogicalDiskToPartition, null, null, null, null, false, null)) { foreach (ManagementObject logicalDisk in logicalDisks) { string letter logicalDisk[DeviceID]?.ToString() ?? ; if (!string.IsNullOrEmpty(letter) !letters.Contains(letter)) { letters.Add(letter); } } } } } return letters; } private static (string vid, string pid) GetVidPid(string diskPnpDeviceId, string diskSerial) { string pnpSerial GetLastSegment(diskPnpDeviceId); using (var searcher new ManagementObjectSearcher( SELECT PNPDeviceID FROM Win32_PnPEntity WHERE PNPDeviceID LIKE USB\\\\VID_%)) { foreach (ManagementObject entity in searcher.Get()) { string usbPnpId entity[PNPDeviceID]?.ToString() ?? ; string usbSerial GetLastSegment(usbPnpId); if (string.IsNullOrEmpty(usbSerial)) { continue; } bool matched false; if (!string.IsNullOrEmpty(diskSerial) (usbSerial.IndexOf(diskSerial, StringComparison.OrdinalIgnoreCase) 0 || diskSerial.IndexOf(usbSerial, StringComparison.OrdinalIgnoreCase) 0)) { matched true; } else if (!string.IsNullOrEmpty(pnpSerial) (usbSerial.IndexOf(pnpSerial, StringComparison.OrdinalIgnoreCase) 0 || pnpSerial.IndexOf(usbSerial, StringComparison.OrdinalIgnoreCase) 0)) { matched true; } if (!matched) { continue; } string[] parts usbPnpId.Split(\\); if (parts.Length 2) { string[] ids parts[1].Split(); string vid , pid ; foreach (string id in ids) { if (id.StartsWith(VID_, StringComparison.OrdinalIgnoreCase)) vid id.Substring(4); else if (id.StartsWith(PID_, StringComparison.OrdinalIgnoreCase)) pid id.Substring(4); } if (!string.IsNullOrEmpty(vid) !string.IsNullOrEmpty(pid)) { return (vid, pid); } } } } return (, ); } private static string GetLastSegment(string pnpId) { if (string.IsNullOrEmpty(pnpId)) return ; string last pnpId.Substring(pnpId.LastIndexOf(\\) 1); int ampIndex last.IndexOf(); if (ampIndex 0) { last last.Substring(0, ampIndex); } return last; } } class Program { static void Main(string[] args) { Console.WriteLine(正在扫描USB存储设备...); Console.WriteLine(); var drives UsbInfo.GetUsbStorageDevices(); if (drives.Count 0) { Console.WriteLine(未检测到USB存储设备请插入U盘后重试。); } else { foreach (var d in drives) { Console.WriteLine(); Console.WriteLine(VID: d.VID); Console.WriteLine(PID: d.PID); Console.WriteLine(物理序列号: d.SerialNumber); Console.WriteLine(盘符: d.DriveLetters); Console.WriteLine(设备实例ID: d.DeviceInstanceId); Console.WriteLine(); } } Console.WriteLine(按任意键退出...); Console.ReadKey(); } } }4.3 关键函数逻辑说明GetUsbStorageDevices是入口用WQL过滤InterfaceTypeUSB保证只会拿到USB接口的磁盘不会带上SATA、NVMe硬盘。这一步是整段代码的基石如果不加条件你会在结果里看到所有本地硬盘区分起来很痛苦。GetDriveLettersFromIndex负责从磁盘索引找盘符。它通过Win32_DiskPartition的DiskIndex属性找到分区再调用GetRelated关联到Win32_LogicalDisk。这里不建议用Win32_LogicalDisk.DeviceID单独查询因为本地硬盘、移动硬盘、虚拟磁盘都混在一起不经过磁盘链路无法准确定位。GetVidPid做的是“从USB节点反向匹配”。因为Win32_DiskDrive的PNPDeviceID是USBSTOR\DiskVen_...Prod_...Rev_...\...格式不含VID/PID所以我先生成两个候选序列号diskSerialWMI的SerialNumber字段和pnpSerialPNPDeviceID最后一段然后遍历所有PNPDeviceID以USB\VID_开头的USB节点用序列号模糊匹配。匹配成功的USB节点中VID_和PID_后面的四/五位十六进制数就是要找的值。有人会问为什么不用设备管理器里显示的“硬件ID”直接匹配因为Win32_PnPEntity只暴露PNPDeviceID没有现成的硬件ID数组用序列号匹配是纯WMI方案里最稳的方式。4.4 运行输出示例插入一块SanDisk U盘后程序输出大致如下正在扫描USB存储设备... VID: 0781 PID: 5583 物理序列号: 4C530001020830119272 盘符: E: 设备实例ID: USBSTOR\DiskVen_SanDiskProd_Cruzer_BladeRev_1.00\4C5300010208301192720如果同时插入两块U盘会依次输出两段。需要确认哪块对应哪个盘符看盘符字段就行。5. 实测中的坑这些问题不提前知道会被搞蒙5.1 一块U盘出现两个盘符部分U盘出厂时划分了两个分区比如一个公共分区和一个加密分区。这种情况下GetDriveLettersFromIndex会把两个盘符都收集回来输出类似E:, F:。物理序列号完全相同VID/PID也相同只有盘符不同。遇到这种设备你在做白名单时要保留“一块U盘多个盘符”的数据结构不要简单按盘符逐条判断。否则实际插入时只匹配到E:而程序拿到的是F:会误判为未授权。5.2 读卡器TF卡序列号可能不是TF卡的读卡器本身是一个USB设备TF卡插进去后系统识别到的“物理序列号”有相当概率来自读卡器的主控而不是TF卡芯片。这意味着同一张TF卡换一个读卡器序列号可能就变了。如果你在做U盘授权尽量避免用“读卡器TF卡”方案做唯一凭证。如果非要用至少要把读卡器的VID/PID也纳入绑定逻辑。更稳妥的做法是识别出设备属于读卡器后提示用户换直插式U盘。5.3 WMI序列号里的空格和乱码正规U盘的SerialNumber一般是一串连续字符但有些厂商会在内部用空格填充固定长度比如“WD 1234 ABC 9876”去掉空格后才是真实序列号。还有极少数主控会返回乱码或者全空白这时候GetVidPid里的pnpSerial兜底方案就派上用场了。我建议把NormalizeSerial这一行强制保留不管读出来什么样先去掉所有空格。如果SerialNumber为空程序会自动走PNPDeviceID最后一段匹配实测很多老U盘靠这个兜底能拿到正确的VID/PID。5.4 U盘插入后没有盘符有些U盘没有分区或者分区表损坏Windows不会分配盘符但Win32_DiskDrive里仍然能看到这个设备。这种情况下盘符字段会打印为空字符串但VID/PID/物理序列号都能正常获取。反过来这也是个功能你可以用这个工具先检测U盘是否存在再决定是否提示用户去初始化分区。配合热词里的“U盘修复工具”“U盘检测真实容量”场景这个能力很有用。6. 从工具到产品扩展成一个U盘白名单系统6.1 白名单判定逻辑拿到四样信息后最直接的扩展是做一个白名单JSON配置比如[ { VID: 0781, PID: 5583, SerialNumber: 4C530001020830119272, Description: 采购部数据导入盘 } ]判定逻辑按顺序来先匹配VID再匹配PID最后匹配物理序列号。前两项用来快速排除明显不相关的设备序列号用来锁定唯一个体。盘符不进白名单只作为当前会话的展示信息。6.2 插拔事件实时监听与其定时轮询不如用WMI事件监听U盘插拔。核心代码只有几行订阅__InstanceCreationEvent和__InstanceDeletionEvent过滤TargetInstance ISA Win32_DiskDrive然后调用上面封装好的GetUsbStorageDevices刷新列表。我习惯用ManagementEventWatcher做一个后台线程插拔后触发回调。回调里不要做耗时操作直接把新状态发到界面线程更新白名单判断结果。6.3 管控升级方向白名单只是第一步。再往下走可以在检测到非白名单U盘时执行策略通过Windows组策略里的“禁止安装可移动设备”或者用注册表HKLM\SYSTEM\CurrentControlSet\Services\USBSTOR的Start值禁用整个USB存储驱动。但要注意禁用USBSTOR会连键盘鼠标等USB设备一起屏蔽实际项目里要更精细地通过设备安装限制来拦截特定硬件ID。还有一个可做的方向是把设备信息联动到日志系统。每次插拔都记录一条结构日志字段就是文章标题里的四样东西——VID、PID、盘符、物理序列号真正实现U盘行为可审计。我在实际使用中的一个经验是别把程序写在一次性脚本里一定要把这套信息获取封装成一个独立的工具类。后续无论是做命令行工具、Windows服务还是给上位机软件加U盘授权都能直接复用。那几行关联查询和序列号匹配逻辑是我调试了接近一下午才稳定下来的遇到读卡器、量产盘、无盘符设备这些边缘情况时代码里的兜底逻辑能省下大量排查时间。本文还有配套的精品资源点击获取