首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

Windows 獲取 USB 隨身碟序列號實戰:SetupAPI 結合註冊表完整方案

     編輯:關於C++

需求場景

應用程式需識別特定 USB 隨身碟(而非邏輯磁碟機代號),獲取其**硬體唯一序列號**以實現授權綁定、資產追蹤、防拷貝等功能。

核心思路:兩階段交叉驗證

  1. 註冊表階段:從 HKLM\SYSTEM\MountedDevices 解析掛載裝置對應的 **ParentIdPrefix** (如 7&100A16F&0)。
  2. 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 前的子串、大寫
BuildEnumRegPathDevicePath → 註冊表枚舉鍵路徑移除前綴 \\?\、截取 # 前、將 # 替換為 \\
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;
}

常見坑與避坑指南

問題原因解決
註冊表值含大量 \0REG_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,最直接、無需註冊表解析,但需管理員權限開啟裝置句柄。

總結

  1. 核心邏輯:**MountedDevices 註冊表 → ParentIdPrefix → SetupAPI 枚舉磁碟介面 → 註冊表匹配 ParentIdPrefix → DevicePath 提取序列號**。
  2. 關鍵 API:RegOpenKeyEx/QueryValueEx、SetupDiGetClassDevs/EnumDeviceInterfaces/GetDeviceInterfaceDetail、GlobalAlloc/Free。
  3. 注意:註冊表二進位值清洗、大小寫正規化、64 位註冊表重定向、緩衝區兩次調用模式。
  4. 生產代碼建議封裝為 CUsbDiskIdentifier 類,提供 GetSerialByDriveLetter/GetSerialByDevicePath 介面,統一錯誤處理與資源釋放 (RAII)。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved