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

Windows 硬碟實體序號讀取技術:跨平台 (Win9x/NT/2000/XP) 完整實作

     編輯:關於C++

前言:為什麼需要讀取實體序號?

硬碟實體序號(非邏輯磁碟機序號)是廠商燒錄在裝置韌體中的唯一識別碼,常用於軟體授權綁定、資產管理、防盜拷等場景。Windows 不同版本的核心架構差異大,需分別實作:Win9x 使用 Ring 0 直接埠存取;NT 系使用 DeviceIoControl 配合 SMART_RCV_DRIVE_DATA 控制碼。

核心資料結構與常數定義

// IDE 專用
#define MAX_IDE_DRIVES 4
const WORD IDE_ATA_IDENTIFY = 0xEC;    // ATA IDENTIFY 命令
const WORD IDE_ATAPI_IDENTIFY = 0xA1;  // ATAPI IDENTIFY 命令

// SCSI 專用
const DWORD FILE_DEVICE_SCSI = 0x0000001B;
const DWORD IOCTL_SCSI_MINIPORT = 0x0004D008;
const DWORD IOCTL_SCSI_MINIPORT_IDENTIFY = ((FILE_DEVICE_SCSI << 16) + 0x0501);

// IDENTIFY 回傳資料緩衝區大小
const int IDENTIFY_BUFFER_SIZE = 512;

// NT 系 IOCTL 代碼映射
#define GETVERSIONOUTPARAMS GETVERSIONINPARAMS
#define DFP_GET_VERSION SMART_GET_VERSION
#define DFP_SEND_DRIVE_COMMAND SMART_SEND_DRIVE_COMMAND
#define DFP_RCV_DRIVE_DATA SMART_RCV_DRIVE_DATA

統一進入點:平台分派

void ReadPhysicalDrive(TStrings* pSerList, TStrings* pModeList) {
    switch (Win32Platform) {
        case VER_PLATFORM_WIN32_WINDOWS: // Win95/98/ME
            ReadPhysicalDriveOnW9X(pSerList, pModeList);
            break;
        case VER_PLATFORM_WIN32_NT:      // NT/2000/XP/後續版本
            ReadPhysicalDriveOnNT(pSerList, pModeList);
            break;
    }
}

NT 系實作 (Win2000/XP/Server/現代 Windows)

步驟 1:開啟實體磁碟機

char szDriveName[32];
sprintf(szDriveName, "\\\\.\\PhysicalDrive%d", nDrive); // nDrive = 0,1,2,3
HANDLE h = CreateFile(szDriveName,
    GENERIC_READ | GENERIC_WRITE,
    FILE_SHARE_READ | FILE_SHARE_WRITE,
    NULL, OPEN_EXISTING, 0, NULL);

步驟 2:取得版本資訊與裝置對應圖

GETVERSIONOUTPARAMS verParams = {0};
DWORD bytesReturned;
DeviceIoControl(h, DFP_GET_VERSION, NULL, 0,
    &verParams, sizeof(verParams), &bytesReturned, NULL);
// verParams.bIDEDeviceMap 位元圖指示每個 IDE 通道是否有裝置
// bit 4-7: 第 0-3 裝置為 ATAPI (光碟機);bit 0-3: 為 ATA (硬碟)

步驟 3:發送 IDENTIFY 命令

SENDCMDINPARAMS inParams = {0};
inParams.cBufferSize = IDENTIFY_BUFFER_SIZE;
inParams.irDriveRegs.bFeaturesReg = 0;
inParams.irDriveRegs.bSectorCountReg = 1;
inParams.irDriveRegs.bSectorNumberReg = 1;
inParams.irDriveRegs.bCylLowReg = 0;
inParams.irDriveRegs.bCylHighReg = 0;
// 主/從盤選擇:主盤 0xA0,從盤 0xB0
inParams.irDriveRegs.bDriveHeadReg = (nDrive & 1) ? 0xB0 : 0xA0;
// 根據裝置類型選擇命令
BYTE cmd = (verParams.bIDEDeviceMap >> nDrive & 0x10) ? IDE_ATAPI_IDENTIFY : IDE_ATA_IDENTIFY;
inParams.irDriveRegs.bCommandReg = cmd;
inParams.bDriveNumber = nDrive;

BYTE outBuffer[sizeof(SENDCMDOUTPARAMS) + IDENTIFY_BUFFER_SIZE - 1];
DeviceIoControl(h, DFP_RCV_DRIVE_DATA,
    &inParams, sizeof(SENDCMDINPARAMS) - 1,
    outBuffer, sizeof(outBuffer),
    &bytesReturned, NULL);

步驟 4:解析 IDENTIFY 資料 (IDSECTOR 結構)

USHORT* pIdSector = (USHORT*)((SENDCMDOUTPARAMS*)outBuffer)->bBuffer;
DWORD diskData[256];
for (int i = 0; i < 256; i++) diskData[i] = pIdSector[i];

