需求場景
應用程式需識別特定 USB 隨身碟(而非邏輯磁碟機代號),獲取其**硬體唯一序列號**以實現授權綁定、資產追蹤、防拷貝等功能。
核心思路:兩階段交叉驗證
- 註冊表階段:從
HKLM\SYSTEM\MountedDevices 解析掛載裝置對應的 **ParentIdPrefix** (如 7&100A16F&0)。
- SetupAPI 階段:枚舉
GUID_DEVINTERFACE_DISK 介面裝置,查詢各裝置註冊表 ParentIdPrefix,匹配成功者即為目標隨身碟,提取其 **DevicePath** 中的序列號段。
關鍵註冊表路徑與結構
// 掛載點映射
HKLM\SYSTEM\MountedDevices
\DosDevices\L: → REG_BINARY: \??\STORAGE#RemovableMedia#7&100a16f&0&R#{53f5630d-b6bf-11d0-94f2-00a0c91efb8b}
// 裝置實例鍵 (SetupAPI 遍歷得到)
HKLM\SYSTEM\CurrentControlSet\Enum\USBSTOR\Disk&Ven_Generic&Prod_Flash_Disk\7&100a16f&0
ParentIdPrefix = "7&100A16F&0" (REG_SZ)
完整實現步驟 (C++ / Windows API)
1. 判斷磁碟機為可移動裝置
char szDrv[4] = "L:\\";
if (GetDriveType(szDrv) != DRIVE_REMOVABLE) return FALSE;
2. 讀取 MountedDevices 獲取 ParentIdPrefix
HKEY hKey;
RegOpenKeyEx(HKEY_LOCAL_MACHINE, "SYSTEM\\MountedDevices", 0, KEY_READ, &hKey);
DWORD size = 0;
RegQueryValueEx(hKey, "\\DosDevices\\L:", NULL, NULL, NULL, &size);
BYTE *binData = new BYTE[size];
RegQueryValueEx(hKey, "\\DosDevices\\L:", NULL, NULL, binData, &size);
// 過濾 \0 字元,提取 "7&100a16f&0"
char *usbKey = ExtractParentIdPrefix(binData, size); // 大寫正規化
RegCloseKey(hKey);
3. SetupAPI 枚舉磁碟介面裝置
GUID guidDisk = GUID_DEVINTERFACE_DISK; // {53f56307-b6bf-11d0-94f2-00a0c91efb8b}
HDEVINFO hDevInfo = SetupDiGetClassDevs(&guidDisk, NULL, NULL, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
SP_DEVICE_INTERFACE_DATA did = { sizeof(SP_DEVICE_INTERFACE_DATA) };
for (DWORD i = 0; SetupDiEnumDeviceInterfaces(hDevInfo, NULL, &guidDisk, i, &did); ++i) {
// 獲取介面詳細資訊 (含 DevicePath)
DWORD reqSize = 0;
SetupDiGetDeviceInterfaceDetail(hDevInfo, &did, NULL, 0, &reqSize, NULL);
PSP_DEVICE_INTERFACE_DETAIL_DATA pDetail = (PSP_DEVICE_INTERFACE_DETAIL_DATA)GlobalAlloc(GPTR, reqSize);
pDetail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA);
SP_DEVINFO_DATA devData = { sizeof(SP_DEVINFO_DATA) };
SetupDiGetDeviceInterfaceDetail(hDevInfo, &did, pDetail, reqSize, NULL, &devData);
// 從 DevicePath 解析裝置實例路徑,打開對應註冊表鍵
// DevicePath 格式: \\?\usbstor#disk&ven_generic&prod_flash_disk#7&100a16f&0#{...}
char *regPath = BuildEnumRegPath(pDetail->DevicePath); // SYSTEM\CurrentControlSet\Enum\...
HKEY hDevKey;
if (RegOpenKeyEx(HKEY_LOCAL_MACHINE, regPath, 0, KEY_READ, &hDevKey) == ERROR_SUCCESS) {
char pidPrefix[64]; DWORD pidSize = sizeof(pidPrefix);
if (RegQueryValueEx(hDevKey, "ParentIdPrefix", NULL, NULL, (LPBYTE)pidPrefix, &pidSize) == ERROR_SUCCESS) {
_strupr(pidPrefix);
if (strcmp(pidPrefix, usbKey) == 0) { // 匹配成功!
// 從 DevicePath 提取序列號段
char *serial = ExtractSerialFromDevicePath(pDetail->DevicePath);
// serial 即為 "7&100A16F&0" 或更詳細硬體序列號
GlobalFree(pDetail);
RegCloseKey(hDevKey);
return serial;
}
}
RegCloseKey(hDevKey);
}
GlobalFree(pDetail);
}
SetupDiDestroyDeviceInfoList(hDevInfo);
關鍵輔助函數實現要點
| 函數 | 功能 | 關鍵邏輯 |
ExtractParentIdPrefix | 從二進位註冊表值提取 ID | 移除 \0、定位 #RemovableMedia# 後、RM 前的子串、大寫 |
BuildEnumRegPath | DevicePath → 註冊表枚舉鍵路徑 | 移除前綴 \\?\、截取 # 前、將 # 替換為 \\ |
ExtractSerialFromDevicePath | 提取最終序列號 | 定位倒數第二個 # 與 { 之間、將 & 替換為終止符 |
完整代碼結構 (C++Builder / MSVC 通用)
// 編譯需連結 setupapi.lib
// #pragma comment(lib, "setupapi.lib")
bool GetUSBDiskSerial(char driveLetter, char *outSerial, int maxLen) {
// 1. 驗證可移動裝置
// 2. 註冊表獲取 usbKey
// 3. SetupAPI 枚舉匹配
// 4. 提取序列號寫入 outSerial
// 5. 釋放資源、關閉句柄
return true/false;
}
常見坑與避坑指南
| 問題 | 原因 | 解決 |
| 註冊表值含大量 \0 | REG_BINARY 以 \0\0 結尾,中間也有 \0 | 逐字元過濾 \0 重建字串 |
| ParentIdPrefix 大小寫不一致 | 註冊表存儲可能大小寫混合 | 統一 _strupr 正規化後比較 |
| SetupDiGetDeviceInterfaceDetail 兩次調用 | 第一次獲取所需緩衝區大小 | 標準模式:NULL 緩衝區獲取 size → 分配 → 二次調用 |
| 64 位系統註冊表重定向 | 32 位程式訪問 HKLM\...\CurrentControlSet 被重定向 | 編譯 64 位或用 KEY_WOW64_64KEY |
| 非隨身碟也被枚舉 (內建硬碟) | GUID_DEVINTERFACE_DISK 包含所有磁碟 | 匹配 ParentIdPrefix 自然過濾,或額外檢查 BusType == BusTypeUsb |
現代替代方案
- WM_DEVICECHANGE + DBT_DEVTYP_DEVICEINTERFACE:即時監聽裝置到達/移除,配合
SetupDiGetDeviceInterfaceDetail 獲取序列號。
- WMI (Win32_DiskDrive / Win32_USBControllerDevice):查詢
SerialNumber、PNPDeviceID,適合腳本/管理工具,C++ 需 COM 互操作較重。
- IOCTL_STORAGE_QUERY_PROPERTY (PropertyId = StorageDeviceProperty):直接對裝置句柄
DeviceIoControl 獲取 STORAGE_DEVICE_DESCRIPTOR 中的 SerialNumberOffset,最直接、無需註冊表解析,但需管理員權限開啟裝置句柄。
總結
- 核心邏輯:**MountedDevices 註冊表 → ParentIdPrefix → SetupAPI 枚舉磁碟介面 → 註冊表匹配 ParentIdPrefix → DevicePath 提取序列號**。
- 關鍵 API:
RegOpenKeyEx/QueryValueEx、SetupDiGetClassDevs/EnumDeviceInterfaces/GetDeviceInterfaceDetail、GlobalAlloc/Free。
- 注意:註冊表二進位值清洗、大小寫正規化、64 位註冊表重定向、緩衝區兩次調用模式。
- 生產代碼建議封裝為
CUsbDiskIdentifier 類,提供 GetSerialByDriveLetter/GetSerialByDevicePath 介面,統一錯誤處理與資源釋放 (RAII)。