在早期的 DOS 與 Windows 9x 時代,開發者常需直接操作硬體以獲取硬碟詳細資訊。本文介紹如何透過 I/O 端口指令,直接向 IDE 控制器發送 IDENTIFY DEVICE (0xEC) 命令,並解析返回的 256 字資料區塊,以取得型號、序號、韌體版本及幾何參數等關鍵資訊。雖然現代作業系統已限制應用層直接存取硬體,但此技術對理解底層儲存架構、編寫啟動磁區工具或進行嵌入式開發仍具參考價值。
程式首先從 BIOS 資料區 (0x40:0x75) 讀取硬碟數量,隨後針對每顆磁碟執行識別流程。關鍵在於輪詢狀態埠 (0x1F7) 的 BSY 位 (Bit 7),確認控制器閒置後,才能安全發送命令。
透過裝置/磁頭暫存器 (0x1F6) 選擇 Master (0xA0) 或 Slave (0xB0) 裝置,隨後向命令埠 (0x1F7) 寫入 0xEC。此時控制器開始準備識別資料,程式需再次等待 DRQ 位 (Bit 3) 置位,代表資料已就緒可讀。
資料埠 (0x1F0) 以 16 位字元模式連續輸出 256 個字 (512 Bytes)。原始碼中的 GetAscii 函式負責將特定位移範圍的高低位元組轉換為可讀字串,對應 ATA 規格定義的欄位:
其餘位移則包含柱面/磁頭/扇區數、緩衝區大小、ECC 位元組數等幾何與效能參數,程式並行呼叫 BIOS INT 13h (AH=08h) 取得 BIOS 回報的幾何資訊,兩者對比可發現邏輯/物理參數差異。
#include <dos.h>
#include <conio.h>
#include <stdio.h>
#include <string.h>
char *GetAscii(unsigned int inData[], int offStart, int offEnd);
int main(void) {
unsigned int diskData[256];
unsigned int offset;
int loop, numDrv;
union REGS regs;
unsigned int biosCyl[2], biosHead[2], biosSec[2];
numDrv = peekb(0x40, 0x75); // BIOS 硬碟計數
for (loop = 0; loop < numDrv; loop++) {
// 等待控制器就緒
while (inp(0x1F7) != 0x50);
// 選擇裝置:Master=0xA0, Slave=0xB0
outp(0x1F6, (loop == 0 ? 0xA0 : 0xB0));
outp(0x1F7, 0xEC); // IDENTIFY DEVICE
// 等待資料就緒 (DRQ=1)
while (inp(0x1F7) != 0x58);
// 讀取 256 字 (512 Bytes)
for (offset = 0; offset != 256; offset++)
diskData[offset] = inpw(0x1F0);
// 取得 BIOS 幾何參數
regs.h.ah = 0x08;
regs.h.dl = 0x80 + loop;
int86(0x13, ®s, ®s);
if (!regs.x.cflag) {
biosHead[loop] = regs.h.dh + 1;
biosSec[loop] = regs.h.cl & 0x3F;
biosCyl[loop] = ((regs.h.cl & 0xC0) << 2) + regs.h.ch + 2;
}
// 輸出結果
printf("DRIVE %d:\n", loop);
printf("Model Number______________________: %s\n", GetAscii(diskData, 27, 46));
printf("Serial Number_____________________: %s\n", GetAscii(diskData, 10, 19));
printf("Controller Revision Number________: %s\n\n", GetAscii(diskData, 23, 26));
printf("Able to do Double Word Transfer___: %6s\n", (diskData[48] == 0 ? "No" : "Yes"));
printf("Controller type___________________: %04X\n", diskData[20]);
printf("Controller buffer size (bytes)____: %6u\n", diskData[21] * 512);
printf("Number of ECC bytes transferred___: %6u\n", diskData[22]);
printf("Number of sectors per interrupt___: %6u\n\n", diskData[47]);
printf("Hard Disk Reports\n");
printf("Number of Cylinders (Fixed)_______: %6u\n", diskData[1]);
printf("Number of Heads___________________: %6u\n", diskData[3]);
printf("Number of Sectors per Track_______: %6u\n\n", diskData[6]);
printf("BIOS Reports\n");
printf("Number of Cylinders_______________: %6u\n", biosCyl[loop]);
printf("Number of Heads___________________: %6u\n", biosHead[loop]);
printf("Number of Sectors per Track_______: %6u\n\n", biosSec[loop]);
printf("Press any key to continue...\n\n");
getch();
}
return 0;
}
char *GetAscii(unsigned int inData[], int offStart, int offEnd) {
static char retVal[255];
int loop, loop1;
for (loop = offStart, loop1 = 0; loop <= offEnd; loop++) {
retVal[loop1++] = (char)(inData[loop] >> 8); // High byte
retVal[loop1++] = (char)(inData[loop] & 0xFF); // Low byte
}
retVal[loop1] = '\0';
return retVal;
}
在 Windows NT 核心系列 (含 2000/XP/Vista/7/10/11) 中,應用程式無法直接使用 inp/outp 存取 I/O 端口。開發者應改用:
DeviceIoControl 配合 IOCTL_STORAGE_QUERY_PROPERTY 或 SMART_RCV_DRIVE_DATA 取得識別資料。Win32_DiskDrive 類別的 Model、SerialNumber、FirmwareRevision 等屬性。IOCTL_ATA_PASS_THROUGH_DIRECT 直接下達 ATA 指令,最接近本文做法且受作業系統保護。本範例展示了經典的「軟體直接驅動硬體」思維,雖已不適用於現代受保護模式作業系統,但其遵循的 ATA/ATAPI 命令協定至今仍是 SATA/SSD 控制器的基礎。理解此流程有助於深入掌握儲存子系統運作、開發韌體更新工具或進行數位鑑識分析。