硬碟實體序號(非邏輯磁碟機序號)是廠商燒錄在裝置韌體中的唯一識別碼,常用於軟體授權綁定、資產管理、防盜拷等場景。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;
}
}
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);
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 (硬碟)
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);
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 允許透過修改 IDT (中斷描述符表) 進入 Ring 0 執行 IN/OUT 指令直接存取 IDE 控制器埠。關鍵步驟:
// 修改 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
IN AL, DX 檢查狀態埠 bit 7)OUT DX, AL 到驅動器/磁頭暫存器)OUT DX, AL 到命令暫存器,AL=0xEC 或 0xA1)REP INSW 從資料埠 0x1F0)經驗發現:首次進入 Ring 0 時若執行 WaitWhileBusy 後續命令會導致藍屏。解決方案:第一次呼叫只做裝置存在性檢測,不發送命令;第二次呼叫才執行完整 IDENTIFY 流程。
透過 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 顯示型號清單
}
SELECT SerialNumber FROM Win32_DiskDrive — 無需驅動程式權限,跨版本相容,推薦現代專案使用。IOCTL_STORAGE_QUERY_PROPERTY 配合 StorageDeviceProperty。本文完整展示了跨 Windows 版本讀取硬碟實體序號的底層技術:NT 系採用標準 DeviceIoControl + SMART IOCTL,Win9x 採用 Ring 0 埠 I/O 並修補 IDT。雖然現代開發推薦使用 WMI 等高層 API,但理解底層實作對於驅動程式開發、韌體工具、安全性研究仍具重要價值。程式碼已在 Win2000/XP 實測通過,Win9x 部分需注意藍屏避坑邏輯。