// 序號位於 word 10-19 (共 20 字元),模型號位於 word 27-46 (共 40 字元)
// 需轉換位元組序:每個 WORD 低位在前、高位在後
char* ConvertToString(DWORD* data, int first, int last) {
    static char buf[1024]; int pos = 0;
    for (int i = first; i <= last; i++) {
        buf[pos++] = char(data[i] / 256);   // 高位元組
        buf[pos++] = char(data[i] % 256);   // 低位元組
    }
    buf[pos] = '\0';
    // 去除尾端空白
    for (int i = pos - 1; i > 0 && buf[i] == ' '; i--) buf[i] = '\0';
    return buf;
}

strcpy(serial, ConvertToString(diskData, 10, 19));
strcpy(model, ConvertToString(diskData, 27, 46));

Win9x 實作:Ring 0 直接埠 I/O

Win9x 允許透過修改 IDT (中斷描述符表) 進入 Ring 0 執行 IN/OUT 指令直接存取 IDE 控制器埠。關鍵步驟:

IDE 控制器基底位址

  • Primary IDE: 0x1F0
  • Secondary IDE: 0x170
  • Tertiary: 0x1E8
  • Quaternary: 0x168

Ring 0 進入機制

// 修改 IDT 第 5 號中斷門 (INT 5) 指向自訂 Ring 0 程式碼
SIDT FWORD PTR idtr;
EAX = DWORD PTR [idtr + 2] + 5 * 8 + 4; // 第 5 個描述符的高 32 位位址
CLI
// 保存原處理器入口,寫入新入口
MOV WORD PTR [EAX-4], BX  // 低 16 位
SHR EBX, 16
MOV WORD PTR [EAX+2], BX  // 高 16 位
INT 5  // 觸發進入 Ring 0
// Ring 0 程式碼執行完以 IRET 返回
// 恢復原 IDT 入口
STI

Ring 0 核心流程

  1. 等待控制器不忙 (IN AL, DX 檢查狀態埠 bit 7)
  2. 選擇主/從盤 (OUT DX, AL 到驅動器/磁頭暫存器)
  3. 發送 IDENTIFY 命令 (OUT DX, AL 到命令暫存器,AL=0xEC 或 0xA1)
  4. 等待完成,檢查錯誤 (狀態埠 bit 0)
  5. 若 ATA 命令失敗,嘗試 ATAPI 命令
  6. 讀取 256 WORD 資料 (REP INSW 從資料埠 0x1F0)

藍屏避坑:首次呼叫不執行命令

經驗發現:首次進入 Ring 0 時若執行 WaitWhileBusy 後續命令會導致藍屏。解決方案:第一次呼叫只做裝置存在性檢測,不發送命令;第二次呼叫才執行完整 IDENTIFY 流程。

SCSI 裝置支援 (NT 系)

透過 IOCTL_SCSI_MINIPORT 發送 SCSI PASS-THROUGH 指令,封裝 ATA IDENTIFY 命令。較複雜,需構建 SRB_IO_CONTROL、SCSI_PASS_THROUGH 結構,本文程式碼包含 ReadIDEDriveAsScsiDriveOnNT() 參考實作。

關鍵工具函式:位元組序轉換

char* ConvertToString(DWORD dwDiskData[256], int nFirstIndex, int nLastIndex) {
    static char szResBuf[1024];
    int nPosition = 0;
    for (int nIndex = nFirstIndex; nIndex <= nLastIndex; nIndex++) {
        szResBuf[nPosition++] = char(dwDiskData[nIndex] / 256); // 高位元組
        szResBuf[nPosition++] = char(dwDiskData[nIndex] % 256); // 低位元組
    }
    szResBuf[nPosition] = '\0';
    // 移除尾端空白
    for (int nIndex = nPosition - 1; nIndex > 0 && szResBuf[nIndex] == ' '; nIndex--)
        szResBuf[nIndex] = '\0';
    return szResBuf;
}

使用範例

void __fastcall TForm1::Button1Click(TObject* Sender) {
    ReadPhysicalDrive(Memo1->Lines, Memo2->Lines);
    // Memo1 顯示序號清單,Memo2 顯示型號清單
}

現代化考量與替代方案

  • WMI (Windows Management Instrumentation):SELECT SerialNumber FROM Win32_DiskDrive — 無需驅動程式權限,跨版本相容,推薦現代專案使用。
  • Storage Device Query API (Windows 8+):IOCTL_STORAGE_QUERY_PROPERTY 配合 StorageDeviceProperty。
  • 權限問題:實體磁碟機存取需系統管理員權限;WMI 查詢通常只需標準使用者權限。
  • 虛擬機/RAID/USB 裝置:IDENTIFY 命令回傳資料可能不同,需實測驗證。

總結

本文完整展示了跨 Windows 版本讀取硬碟實體序號的底層技術:NT 系採用標準 DeviceIoControl + SMART IOCTL,Win9x 採用 Ring 0 埠 I/O 並修補 IDT。雖然現代開發推薦使用 WMI 等高層 API,但理解底層實作對於驅動程式開發、韌體工具、安全性研究仍具重要價值。程式碼已在 Win2000/XP 實測通過,Win9x 部分需注意藍屏避坑邏輯。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved