DeviceIoControl

 實戰DeviceIoControl 之一:通過API訪問設備驅動程序

Q 在NT/2000/XP中,我想用VC編寫應用程序訪問硬件設備,如獲取磁盤參數、讀寫絕對扇區數據、測試光驅實際速度等,該從哪裏入手呢?

A 在NT/2000/XP中,應用程序可以通過API函數DeviceIoControl來實現對設備的訪問—獲取信息,發送命令,交換數據等。利用該接口函數向指定的設備驅動發送正確的控制碼及數據,然後分析它的響應,就可以達到我們的目的。

DeviceIoControl的函數原型爲

BOOL DeviceIoControl( HANDLE hDevice, // 設備句柄 DWORD dwIoControlCode, // 控制碼 LPVOID lpInBuffer, // 輸入數據緩衝區指針 DWORD nInBufferSize, // 輸入數據緩衝區長度 LPVOID lpOutBuffer, // 輸出數據緩衝區指針 DWORD nOutBufferSize, // 輸出數據緩衝區長度 LPDWORD lpBytesReturned, // 輸出數據實際長度單元長度 LPOVERLAPPED lpOverlapped // 重疊操作結構指針 ); 

設備句柄用來標識你所訪問的設備。

發送不同的控制碼,可以調用設備驅動程序的不同類型的功能。在頭文件winioctl.h中,預定義的標準設備控制碼,都以IOCTL或FSCTL開頭。例如,IOCTL_DISK_GET_DRIVE_GEOMETRY是對物理驅動器取結構參數(介質類型、柱面數、每柱面磁道數、每磁道扇區數等)的控制碼,FSCTL_LOCK_VOLUME是對邏輯驅動器的卷加鎖的控制碼。

輸入輸出數據緩衝區是否需要,是何種結構,以及佔多少字節空間,完全由不同設備的不同操作類型決定。在頭文件winioctl.h中,已經爲標準設備預定義了一些輸入輸出數據結構。重疊操作結構指針設置爲NULL,DeviceIoControl將進行阻塞調用;否則,應在編程時按異步操作設計。

Q 設備句柄是從哪裏獲得的?

A 設備句柄可以用API函數CreateFile獲得。它的原型爲

HANDLE CreateFile( LPCTSTR lpFileName, // 文件名/設備路徑 DWORD dwDesiredAccess, // 訪問方式 DWORD dwShareMode, // 共享方式 LPSECURITY_ATTRIBUTES lpSecurityAttributes, // 安全描述符指針 DWORD dwCreationDisposition, // 創建方式 DWORD dwFlagsAndAttributes, // 文件屬性及標誌 HANDLE hTemplateFile // 模板文件的句柄 ); 

CreateFile這個函數用處很多,這裏我們用它“打開”設備驅動程序,得到設備的句柄。操作完成後用CloseHandle關閉設備句柄。

與普通文件名有所不同,設備驅動的“文件名”(常稱爲“設備路徑”)形式固定爲“\\.\DeviceName”(注意在C程序中該字符串寫法爲“\\\\.\\DeviceName”),DeviceName必須與設備驅動程序內定義的設備名稱一致。

一般地,調用CreateFile獲得設備句柄時,訪問方式參數設置爲0或GENERIC_READ|GENERIC_WRITE,共享方式參數設置爲FILE_SHARE_READ|FILE_SHARE_WRITE,創建方式參數設置爲OPEN_EXISTING,其它參數設置爲0或NULL。

Q 可是,我怎麼知道設備名稱是什麼呢?

A 一些存儲設備的名稱是微軟定義好的,不可能有什麼變化。大體列出如下

軟盤驅動器 A:, B:
硬盤邏輯分區 C:, D:, E:, ...
物理驅動器 PHYSICALDRIVEx
CD-ROM, DVD/ROM CDROMx
磁帶機 TAPEx

其中,物理驅動器不包括軟驅和光驅。邏輯驅動器可以是IDE/SCSI/PCMCIA/USB接口的硬盤分區(卷)、光驅、MO、CF卡等,甚至是虛擬盤。x=0,1,2 ……

其它的設備名稱需通過驅動接口的GUID調用設備管理函數族取得,這裏暫不討論。

Q 請舉一個簡單的例子說明如何通過DeviceIoControl訪問設備驅動程序。

A 這裏有一個從MSDN上摘抄來的demo程序,演示在NT/2000/XP中如何通過DeviceIoControl獲取硬盤的基本參數。

/* The code of interest is in the subroutine GetDriveGeometry. The code in main shows how to interpret the results of the IOCTL call. */ #include <windows.h> #include <winioctl.h> BOOL GetDriveGeometry(DISK_GEOMETRY *pdg) { HANDLE hDevice; // handle to the drive to be examined BOOL bResult; // results flag DWORD junk; // discard results hDevice = CreateFile("\\\\.\\PhysicalDrive0", // drive to open 0, // no access to the drive FILE_SHARE_READ | // share mode FILE_SHARE_WRITE, NULL, // default security attributes OPEN_EXISTING, // disposition 0, // file attributes NULL); // do not copy file attributes if (hDevice == INVALID_HANDLE_VALUE) // cannot open the drive { return (FALSE); } bResult = DeviceIoControl(hDevice, // device to be queried IOCTL_DISK_GET_DRIVE_GEOMETRY, // operation to perform NULL, 0, // no input buffer pdg, sizeof(*pdg), // output buffer &junk, // # bytes returned (LPOVERLAPPED) NULL); // synchronous I/O CloseHandle(hDevice); return (bResult); } int main(int argc, char *argv[]) { DISK_GEOMETRY pdg; // disk drive geometry structure BOOL bResult; // generic results flag ULONGLONG DiskSize; // size of the drive, in bytes bResult = GetDriveGeometry (&pdg); if (bResult) { printf("Cylinders = %I64d\n", pdg.Cylinders); printf("Tracks per cylinder = %ld\n", (ULONG) pdg.TracksPerCylinder); printf("Sectors per track = %ld\n", (ULONG) pdg.SectorsPerTrack); printf("Bytes per sector = %ld\n", (ULONG) pdg.BytesPerSector); DiskSize = pdg.Cylinders.QuadPart * (ULONG)pdg.TracksPerCylinder * (ULONG)pdg.SectorsPerTrack * (ULONG)pdg.BytesPerSector; printf("Disk size = %I64d (Bytes) = %I64d (Mb)\n", DiskSize, DiskSize / (1024 * 1024)); } else { printf("GetDriveGeometry failed. Error %ld.\n", GetLastError()); } return ((int)bResult); } 

Q 如果將設備名換成“A:”就可以取A盤參數,換成“CDROM0”就可以取CDROM參數,是這樣嗎?

A 這個問題暫不做回答。請動手試一下。

現在我們總結一下通過DeviceIoControl訪問設備驅動程序的“三步曲”:首先用CreateFile取得設備句柄,然後用DeviceIoControl與設備進行I/O,最後別忘記用CloseHandle關閉設備句柄。

 

實戰DeviceIoControl 之二:獲取軟盤/硬盤/光盤的參數

Q 在MSDN的那個demo中,將設備名換成“A:”取A盤參數,先用資源管理器讀一下盤,再運行這個程序可以成功,但換一張盤後就失敗;換成“CDROM0”取CDROM參數,無論如何都不行。這個問題如何解決呢?

A 取軟盤參數是從軟盤上讀取格式化後的信息,也就是必須執行讀操作,這一點與硬盤不同。將CreateFile中的訪問方式改爲GENERIC_READ就行了。

IOCTL_DISK_GET_DRIVE_GEOMETRY這個I/O控制碼,對軟盤和硬盤有效,但對一些可移動媒介如CD/DVD-ROM、TAPE等就不管用了。要取CDROM參數,還得另闢蹊徑。IOCTL_STORAGE_GET_MEDIA_TYPES_EX能夠幫我們解決問題。

Q 使用這些I/O控制碼,需要什麼樣的輸入輸出數據格式呢?

A DeviceIoControl使用這兩個控制碼時,都不需要輸入數據。

IOCTL_DISK_GET_DRIVE_GEOMETRY直接輸出一個DISK_GEOMETRY結構:

typedef struct _DISK_GEOMETRY { LARGE_INTEGER Cylinders; // 柱面數 MEDIA_TYPE MediaType; // 介質類型 DWORD TracksPerCylinder; // 每柱面的磁道數 DWORD SectorsPerTrack; // 每磁道的扇區數 DWORD BytesPerSector; // 每扇區的字節數 } DISK_GEOMETRY; 

IOCTL_STORAGE_GET_MEDIA_TYPES_EX輸出一個GET_MEDIA_TYPES結構:

typedef struct _GET_MEDIA_TYPES { DWORD DeviceType; // 設備類型 DWORD MediaInfoCount; // 介質信息條數 DEVICE_MEDIA_INFO MediaInfo[1]; // 介質信息 } GET_MEDIA_TYPES; 

讓我們來看一下DEVICE_MEDIA_INFO結構的定義:

typedef struct _DEVICE_MEDIA_INFO { union { struct { LARGE_INTEGER Cylinders; // 柱面數 STORAGE_MEDIA_TYPE MediaType; // 介質類型 DWORD TracksPerCylinder; // 每柱面的磁道數 DWORD SectorsPerTrack; // 每磁道的扇區數 DWORD BytesPerSector; // 每扇區的字節數 DWORD NumberMediaSides; // 介質面數 DWORD MediaCharacteristics; // 介質特性 } DiskInfo; // 硬盤信息 struct { LARGE_INTEGER Cylinders; // 柱面數 STORAGE_MEDIA_TYPE MediaType; // 介質類型 DWORD TracksPerCylinder; // 每柱面的磁道數 DWORD SectorsPerTrack; // 每磁道的扇區數 DWORD BytesPerSector; // 每扇區的字節數 DWORD NumberMediaSides; // 介質面數 DWORD MediaCharacteristics; // 介質特性 } RemovableDiskInfo; // “可移動盤”信息 struct { STORAGE_MEDIA_TYPE MediaType; // 介質類型 DWORD MediaCharacteristics; // 介質特性 DWORD CurrentBlockSize; // 塊的大小 } TapeInfo; // 磁帶信息 } DeviceSpecific; } DEVICE_MEDIA_INFO; 

其中CD-ROM屬於“可移動盤”的範圍。請注意,GET_MEDIA_TYPES結構本身只定義了一條DEVICE_MEDIA_INFO,額外的DEVICE_MEDIA_INFO需要緊接此結構的另外的空間。

Q 調用方法我瞭解了,請用VC舉個例子來實現我所期待已久的功能吧?

A 好,現在就演示一下如何取軟盤/硬盤/光盤的參數。測試時,記得要有軟盤/光盤插在驅動器裏喔!

首先,用MFC AppWizard生成一個單文檔的應用程序,取名爲DiskGeometry,讓它的View基於CEditView。

然後,添加以下的.h和.cpp文件。

////////////////////////////////////////////////////////////////////////////// // GetDiskGeometry.h ////////////////////////////////////////////////////////////////////////////// #if !defined(GET_DISK_GEOMETRY_H__) #define GET_DISK_GEOMETRY_H__ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #include <winioctl.h> BOOL GetDriveGeometry(const char* filename, DISK_GEOMETRY *pdg); #endif // !defined(GET_DISK_GEOMETRY_H__) ////////////////////////////////////////////////////////////////////////////// // GetDiskGeometry.cpp ////////////////////////////////////////////////////////////////////////////// #include "stdafx.h" #include "GetDiskGeometry.h" // IOCTL_STORAGE_GET_MEDIA_TYPES_EX可能返回不止一條DEVICE_MEDIA_INFO,故定義足夠的空間 #define MEDIA_INFO_SIZE sizeof(GET_MEDIA_TYPES)+15*sizeof(DEVICE_MEDIA_INFO) // filename -- 用於設備的文件名 // pdg -- 參數緩衝區指針 BOOL GetDriveGeometry(const char* filename, DISK_GEOMETRY *pdg) { HANDLE hDevice; // 設備句柄 BOOL bResult; // DeviceIoControl的返回結果 GET_MEDIA_TYPES *pmt; // 內部用的輸出緩衝區 DWORD dwOutBytes; // 輸出數據長度 // 打開設備 hDevice = ::CreateFile(filename, // 文件名 GENERIC_READ, // 軟驅需要讀盤 FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享方式 NULL, // 默認的安全描述符 OPEN_EXISTING, // 創建方式 0, // 不需設置文件屬性 NULL); // 不需參照模板文件 if (hDevice == INVALID_HANDLE_VALUE) { // 設備無法打開... return FALSE; } // 用IOCTL_DISK_GET_DRIVE_GEOMETRY取磁盤參數 bResult = ::DeviceIoControl(hDevice, // 設備句柄 IOCTL_DISK_GET_DRIVE_GEOMETRY, // 取磁盤參數 NULL, 0, // 不需要輸入數據 pdg, sizeof(DISK_GEOMETRY), // 輸出數據緩衝區 &dwOutBytes, // 輸出數據長度 (LPOVERLAPPED)NULL); // 用同步I/O // 如果失敗,再用IOCTL_STORAGE_GET_MEDIA_TYPES_EX取介質類型參數 if (!bResult) { pmt = (GET_MEDIA_TYPES *)new BYTE[MEDIA_INFO_SIZE]; bResult = ::DeviceIoControl(hDevice, // 設備句柄 IOCTL_STORAGE_GET_MEDIA_TYPES_EX, // 取介質類型參數 NULL, 0, // 不需要輸入數據 pmt, MEDIA_INFO_SIZE, // 輸出數據緩衝區 &dwOutBytes, // 輸出數據長度 (LPOVERLAPPED)NULL); // 用同步I/O if (bResult) { // 注意到結構DEVICE_MEDIA_INFO是在結構DISK_GEOMETRY的基礎上擴充的 // 爲簡化程序,用memcpy代替如下多條賦值語句: // pdg->MediaType = (MEDIA_TYPE)pmt->MediaInfo[0].DeviceSpecific.DiskInfo.MediaType; // pdg->Cylinders = pmt->MediaInfo[0].DeviceSpecific.DiskInfo.Cylinders; // pdg->TracksPerCylinder = pmt->MediaInfo[0].DeviceSpecific.DiskInfo.TracksPerCylinder; // ... ... ::memcpy(pdg, pmt->MediaInfo, sizeof(DISK_GEOMETRY)); } delete pmt; } // 關閉設備句柄 ::CloseHandle(hDevice); return (bResult); } 

然後,在Toolbar的IDR_MAINFRAME上添加一個按鈕,ID爲ID_GET_DISK_GEOMETRY。打開ClassWizard,在DiskGeometryView中

添加ID_GET_DISK_GEOMETRY的映射函數OnGetDiskGeometry。打開DiskGeometryView.cpp,包含頭文件GetDiskGeometry.h。

在OnGetDiskGeometry中,添加以下代碼

 const char *szDevName[]= { "\\\\.\\A:", "\\\\.\\B:", "\\\\.\\PhysicalDrive0", "\\\\.\\PhysicalDrive1", "\\\\.\\PhysicalDrive2", "\\\\.\\PhysicalDrive3", "\\\\.\\Cdrom0", "\\\\.\\Cdrom1", }; DISK_GEOMETRY dg; ULONGLONG DiskSize; BOOL bResult; CString strMsg; CString strTmp; for (int i = 0; i < sizeof(szDevName)/sizeof(char*); i++) { bResult = GetDriveGeometry(szDevName[i], &dg); strTmp.Format("\r\n%s result = %s\r\n", szDevName[i], bResult ? "success" : "failure"); strMsg+=strTmp; if (!bResult) continue; strTmp.Format(" Media Type = %d\r\n", dg.MediaType); strMsg+=strTmp; strTmp.Format(" Cylinders = %I64d\r\n", dg.Cylinders); strMsg+=strTmp; strTmp.Format(" Tracks per cylinder = %ld\r\n", (ULONG) dg.TracksPerCylinder); strMsg+=strTmp; strTmp.Format(" Sectors per track = %ld\r\n", (ULONG) dg.SectorsPerTrack); strMsg+=strTmp; strTmp.Format(" Bytes per sector = %ld\r\n", (ULONG) dg.BytesPerSector); strMsg+=strTmp; DiskSize = dg.Cylinders.QuadPart * (ULONG)dg.TracksPerCylinder * (ULONG)dg.SectorsPerTrack * (ULONG)dg.BytesPerSector; strTmp.Format(" Disk size = %I64d (Bytes) = %I64d (Mb)\r\n", DiskSize, DiskSize / (1024 * 1024)); strMsg+=strTmp; } CEdit& Edit = GetEditCtrl(); Edit.SetWindowText(strMsg); 

最後,最後幹什麼呢?編譯,運行......

本文Demo源碼:DiskGeometry.zip (21KB)

實戰DeviceIoControl 之三:製作磁盤鏡像文件

Q DOS命令DISKCOPY給我很深的印象,現在也有許多“克隆”軟件,可以對磁盤進行全盤複製。我想,要製作磁盤鏡像文件,DeviceIoControl應該很有用武之地吧?

A 是的。這裏舉一個製作軟盤鏡像文件,功能類似於“DISKCOPY”的例子。

本例實現其功能的核心代碼如下:

// 打開磁盤 HANDLE OpenDisk(LPCTSTR filename) { HANDLE hDisk; // 打開設備 hDisk = ::CreateFile(filename, // 文件名 GENERIC_READ | GENERIC_WRITE, // 讀寫方式 FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享方式 NULL, // 默認的安全描述符 OPEN_EXISTING, // 創建方式 0, // 不需設置文件屬性 NULL); // 不需參照模板文件 return hDisk; } // 獲取磁盤參數 BOOL GetDiskGeometry(HANDLE hDisk, PDISK_GEOMETRY lpGeometry) { DWORD dwOutBytes; BOOL bResult; // 用IOCTL_DISK_GET_DRIVE_GEOMETRY取磁盤參數 bResult = ::DeviceIoControl(hDisk, // 設備句柄 IOCTL_DISK_GET_DRIVE_GEOMETRY, // 取磁盤參數 NULL, 0, // 不需要輸入數據 lpGeometry, sizeof(DISK_GEOMETRY), // 輸出數據緩衝區 &dwOutBytes, // 輸出數據長度 (LPOVERLAPPED)NULL); // 用同步I/O return bResult; } // 從指定磁道開始讀磁盤 BOOL ReadTracks(HANDLE hDisk, PDISK_GEOMETRY lpGeometry, LPVOID pBuf, DWORD dwStartCylinder, DWORD dwCylinderNumber) { DWORD VirtBufSize; DWORD BytesRead; // 大小 VirtBufSize = lpGeometry->TracksPerCylinder * lpGeometry->SectorsPerTrack * lpGeometry->BytesPerSector; // 偏移 ::SetFilePointer(hDisk, VirtBufSize*dwStartCylinder, NULL, FILE_BEGIN); return ::ReadFile(hDisk, pBuf, VirtBufSize*dwCylinderNumber, &BytesRead, NULL); } // 從指定磁道開始寫磁盤 BOOL WriteTracks(HANDLE hDisk, PDISK_GEOMETRY lpGeometry, LPVOID pBuf, DWORD dwStartCylinder, DWORD dwCylinderNumber) { DWORD VirtBufSize; DWORD BytesWritten; // 大小 VirtBufSize = lpGeometry->TracksPerCylinder * lpGeometry->SectorsPerTrack * lpGeometry->BytesPerSector; // 偏移 ::SetFilePointer(hDisk, VirtBufSize*dwStartCylinder, NULL, FILE_BEGIN); return ::WriteFile(hDisk, pBuf, VirtBufSize*dwCylinderNumber, &BytesWritten, NULL); } // 從指定磁道開始格式化磁盤 BOOL LowLevelFormatTracks(HANDLE hDisk, PDISK_GEOMETRY lpGeometry, DWORD dwStartCylinder, DWORD dwCylinderNumber) { FORMAT_PARAMETERS FormatParameters; PBAD_TRACK_NUMBER lpBadTrack; DWORD dwOutBytes; DWORD dwBufSize; BOOL bResult; FormatParameters.MediaType = lpGeometry->MediaType; FormatParameters.StartCylinderNumber = dwStartCylinder; FormatParameters.EndCylinderNumber = dwStartCylinder + dwCylinderNumber - 1; FormatParameters.StartHeadNumber = 0; FormatParameters.EndHeadNumber = lpGeometry->TracksPerCylinder - 1; dwBufSize = lpGeometry->TracksPerCylinder * sizeof(BAD_TRACK_NUMBER); lpBadTrack = (PBAD_TRACK_NUMBER) new BYTE[dwBufSize]; // 用IOCTL_DISK_FORMAT_TRACKS對連續磁道進行低級格式化 bResult = ::DeviceIoControl(hDisk, // 設備句柄 IOCTL_DISK_FORMAT_TRACKS, // 低級格式化 &FormatParameters, sizeof(FormatParameters), // 輸入數據緩衝區 lpBadTrack, dwBufSize, // 輸出數據緩衝區 &dwOutBytes, // 輸出數據長度 (LPOVERLAPPED)NULL); // 用同步I/O delete lpBadTrack; return bResult; } // 將卷鎖定 BOOL LockVolume(HANDLE hDisk) { DWORD dwOutBytes; BOOL bResult; // 用FSCTL_LOCK_VOLUME鎖卷 bResult = ::DeviceIoControl(hDisk, // 設備句柄 FSCTL_LOCK_VOLUME, // 鎖卷 NULL, 0, // 不需要輸入數據 NULL, 0, // 不需要輸出數據 &dwOutBytes, // 輸出數據長度 (LPOVERLAPPED)NULL); // 用同步I/O return bResult; } // 將卷解鎖 BOOL UnlockVolume(HANDLE hDisk) { DWORD dwOutBytes; BOOL bResult; // 用FSCTL_UNLOCK_VOLUME開卷鎖 bResult = ::DeviceIoControl(hDisk, // 設備句柄 FSCTL_UNLOCK_VOLUME, // 開卷鎖 NULL, 0, // 不需要輸入數據 NULL, 0, // 不需要輸出數據 &dwOutBytes, // 輸出數據長度 (LPOVERLAPPED)NULL); // 用同步I/O return bResult; } // 將卷卸下 // 該操作使系統重新辨識磁盤,等效於重新插盤 BOOL DismountVolume(HANDLE hDisk) { DWORD dwOutBytes; BOOL bResult; // 用FSCTL_DISMOUNT_VOLUME卸卷 bResult = ::DeviceIoControl(hDisk, // 設備句柄 FSCTL_DISMOUNT_VOLUME, // 卸卷 NULL, 0, // 不需要輸入數據 NULL, 0, // 不需要輸出數據 &dwOutBytes, // 輸出數據長度 (LPOVERLAPPED)NULL); // 用同步I/O return bResult; } 

將軟盤保存成鏡像文件的步驟簡單描述爲:
1、創建空的鏡像文件。
2、調用OpenDisk打開軟盤。成功轉3,失敗轉8。
3、調用LockVolume將卷鎖定。成功轉4,失敗轉7。
4、調用GetDiskGeometry獲取參數。成功轉5,失敗轉6。
5、將磁盤參數寫入鏡像文件作爲文件頭。調用ReadTracks按柱面讀出數據,保存在鏡像文件中。循環次數等於柱面數。
6、調用UnlockVolume將卷解鎖。
7、調用CloseDisk關閉軟盤。
8、關閉鏡像文件。

將鏡像文件載入軟盤的步驟簡單描述爲:
1、打開鏡像文件。
2、調用OpenDisk打開軟盤。成功轉3,失敗轉11。
3、調用LockVolume將卷鎖定。成功轉4,失敗轉10。
4、調用GetDiskGeometry獲取參數。成功轉5,失敗轉9。
5、從鏡像文件中讀出文件頭,判斷兩個磁盤參數是否一致。不一致轉6,否則轉7。
6、調用LowLevelFormatTracks按柱面格式化軟盤。循環次數等於柱面數。成功轉7,失敗轉8。
7、從鏡像文件中讀出數據,並調用WriteTracks按柱面寫入磁盤。循環次數等於柱面數。
8、調用DismountVolume將卷卸下。
9、調用UnlockVolume將卷解鎖。
10、調用CloseDisk關閉軟盤。
11、關閉鏡像文件。

Q 我注意到,磁盤讀寫和格式化是按柱面進行的,有什麼道理嗎?

A 沒有特別的原因,只是因爲在這個例子中可以方便地顯示處理進度。

有一點需要特別提及,按絕對地址讀寫磁盤數據時,“最小單位”是扇區,地址一定要與扇區對齊,長度也要等於扇區長度的整數倍。比如,每扇區512字節,那麼起始地址和數據長度都應能被512整除才行。

Q 我忽然產生了一個偉大的想法,用絕對地址讀寫的方式使用磁盤,包括U盤啦,MO啦,而不是用現成的文件系統,那不是可以將數據保密了嗎?

A 當然,只要你喜歡。可千萬別在你的系統盤上做試驗,否則......可別怪bhw98沒有提醒過你喔!

Q 我知道怎麼測試光驅的傳輸速度了,就用上面的方法,讀出一定長度數據,除以所需時間,應該可以吧?

A 可以。但取光盤參數時要用IOCTL_STORAGE_GET_MEDIA_TYPES_EX,我們已經探討過的。

本文Demo源碼:FloppyImage.zip (16KB)Microsoft的例子:vs6samples.exe (134,518KB) 

 

實戰DeviceIoControl 之四:獲取硬盤的詳細信息

Q 用IOCTL_DISK_GET_DRIVE_GEOMETRY或IOCTL_STORAGE_GET_MEDIA_TYPES_EX只能得到很少的磁盤參數,我想獲得包括硬盤序列號在內的更加詳細的信息,有什麼辦法呀?

A 確實,用你所說的I/O控制碼,只能得到最基本的磁盤參數。獲取磁盤出廠信息的I/O控制碼,微軟在VC/MFC環境中沒有開放,在DDK中可以發現一些線索。早先,Lynn McGuire寫了一個很出名的獲取IDE硬盤詳細信息的程序DiskID32,下面的例子是在其基礎上經過增刪和改進而成的。

本例中,我們要用到ATA/APAPI的IDENTIFY DEVICE指令。ATA/APAPI是國際組織T13起草和發佈的IDE/EIDE/UDMA硬盤及其它可移動存儲設備與主機接口的標準,至今已經到了ATA/APAPI-7版本。該接口標準規定了ATA/ATAPI設備的輸入輸出寄存器和指令集。欲瞭解更詳細的ATA/ATAPI技術資料,可訪問T13的站點。

用到的常量及數據結構有以下一些:

// IOCTL控制碼 // #define DFP_SEND_DRIVE_COMMAND 0x0007c084 #define DFP_SEND_DRIVE_COMMAND CTL_CODE(IOCTL_DISK_BASE, 0x0021, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) // #define DFP_RECEIVE_DRIVE_DATA 0x0007c088 #define DFP_RECEIVE_DRIVE_DATA CTL_CODE(IOCTL_DISK_BASE, 0x0022, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) #define FILE_DEVICE_SCSI 0x0000001B #define IOCTL_SCSI_MINIPORT_IDENTIFY ((FILE_DEVICE_SCSI << 16) + 0x0501) #define IOCTL_SCSI_MINIPORT 0x0004D008 // see NTDDSCSI.H for definition // ATA/ATAPI指令 #define IDE_ATA_IDENTIFY 0xEC // ATA的ID指令(IDENTIFY DEVICE) // IDE命令寄存器 typedef struct _IDEREGS { BYTE bFeaturesReg; // 特徵寄存器(用於SMART命令) BYTE bSectorCountReg; // 扇區數目寄存器 BYTE bSectorNumberReg; // 開始扇區寄存器 BYTE bCylLowReg; // 開始柱面低字節寄存器 BYTE bCylHighReg; // 開始柱面高字節寄存器 BYTE bDriveHeadReg; // 驅動器/磁頭寄存器 BYTE bCommandReg; // 指令寄存器 BYTE bReserved; // 保留 } IDEREGS, *PIDEREGS, *LPIDEREGS; // 從驅動程序返回的狀態 typedef struct _DRIVERSTATUS { BYTE bDriverError; // 錯誤碼 BYTE bIDEStatus; // IDE狀態寄存器 BYTE bReserved[2]; // 保留 DWORD dwReserved[2]; // 保留 } DRIVERSTATUS, *PDRIVERSTATUS, *LPDRIVERSTATUS; // IDE設備IOCTL輸入數據結構 typedef struct _SENDCMDINPARAMS { DWORD cBufferSize; // 緩衝區字節數 IDEREGS irDriveRegs; // IDE寄存器組 BYTE bDriveNumber; // 驅動器號 BYTE bReserved[3]; // 保留 DWORD dwReserved[4]; // 保留 BYTE bBuffer[1]; // 輸入緩衝區(此處象徵性地包含1字節) } SENDCMDINPARAMS, *PSENDCMDINPARAMS, *LPSENDCMDINPARAMS; // IDE設備IOCTL輸出數據結構 typedef struct _SENDCMDOUTPARAMS { DWORD cBufferSize; // 緩衝區字節數 DRIVERSTATUS DriverStatus; // 驅動程序返回狀態 BYTE bBuffer[1]; // 輸入緩衝區(此處象徵性地包含1字節) } SENDCMDOUTPARAMS, *PSENDCMDOUTPARAMS, *LPSENDCMDOUTPARAMS; // IDE的ID命令返回的數據 // 共512字節(256個WORD),這裏僅定義了一些感興趣的項(基本上依據ATA/ATAPI-4) typedef struct _IDINFO { USHORT wGenConfig; // WORD 0: 基本信息字 USHORT wNumCyls; // WORD 1: 柱面數 USHORT wReserved2; // WORD 2: 保留 USHORT wNumHeads; // WORD 3: 磁頭數 USHORT wReserved4; // WORD 4: 保留 USHORT wReserved5; // WORD 5: 保留 USHORT wNumSectorsPerTrack; // WORD 6: 每磁道扇區數 USHORT wVendorUnique[3]; // WORD 7-9: 廠家設定值 CHAR sSerialNumber[20]; // WORD 10-19:序列號 USHORT wBufferType; // WORD 20: 緩衝類型 USHORT wBufferSize; // WORD 21: 緩衝大小 USHORT wECCSize; // WORD 22: ECC校驗大小 CHAR sFirmwareRev[8]; // WORD 23-26: 固件版本 CHAR sModelNumber[40]; // WORD 27-46: 內部型號 USHORT wMoreVendorUnique; // WORD 47: 廠家設定值 USHORT wReserved48; // WORD 48: 保留 struct { USHORT reserved1:8; USHORT DMA:1; // 1=支持DMA USHORT LBA:1; // 1=支持LBA USHORT DisIORDY:1; // 1=可不使用IORDY USHORT IORDY:1; // 1=支持IORDY USHORT SoftReset:1; // 1=需要ATA軟啓動 USHORT Overlap:1; // 1=支持重疊操作 USHORT Queue:1; // 1=支持命令隊列 USHORT InlDMA:1; // 1=支持交叉存取DMA } wCapabilities; // WORD 49: 一般能力 USHORT wReserved1; // WORD 50: 保留 USHORT wPIOTiming; // WORD 51: PIO時序 USHORT wDMATiming; // WORD 52: DMA時序 struct { USHORT CHSNumber:1; // 1=WORD 54-58有效 USHORT CycleNumber:1; // 1=WORD 64-70有效 USHORT UnltraDMA:1; // 1=WORD 88有效 USHORT reserved:13; } wFieldValidity; // WORD 53: 後續字段有效性標誌 USHORT wNumCurCyls; // WORD 54: CHS可尋址的柱面數 USHORT wNumCurHeads; // WORD 55: CHS可尋址的磁頭數 USHORT wNumCurSectorsPerTrack; // WORD 56: CHS可尋址每磁道扇區數 USHORT wCurSectorsLow; // WORD 57: CHS可尋址的扇區數低位字 USHORT wCurSectorsHigh; // WORD 58: CHS可尋址的扇區數高位字 struct { USHORT CurNumber:8; // 當前一次性可讀寫扇區數 USHORT Multi:1; // 1=已選擇多扇區讀寫 USHORT reserved1:7; } wMultSectorStuff; // WORD 59: 多扇區讀寫設定 ULONG dwTotalSectors; // WORD 60-61: LBA可尋址的扇區數 USHORT wSingleWordDMA; // WORD 62: 單字節DMA支持能力 struct { USHORT Mode0:1; // 1=支持模式0 (4.17Mb/s) USHORT Mode1:1; // 1=支持模式1 (13.3Mb/s) USHORT Mode2:1; // 1=支持模式2 (16.7Mb/s) USHORT Reserved1:5; USHORT Mode0Sel:1; // 1=已選擇模式0 USHORT Mode1Sel:1; // 1=已選擇模式1 USHORT Mode2Sel:1; // 1=已選擇模式2 USHORT Reserved2:5; } wMultiWordDMA; // WORD 63: 多字節DMA支持能力 struct { USHORT AdvPOIModes:8; // 支持高級POI模式數 USHORT reserved:8; } wPIOCapacity; // WORD 64: 高級PIO支持能力 USHORT wMinMultiWordDMACycle; // WORD 65: 多字節DMA傳輸週期的最小值 USHORT wRecMultiWordDMACycle; // WORD 66: 多字節DMA傳輸週期的建議值 USHORT wMinPIONoFlowCycle; // WORD 67: 無流控制時PIO傳輸週期的最小值 USHORT wMinPOIFlowCycle; // WORD 68: 有流控制時PIO傳輸週期的最小值 USHORT wReserved69[11]; // WORD 69-79: 保留 struct { USHORT Reserved1:1; USHORT ATA1:1; // 1=支持ATA-1 USHORT ATA2:1; // 1=支持ATA-2 USHORT ATA3:1; // 1=支持ATA-3 USHORT ATA4:1; // 1=支持ATA/ATAPI-4 USHORT ATA5:1; // 1=支持ATA/ATAPI-5 USHORT ATA6:1; // 1=支持ATA/ATAPI-6 USHORT ATA7:1; // 1=支持ATA/ATAPI-7 USHORT ATA8:1; // 1=支持ATA/ATAPI-8 USHORT ATA9:1; // 1=支持ATA/ATAPI-9 USHORT ATA10:1; // 1=支持ATA/ATAPI-10 USHORT ATA11:1; // 1=支持ATA/ATAPI-11 USHORT ATA12:1; // 1=支持ATA/ATAPI-12 USHORT ATA13:1; // 1=支持ATA/ATAPI-13 USHORT ATA14:1; // 1=支持ATA/ATAPI-14 USHORT Reserved2:1; } wMajorVersion; // WORD 80: 主版本 USHORT wMinorVersion; // WORD 81: 副版本 USHORT wReserved82[6]; // WORD 82-87: 保留 struct { USHORT Mode0:1; // 1=支持模式0 (16.7Mb/s) USHORT Mode1:1; // 1=支持模式1 (25Mb/s) USHORT Mode2:1; // 1=支持模式2 (33Mb/s) USHORT Mode3:1; // 1=支持模式3 (44Mb/s) USHORT Mode4:1; // 1=支持模式4 (66Mb/s) USHORT Mode5:1; // 1=支持模式5 (100Mb/s) USHORT Mode6:1; // 1=支持模式6 (133Mb/s) USHORT Mode7:1; // 1=支持模式7 (166Mb/s) ??? USHORT Mode0Sel:1; // 1=已選擇模式0 USHORT Mode1Sel:1; // 1=已選擇模式1 USHORT Mode2Sel:1; // 1=已選擇模式2 USHORT Mode3Sel:1; // 1=已選擇模式3 USHORT Mode4Sel:1; // 1=已選擇模式4 USHORT Mode5Sel:1; // 1=已選擇模式5 USHORT Mode6Sel:1; // 1=已選擇模式6 USHORT Mode7Sel:1; // 1=已選擇模式7 } wUltraDMA; // WORD 88: Ultra DMA支持能力 USHORT wReserved89[167]; // WORD 89-255 } IDINFO, *PIDINFO; // SCSI驅動所需的輸入輸出共用的結構 typedef struct _SRB_IO_CONTROL { ULONG HeaderLength; // 頭長度 UCHAR Signature[8]; // 特徵名稱 ULONG Timeout; // 超時時間 ULONG ControlCode; // 控制碼 ULONG ReturnCode; // 返回碼 ULONG Length; // 緩衝區長度 } SRB_IO_CONTROL, *PSRB_IO_CONTROL; 

需要引起注意的是IDINFO第57-58 WORD (CHS可尋址的扇區數),因爲不滿足32位對齊的要求,不可定義爲一個ULONG字段。Lynn McGuire的程序里正是由於定義爲一個ULONG字段,導致該結構不可用。

以下是核心代碼:

// 打開設備 // filename: 設備的“文件名”(設備路徑) HANDLE OpenDevice(LPCTSTR filename) { HANDLE hDevice; // 打開設備 hDevice = ::CreateFile(filename, // 文件名 GENERIC_READ | GENERIC_WRITE, // 讀寫方式 FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享方式 NULL, // 默認的安全描述符 OPEN_EXISTING, // 創建方式 0, // 不需設置文件屬性 NULL); // 不需參照模板文件 return hDevice; } // 向驅動發“IDENTIFY DEVICE”命令,獲得設備信息 // hDevice: 設備句柄 // pIdInfo: 設備信息結構指針 BOOL IdentifyDevice(HANDLE hDevice, PIDINFO pIdInfo) { PSENDCMDINPARAMS pSCIP; // 輸入數據結構指針 PSENDCMDOUTPARAMS pSCOP; // 輸出數據結構指針 DWORD dwOutBytes; // IOCTL輸出數據長度 BOOL bResult; // IOCTL返回值 // 申請輸入/輸出數據結構空間 pSCIP = (PSENDCMDINPARAMS)::GlobalAlloc(LMEM_ZEROINIT, sizeof(SENDCMDINPARAMS) - 1); pSCOP = (PSENDCMDOUTPARAMS)::GlobalAlloc(LMEM_ZEROINIT, sizeof(SENDCMDOUTPARAMS) + sizeof(IDINFO) - 1); // 指定ATA/ATAPI命令的寄存器值 // pSCIP->irDriveRegs.bFeaturesReg = 0; // pSCIP->irDriveRegs.bSectorCountReg = 0; // pSCIP->irDriveRegs.bSectorNumberReg = 0; // pSCIP->irDriveRegs.bCylLowReg = 0; // pSCIP->irDriveRegs.bCylHighReg = 0; // pSCIP->irDriveRegs.bDriveHeadReg = 0; pSCIP->irDriveRegs.bCommandReg = IDE_ATA_IDENTIFY; // 指定輸入/輸出數據緩衝區大小 pSCIP->cBufferSize = 0; pSCOP->cBufferSize = sizeof(IDINFO); // IDENTIFY DEVICE bResult = ::DeviceIoControl(hDevice, // 設備句柄 DFP_RECEIVE_DRIVE_DATA, // 指定IOCTL pSCIP, sizeof(SENDCMDINPARAMS) - 1, // 輸入數據緩衝區 pSCOP, sizeof(SENDCMDOUTPARAMS) + sizeof(IDINFO) - 1, // 輸出數據緩衝區 &dwOutBytes, // 輸出數據長度 (LPOVERLAPPED)NULL); // 用同步I/O // 複製設備參數結構 ::memcpy(pIdInfo, pSCOP->bBuffer, sizeof(IDINFO)); // 釋放輸入/輸出數據空間 ::GlobalFree(pSCOP); ::GlobalFree(pSCIP); return bResult; } // 向SCSI MINI-PORT驅動發“IDENTIFY DEVICE”命令,獲得設備信息 // hDevice: 設備句柄 // pIdInfo: 設備信息結構指針 BOOL IdentifyDeviceAsScsi(HANDLE hDevice, int nDrive, PIDINFO pIdInfo) { PSENDCMDINPARAMS pSCIP; // 輸入數據結構指針 PSENDCMDOUTPARAMS pSCOP; // 輸出數據結構指針 PSRB_IO_CONTROL pSRBIO; // SCSI輸入輸出數據結構指針 DWORD dwOutBytes; // IOCTL輸出數據長度 BOOL bResult; // IOCTL返回值 // 申請輸入/輸出數據結構空間 pSRBIO = (PSRB_IO_CONTROL)::GlobalAlloc(LMEM_ZEROINIT, sizeof(SRB_IO_CONTROL) + sizeof(SENDCMDOUTPARAMS) + sizeof(IDINFO) - 1); pSCIP = (PSENDCMDINPARAMS)((char *)pSRBIO + sizeof(SRB_IO_CONTROL)); pSCOP = (PSENDCMDOUTPARAMS)((char *)pSRBIO + sizeof(SRB_IO_CONTROL)); // 填充輸入/輸出數據 pSRBIO->HeaderLength = sizeof(SRB_IO_CONTROL); pSRBIO->Timeout = 10000; pSRBIO->Length = sizeof(SENDCMDOUTPARAMS) + sizeof(IDINFO) - 1; pSRBIO->ControlCode = IOCTL_SCSI_MINIPORT_IDENTIFY; ::strncpy ((char *)pSRBIO->Signature, "SCSIDISK", 8); // 指定ATA/ATAPI命令的寄存器值 // pSCIP->irDriveRegs.bFeaturesReg = 0; // pSCIP->irDriveRegs.bSectorCountReg = 0; // pSCIP->irDriveRegs.bSectorNumberReg = 0; // pSCIP->irDriveRegs.bCylLowReg = 0; // pSCIP->irDriveRegs.bCylHighReg = 0; // pSCIP->irDriveRegs.bDriveHeadReg = 0; pSCIP->irDriveRegs.bCommandReg = IDE_ATA_IDENTIFY; pSCIP->bDriveNumber = nDrive; // IDENTIFY DEVICE bResult = ::DeviceIoControl(hDevice, // 設備句柄 IOCTL_SCSI_MINIPORT, // 指定IOCTL pSRBIO, sizeof(SRB_IO_CONTROL) + sizeof(SENDCMDINPARAMS) - 1, // 輸入數據緩衝區 pSRBIO, sizeof(SRB_IO_CONTROL) + sizeof(SENDCMDOUTPARAMS) + sizeof(IDINFO) - 1, // 輸出數據緩衝區 &dwOutBytes, // 輸出數據長度 (LPOVERLAPPED)NULL); // 用同步I/O // 複製設備參數結構 ::memcpy(pIdInfo, pSCOP->bBuffer, sizeof(IDINFO)); // 釋放輸入/輸出數據空間 ::GlobalFree(pSRBIO); return bResult; } // 將串中的字符兩兩顛倒 // 原因是ATA/ATAPI中的WORD,與Windows採用的字節順序相反 // 驅動程序中已經將收到的數據全部反過來,我們來個負負得正 void AdjustString(char* str, int len) { char ch; int i; // 兩兩顛倒 for (i = 0; i < len; i += 2) { ch = str[i]; str[i] = str[i + 1]; str[i + 1] = ch; } // 若是右對齊的,調整爲左對齊 (去掉左邊的空格) i = 0; while ((i < len) && (str[i] == ' ')) i++; ::memmove(str, &str[i], len - i); // 去掉右邊的空格 i = len - 1; while ((i >= 0) && (str[i] == ' ')) { str[i] = '\0'; i--; } } // 讀取IDE硬盤的設備信息,必須有足夠權限 // nDrive: 驅動器號(0=第一個硬盤,1=0=第二個硬盤,......) // pIdInfo: 設備信息結構指針 BOOL GetPhysicalDriveInfoInNT(int nDrive, PIDINFO pIdInfo) { HANDLE hDevice; // 設備句柄 BOOL bResult; // 返回結果 char szFileName[20]; // 文件名 ::sprintf(szFileName,"\\\\.\\PhysicalDrive%d", nDrive); hDevice = ::OpenDevice(szFileName); if (hDevice == INVALID_HANDLE_VALUE) { return FALSE; } // IDENTIFY DEVICE bResult = ::IdentifyDevice(hDevice, pIdInfo); if (bResult) { // 調整字符串 ::AdjustString(pIdInfo->sSerialNumber, 20); ::AdjustString(pIdInfo->sModelNumber, 40); ::AdjustString(pIdInfo->sFirmwareRev, 8); } ::CloseHandle (hDevice); return bResult; } // 用SCSI驅動讀取IDE硬盤的設備信息,不受權限制約 // nDrive: 驅動器號(0=Primary Master, 1=Promary Slave, 2=Secondary master, 3=Secondary slave) // pIdInfo: 設備信息結構指針 BOOL GetIdeDriveAsScsiInfoInNT(int nDrive, PIDINFO pIdInfo) { HANDLE hDevice; // 設備句柄 BOOL bResult; // 返回結果 char szFileName[20]; // 文件名 ::sprintf(szFileName,"\\\\.\\Scsi%d:", nDrive/2); hDevice = ::OpenDevice(szFileName); if (hDevice == INVALID_HANDLE_VALUE) { return FALSE; } // IDENTIFY DEVICE bResult = ::IdentifyDeviceAsScsi(hDevice, nDrive%2, pIdInfo); // 檢查是不是空串 if (pIdInfo->sModelNumber[0] == '\0') { bResult = FALSE; } if (bResult) { // 調整字符串 ::AdjustString(pIdInfo->sSerialNumber, 20); ::AdjustString(pIdInfo->sModelNumber, 40); ::AdjustString(pIdInfo->sFirmwareRev, 8); } return bResult; } 

Q 我注意到ATA/ATAPI裏,以及DiskID32裏,有一個“IDENTIFY PACKET DEVICE”指令,與“IDENTIFY DEVICE”有什麼區別?

A IDENTIFY DEVICE專門用於固定硬盤,而IDENTIFY PACKET DEVICE用於可移動存儲設備如CDROM、CF、MO、ZIP、TAPE等。因爲驅動程序的原因,實際上用本例的方法,不管是IDENTIFY DEVICE也好,IDENTIFY PACKET DEVICE也好,獲取可移動存儲設備的詳細信息,一般是做不到的。而且除了IDE硬盤,對SCSI、USB等接口的硬盤也不起作用。除非廠商提供的驅動支持這樣的功能。

Q ATA/ATAPI有很多指令,如READ SECTORS, WRITE SECTORS, SECURITY, SLEEP, STANDBY等,利用上述方法,是否可進行相應操作?

A 應該沒問題。但切記,要慎重慎重再慎重!

Q 關於權限問題,請解釋一下好嗎?

A 在NT/2000/XP下,administrator可以管理設備,上述兩種訪問驅動的方法都行。但在user身份下,或者登錄到域後,用戶無法訪問PhysicalDrive驅動的核心層,但SCSI MINI-PORT驅動卻可以。目前是可以,不知道Windows以後的版本是否支持。因爲這肯定是一個安全隱患。

另外,我們着重討論NT/2000/XP中DeviceIoControl的應用,如果需要在98/ME中得到包括硬盤序列號在內的更加詳細的信息,請參考DiskID32。

[相關資源]

本文Demo源碼:IdeDiskInfo.zip (25KB)Lynn McGuire的 DiskID32.zip (30KB)T13官方網站:http://www.t13.org

實戰DeviceIoControl 之五:列舉已安裝的存儲設備

Q 前幾次我們討論的都是設備名比較清楚的情況,有了設備名(路徑),就可以直接調用CreateFile打開設備,進行它所支持的I/O操作了。如果事先並不能確切知道設備名,如何去訪問設備呢?

A 訪問設備必須用設備句柄,而得到設備句柄必須知道設備路徑,這個套路以你我之力是改變不了的。每個設備都有它所屬類型的GUID,我們順着這個GUID就能獲得設備路徑。

GUID是同類或同種設備的全球唯一識別碼,它是一個128 bit(16字節)的整形數,真實面目爲

typedef struct _GUID { unsigned long Data1; unsigned short Data2; unsigned short Data3; unsigned char Data4[8]; } GUID, *PGUID; 

例如,Disk類的GUID爲“53f56307-b6bf-11d0-94f2-00a0c91efb8b”,在我們的程序裏可以定義爲

const GUID DiskClassGuid = {0x53f56307L, 0xb6bf, 0x11d0, {0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b)}; 

或者用一個宏來定義

DEFINE_GUID(DiskClassGuid, 0x53f56307L, 0xb6bf, 0x11d0, 0x94, 0xf2, 0x00, 0xa0, 0xc9, 0x1e, 0xfb, 0x8b); 

通過GUID找出設備路徑,需要用到一組設備管理的API函數

SetupDiGetClassDevs, SetupDiEnumDeviceInterfaces, SetupDiGetInterfaceDeviceDetail, SetupDiDestroyDeviceInfoList,

以及結構SP_DEVICE_INTERFACE_DATA, SP_DEVICE_INTERFACE_DETAIL_DATA。

有關信息請查閱MSDN,這裏就不詳細介紹了。

實現GUID到設備路徑的代碼如下:

// SetupDiGetInterfaceDeviceDetail所需要的輸出長度,定義足夠大 #define INTERFACE_DETAIL_SIZE (1024) // 根據GUID獲得設備路徑 // lpGuid: GUID指針 // pszDevicePath: 設備路徑指針的指針 // 返回: 成功得到的設備路徑個數,可能不止1個 int GetDevicePath(LPGUID lpGuid, LPTSTR* pszDevicePath) { HDEVINFO hDevInfoSet; SP_DEVICE_INTERFACE_DATA ifdata; PSP_DEVICE_INTERFACE_DETAIL_DATA pDetail; int nCount; BOOL bResult; // 取得一個該GUID相關的設備信息集句柄 hDevInfoSet = ::SetupDiGetClassDevs(lpGuid, // class GUID  NULL, // 無關鍵字  NULL, // 不指定父窗口句柄  DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); // 目前存在的設備 // 失敗... if (hDevInfoSet == INVALID_HANDLE_VALUE) { return 0; } // 申請設備接口數據空間 pDetail = (PSP_DEVICE_INTERFACE_DETAIL_DATA)::GlobalAlloc(LMEM_ZEROINIT, INTERFACE_DETAIL_SIZE); pDetail->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA); nCount = 0; bResult = TRUE; // 設備序號=0,1,2... 逐一測試設備接口,到失敗爲止 while (bResult) { ifdata.cbSize = sizeof(ifdata); // 枚舉符合該GUID的設備接口 bResult = ::SetupDiEnumDeviceInterfaces( hDevInfoSet, // 設備信息集句柄 NULL, // 不需額外的設備描述 lpGuid, // GUID (ULONG)nCount, // 設備信息集裏的設備序號 &ifdata); // 設備接口信息 if (bResult) { // 取得該設備接口的細節(設備路徑) bResult = SetupDiGetInterfaceDeviceDetail( hDevInfoSet, // 設備信息集句柄 &ifdata, // 設備接口信息 pDetail, // 設備接口細節(設備路徑) INTERFACE_DETAIL_SIZE, // 輸出緩衝區大小 NULL, // 不需計算輸出緩衝區大小(直接用設定值) NULL); // 不需額外的設備描述 if (bResult) { // 複製設備路徑到輸出緩衝區 ::strcpy(pszDevicePath[nCount], pDetail->DevicePath); // 調整計數值 nCount++; } } } // 釋放設備接口數據空間 ::GlobalFree(pDetail); // 關閉設備信息集句柄 ::SetupDiDestroyDeviceInfoList(hDevInfoSet); return nCount; } 

調用GetDevicePath函數時要注意,pszDevicePath是個指向字符串指針的指針,例如可以這樣

 int i; char* szDevicePath[MAX_DEVICE]; // 設備路徑 // 分配需要的空間 for (i = 0; i < MAX_DEVICE; i++) { szDevicePath[i] = new char[256]; } // 取設備路徑 nDevice = ::GetDevicePath((LPGUID)&DiskClassGuid, szDevicePath); // 逐一獲取設備信息 for (i = 0; i < nDevice; i++) { // 打開設備 hDevice = ::OpenDevice(szDevicePath[i]); if (hDevice != INVALID_HANDLE_VALUE) { ... ... // I/O操作 ::CloseHandle(hDevice); } } // 釋放空間 for (i = 0; i & lt; MAX_DEVICE; i++) { delete []szDevicePath[i]; } 

本例的Project中除了要包含winioctl.h外,還要包含initguid.h,setupapi.h,以及連接setupapi.lib。

Q 得到設備路徑後,就可以到下一步,用CreateFile打開設備,然後用DeviceIoControl進行讀寫了吧?

A 是的。儘管該設備路徑與以前我們接觸的那些不太一樣。本是“\\.\PhysicalDrive0”,現在鳥槍換炮,變成了類似這樣的一副尊容:

“\\?\ide#diskmaxtor_2f040j0__________________________vam51jj0#3146563447534558202020202020202020202020#{53f56307-b6bf-11d0-94f2-00a0c91efb8b}”。

其實這個設備名在註冊表的某處可以找到,例如在Win2000中這個名字可以位於

HKEY_LOCAL_MACHINE\System\CurrentControlSet\Services\Disk\Enum\0,

只不過“#”換成了“\”。分析一下這樣的設備路徑,你會發現很有趣的東西,它們是由接口類型、產品型號、固件版本、序列號、計算機名、GUID等信息組合而成的。當然,它是沒有規範的,不能指望從這裏面得到你希望知道的東西。

用CreateFile打開設備後,對於存儲設備,IOCTL_DISK_GET_DRIVE_GEOMETRY,IOCTL_STORAGE_GET_MEDIA_TYPES_EX等I/O控制碼照常使用。

今天我們討論一個新的控制碼:IOCTL_STORAGE_QUERY_PROPERTY,獲取設備屬性信息,希望得到系統中所安裝的各種固定的和可移動的硬盤、優盤和CD/DVD-ROM/R/W的接口類型、序列號、產品ID等信息。

// IOCTL控制碼 #define IOCTL_STORAGE_QUERY_PROPERTY CTL_CODE(IOCTL_STORAGE_BASE, 0x0500, METHOD_BUFFERED, FILE_ANY_ACCESS) // 存儲設備的總線類型 typedef enum _STORAGE_BUS_TYPE { BusTypeUnknown = 0x00, BusTypeScsi, BusTypeAtapi, BusTypeAta, BusType1394, BusTypeSsa, BusTypeFibre, BusTypeUsb, BusTypeRAID, BusTypeMaxReserved = 0x7F } STORAGE_BUS_TYPE, *PSTORAGE_BUS_TYPE; // 查詢存儲設備屬性的類型 typedef enum _STORAGE_QUERY_TYPE { PropertyStandardQuery = 0, // 讀取描述 PropertyExistsQuery, // 測試是否支持 PropertyMaskQuery, // 讀取指定的描述 PropertyQueryMaxDefined // 驗證數據 } STORAGE_QUERY_TYPE, *PSTORAGE_QUERY_TYPE; // 查詢存儲設備還是適配器屬性 typedef enum _STORAGE_PROPERTY_ID { StorageDeviceProperty = 0, // 查詢設備屬性 StorageAdapterProperty // 查詢適配器屬性 } STORAGE_PROPERTY_ID, *PSTORAGE_PROPERTY_ID; // 查詢屬性輸入的數據結構 typedef struct _STORAGE_PROPERTY_QUERY { STORAGE_PROPERTY_ID PropertyId; // 設備/適配器 STORAGE_QUERY_TYPE QueryType; // 查詢類型  UCHAR AdditionalParameters[1]; // 額外的數據(僅定義了象徵性的1個字節) } STORAGE_PROPERTY_QUERY, *PSTORAGE_PROPERTY_QUERY; // 查詢屬性輸出的數據結構 typedef struct _STORAGE_DEVICE_DESCRIPTOR { ULONG Version; // 版本 ULONG Size; // 結構大小 UCHAR DeviceType; // 設備類型 UCHAR DeviceTypeModifier; // SCSI-2額外的設備類型 BOOLEAN RemovableMedia; // 是否可移動 BOOLEAN CommandQueueing; // 是否支持命令隊列 ULONG VendorIdOffset; // 廠家設定值的偏移 ULONG ProductIdOffset; // 產品ID的偏移 ULONG ProductRevisionOffset; // 產品版本的偏移 ULONG SerialNumberOffset; // 序列號的偏移 STORAGE_BUS_TYPE BusType; // 總線類型 ULONG RawPropertiesLength; // 額外的屬性數據長度 UCHAR RawDeviceProperties[1]; // 額外的屬性數據(僅定義了象徵性的1個字節) } STORAGE_DEVICE_DESCRIPTOR, *PSTORAGE_DEVICE_DESCRIPTOR; // 取設備屬性信息 // hDevice -- 設備句柄 // pDevDesc -- 輸出的設備描述和屬性信息緩衝區指針(包含連接在一起的兩部分) BOOL GetDriveProperty(HANDLE hDevice, PSTORAGE_DEVICE_DESCRIPTOR pDevDesc) { STORAGE_PROPERTY_QUERY Query; // 查詢輸入參數 DWORD dwOutBytes; // IOCTL輸出數據長度 BOOL bResult; // IOCTL返回值 // 指定查詢方式 Query.PropertyId = StorageDeviceProperty; Query.QueryType = PropertyStandardQuery; // 用IOCTL_STORAGE_QUERY_PROPERTY取設備屬性信息 bResult = ::DeviceIoControl(hDevice, // 設備句柄 IOCTL_STORAGE_QUERY_PROPERTY, // 取設備屬性信息 &Query, sizeof(STORAGE_PROPERTY_QUERY), // 輸入數據緩衝區 pDevDesc, pDevDesc->Size, // 輸出數據緩衝區 &dwOutBytes, // 輸出數據長度 (LPOVERLAPPED)NULL); // 用同步I/O  return bResult; } 

Q 我用這個方法從IOCTL_STORAGE_QUERY_PROPERTY返回的數據中,沒有得到CDROM和USB接口的外置硬盤的序列號、產品ID等信息。但從設備路徑上看,明明是有這些信息的,爲什麼它沒有填充到STORAGE_DEVICE_DESCRIPTOR中呢?再就是爲什麼硬盤序列號本是“D22P7KHE            ”,爲什麼它填充的是“3146563447534558202020202020202020202020”這種形式呢?

A 對這兩個問題我也是心存疑惑,但又不敢妄加猜測,正琢磨着向微軟請教呢。

[相關資源]

本文Demo源碼:StorageEnum.zip (23KB)

實戰DeviceIoControl 之六:訪問物理端口

Q 在NT/2000/XP中,如何讀取CMOS數據?

Q 在NT/2000/XP中,如何控制speaker發聲?

Q 在NT/2000/XP中,如何直接訪問物理端口?

A 看似小小問題,難倒多少好漢!

NT/2000/XP從安全性、可靠性、穩定性上考慮,應用程序和操作系統是分開的,操作系統代碼運行在覈心態,有權訪問系統數據和硬件,能執行特權指令;應用程序運行在用戶態,能夠使用的接口和訪問系統數據的權限都受到嚴格限制。當用戶程序調用系統服務時,處理器捕獲該調用,然後把調用的線程切換到核心態。當系統服務完成後,操作系統將線程描述表切換回用戶態,調用者繼續運行。

想在用戶態應用程序中實現I/O讀寫,直接存取硬件,可以通過編寫驅動程序,實現CreateFile、CloseHandle、 DeviceIOControl、ReadFile、WriteFile等功能。從Windows 2000開始,引入WDM核心態驅動程序的概念。

下面是本人寫的一個非常簡單的驅動程序,可實現字節型端口I/O。

#include <ntddk.h> #include "MyPort.h" // 設備類型定義 // 0-32767被Microsoft佔用,用戶自定義可用32768-65535 #define FILE_DEVICE_MYPORT 0x0000f000 // I/O控制碼定義 // 0-2047被Microsoft佔用,用戶自定義可用2048-4095  #define MYPORT_IOCTL_BASE 0xf00 #define IOCTL_MYPORT_READ_BYTE CTL_CODE(FILE_DEVICE_MYPORT, MYPORT_IOCTL_BASE, METHOD_BUFFERED, FILE_ANY_ACCESS) #define IOCTL_MYPORT_WRITE_BYTE CTL_CODE(FILE_DEVICE_MYPORT, MYPORT_IOCTL_BASE+1, METHOD_BUFFERED, FILE_ANY_ACCESS) // IOPM是65536個端口的位屏蔽矩陣,包含8192字節(8192 x 8 = 65536) // 0 bit: 允許應用程序訪問對應端口 // 1 bit: 禁止應用程序訪問對應端口 #define IOPM_SIZE 8192 typedef UCHAR IOPM[IOPM_SIZE]; IOPM *pIOPM = NULL; // 設備名(要求以UNICODE表示) const WCHAR NameBuffer[] = L"\\Device\\MyPort"; const WCHAR DOSNameBuffer[] = L"\\DosDevices\\MyPort"; // 這是兩個在ntoskrnl.exe中的未見文檔的服務例程 // 沒有現成的已經說明它們原型的頭文件,我們自己聲明 void Ke386SetIoAccessMap(int, IOPM *); void Ke386IoSetAccessProcess(PEPROCESS, int); // 函數原型預先說明 NTSTATUS MyPortDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp); void MyPortUnload(IN PDRIVER_OBJECT DriverObject); // 驅動程序入口,由系統自動調用,就像WIN32應用程序的WinMain NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath) { PDEVICE_OBJECT deviceObject; NTSTATUS status; UNICODE_STRING uniNameString, uniDOSString; // 爲IOPM分配內存 pIOPM = MmAllocateNonCachedMemory(sizeof(IOPM)); if (pIOPM == 0) { return STATUS_INSUFFICIENT_RESOURCES; } // IOPM全部初始化爲0(允許訪問所有端口) RtlZeroMemory(pIOPM, sizeof(IOPM)); // 將IOPM加載到當前進程 Ke386IoSetAccessProcess(PsGetCurrentProcess(), 1); Ke386SetIoAccessMap(1, pIOPM); // 指定驅動名字 RtlInitUnicodeString(&uniNameString, NameBuffer); RtlInitUnicodeString(&uniDOSString, DOSNameBuffer); // 創建設備 status = IoCreateDevice(DriverObject, 0, &uniNameString, FILE_DEVICE_MYPORT, 0, FALSE, &deviceObject); if (!NT_SUCCESS(status)) { return status; } // 創建WIN32應用程序需要的符號連接 status = IoCreateSymbolicLink (&uniDOSString, &uniNameString); if (!NT_SUCCESS(status)) { return status; } // 指定驅動程序有關操作的模塊入口(函數指針) // 涉及以下兩個模塊:MyPortDispatch和MyPortUnload DriverObject->MajorFunction[IRP_MJ_CREATE] = DriverObject->MajorFunction[IRP_MJ_CLOSE] = DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyPortDispatch; DriverObject->DriverUnload = MyPortUnload; return STATUS_SUCCESS; } // IRP處理模塊 NTSTATUS MyPortDispatch(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp) { PIO_STACK_LOCATION IrpStack; ULONG dwInputBufferLength; ULONG dwOutputBufferLength; ULONG dwIoControlCode; PULONG pvIOBuffer; NTSTATUS ntStatus; // 填充幾個默認值 Irp->IoStatus.Status = STATUS_SUCCESS; // 返回狀態 Irp->IoStatus.Information = 0; // 輸出長度 IrpStack = IoGetCurrentIrpStackLocation(Irp); // Get the pointer to the input/output buffer and it's length // 輸入輸出共用的緩衝區 // 因爲我們在IOCTL中指定了METHOD_BUFFERED, pvIOBuffer = Irp->AssociatedIrp.SystemBuffer; switch (IrpStack->MajorFunction) { case IRP_MJ_CREATE: // 與WIN32應用程序中的CreateFile對應 break; case IRP_MJ_CLOSE: // 與WIN32應用程序中的CloseHandle對應 break; case IRP_MJ_DEVICE_CONTROL: // 與WIN32應用程序中的DeviceIoControl對應 dwIoControlCode = IrpStack->Parameters.DeviceIoControl.IoControlCode; switch (dwIoControlCode) { // 我們約定,緩衝區共兩個DWORD,第一個DWORD爲端口,第二個DWORD爲數據 // 一般做法是專門定義一個結構,此處簡單化處理了 case IOCTL_MYPORT_READ_BYTE: // 從端口讀字節 pvIOBuffer[1] = _inp(pvIOBuffer[0]); Irp->IoStatus.Information = 8; // 輸出長度爲8 break; case IOCTL_MYPORT_WRITE_BYTE: // 寫字節到端口 _outp(pvIOBuffer[0], pvIOBuffer[1]); break; default: // 不支持的IOCTL Irp->IoStatus.Status = STATUS_INVALID_PARAMETER; } } ntStatus = Irp->IoStatus.Status; IoCompleteRequest (Irp, IO_NO_INCREMENT); return ntStatus; } // 刪除驅動 void MyPortUnload(IN PDRIVER_OBJECT DriverObject) { UNICODE_STRING uniDOSString; if(pIOPM) { // 釋放IOPM佔用的空間 MmFreeNonCachedMemory(pIOPM, sizeof(IOPM)); } RtlInitUnicodeString(&uniDOSString, DOSNameBuffer); // 刪除符號連接和設備 IoDeleteSymbolicLink (&uniDOSString); IoDeleteDevice(DriverObject->DeviceObject); } 

下面給出實現設備驅動程序的動態加載的源碼。動態加載的好處是,你不用做任何添加新硬件的操作,也不用編輯註冊表,更不用重新啓動計算機。

// 安裝驅動並啓動服務 // lpszDriverPath: 驅動程序路徑 // lpszServiceName: 服務名  BOOL StartDriver(LPCTSTR lpszDriverPath, LPCTSTR lpszServiceName) { SC_HANDLE hSCManager; // 服務控制管理器句柄 SC_HANDLE hService; // 服務句柄 DWORD dwLastError; // 錯誤碼 BOOL bResult = FALSE; // 返回值 // 打開服務控制管理器 hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); if (hSCManager) { // 創建服務 hService = CreateService(hSCManager, lpszServiceName, lpszServiceName, SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER, SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, lpszDriverPath, NULL, NULL, NULL, NULL, NULL); if (hService == NULL) { if (::GetLastError() == ERROR_SERVICE_EXISTS) { hService = ::OpenService(hSCManager, lpszServiceName, SERVICE_ALL_ACCESS); } } if (hService) { // 啓動服務 bResult = StartService(hService, 0, NULL); // 關閉服務句柄 CloseServiceHandle(hService); } // 關閉服務控制管理器句柄 CloseServiceHandle(hSCManager); } return bResult; } // 停止服務並卸下驅動 // lpszServiceName: 服務名  BOOL StopDriver(LPCTSTR lpszServiceName) { SC_HANDLE hSCManager; // 服務控制管理器句柄 SC_HANDLE hService; // 服務句柄 BOOL bResult; // 返回值 SERVICE_STATUS ServiceStatus; bResult = FALSE; // 打開服務控制管理器 hSCManager = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS); if (hSCManager) { // 打開服務 hService = OpenService(hSCManager, lpszServiceName, SERVICE_ALL_ACCESS); if (hService) { // 停止服務 bResult = ControlService(hService, SERVICE_CONTROL_STOP, &ServiceStatus); // 刪除服務 bResult = bResult && DeleteService(hService); // 關閉服務句柄 CloseServiceHandle(hService); } // 關閉服務控制管理器句柄 CloseServiceHandle(hSCManager); } return bResult; } 

應用程序實現端口I/O的接口如下:

// 全局的設備句柄 HANDLE hMyPort; // 打開設備 // lpszDevicePath: 設備的路徑 HANDLE OpenDevice(LPCTSTR lpszDevicePath) { HANDLE hDevice; // 打開設備 hDevice = ::CreateFile(lpszDevicePath, // 設備路徑 GENERIC_READ | GENERIC_WRITE, // 讀寫方式 FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享方式 NULL, // 默認的安全描述符 OPEN_EXISTING, // 創建方式 0, // 不需設置文件屬性 NULL); // 不需參照模板文件 return hDevice; } // 打開端口驅動 BOOL OpenMyPort() { BOOL bResult; // 設備名爲"MyPort",驅動程序位於Windows的"system32\drivers"目錄中 bResult = StartDriver("system32\\drivers\\MyPort.sys", "MyPort"); // 設備路徑爲"\\.\MyPort" if (bResult) { hMyPort = OpenDevice("\\\\.\\MyPort"); } return (bResult && (hMyPort != INVALID_HANDLE_VALUE)); } // 關閉端口驅動 BOOL CloseMyPort() { return (CloseHandle(hMyPort) && StopDriver("MyPort")); } // 從指定端口讀一個字節 // port: 端口 BYTE ReadPortByte(WORD port) { DWORD buf[2]; // 輸入輸出緩衝區  DWORD dwOutBytes; // IOCTL輸出數據長度 buf[0] = port; // 第一個DWORD是端口 // buf[1] = 0; // 第二個DWORD是數據 // 用IOCTL_MYPORT_READ_BYTE讀端口 ::DeviceIoControl(hMyPort, // 設備句柄 IOCTL_MYPORT_READ_BYTE, // 取設備屬性信息 buf, sizeof(buf), // 輸入數據緩衝區 buf, sizeof(buf), // 輸出數據緩衝區 &dwOutBytes, // 輸出數據長度 (LPOVERLAPPED)NULL); // 用同步I/O  return (BYTE)buf[1]; } // 將一個字節寫到指定端口 // port: 端口 // data: 字節數據 void WritePortByte(WORD port, BYTE data) { DWORD buf[2]; // 輸入輸出緩衝區  DWORD dwOutBytes; // IOCTL輸出數據長度 buf[0] = port; // 第一個DWORD是端口 buf[1] = data; // 第二個DWORD是數據 // 用IOCTL_MYPORT_WRITE_BYTE寫端口 ::DeviceIoControl(hMyPort, // 設備句柄 IOCTL_MYPORT_WRITE_BYTE, // 取設備屬性信息 buf, sizeof(buf), // 輸入數據緩衝區 buf, sizeof(buf), // 輸出數據緩衝區 &dwOutBytes, // 輸出數據長度 (LPOVERLAPPED)NULL); // 用同步I/O } 

有了ReadPortByte和WritePortByte這兩個函數,我們就能很容易地操縱CMOS和speaker了(關於CMOS值的含義以及定時器寄存器定義,請參考相應的硬件資料):

// 0x70是CMOS索引端口(只寫) // 0x71是CMOS數據端口 BYTE ReadCmos(BYTE index) { BYTE data; ::WritePortByte(0x70, index); data = ::ReadPortByte(0x71); return data; } // 0x61是speaker控制端口 // 0x43是8253/8254定時器控制端口 // 0x42是8253/8254定時器通道2的端口 void Sound(DWORD freq) { BYTE data; if ((freq >= 20) && (freq <= 20000)) { freq = 1193181 / freq; data = ::ReadPortByte(0x61); if ((data & 3) == 0) { ::WritePortByte(0x61, data | 3); ::WritePortByte(0x43, 0xb6); } ::WritePortByte(0x42, (BYTE)(freq % 256)); ::WritePortByte(0x42, (BYTE)(freq / 256)); } } void NoSound(void) { BYTE data; data = ::ReadPortByte(0x61); ::WritePortByte(0x61, data & 0xfc); } 
 // 以下讀出CMOS 128個字節 for (int i = 0; i < 128; i++) { BYTE data = ::ReadCmos(i); ... ... } // 以下用C調演奏“多-來-米” // 1 = 262 Hz ::Sound(262); ::Sleep(200); ::NoSound(); // 2 = 288 Hz ::Sound(288); ::Sleep(200); ::NoSound(); // 3 = 320 Hz ::Sound(320); ::Sleep(200); ::NoSound(); 

Q 就是個簡單的端口I/O,這麼麻煩才能實現,搞得俺頭腦稀昏,有沒有簡潔明瞭的辦法啊?

A 上面的例子,之所以從編寫驅動程序,到安裝驅動,到啓動服務,到打開設備,到訪問設備,一直到讀寫端口,這樣一路下來,是爲了揭示在NT/2000/XP中硬件訪問技術的本質。假如將所有過程封裝起來,只提供OpenMyPort, CloseMyPort, ReadPortByte, WritePortByte甚至更高層的ReadCmos、WriteCmos、Sound、NoSound給你調用,是不是會感覺清爽許多?

實際上,我們平常做的基於一定硬件的二次開發,一般會先安裝驅動程序(DRV)和用戶接口的運行庫(DLL),然後在此基礎上開發出我們的應用程序(APP)。DRV、DLL、APP三者分別運行在覈心態、核心態/用戶態聯絡帶、用戶態。比如買了一塊圖象採集卡,要先安裝核心驅動,它的“Development Tool Kit”,提供類似於PCV_Initialize, PCV_Capture等的API,就是扮演核心態和用戶態聯絡員的角色。我們根本不需要CreateFile、CloseHandle、 DeviceIOControl、ReadFile、WriteFile等較低層次的直接調用。

Yariv Kaplan寫過一個WinIO的例子,能實現對物理端口和內存的訪問,提供了DRV、DLL、APP三方面的源碼,有興趣的話可以深入研究一下。

[相關資源]

本文驅動程序源碼:MyPort.zip (3KB, 編譯環境: VC6+2000DDK)本文應用程序源碼:MyPortIo.zip (22KB, 文件MyPort.sys需複製到windows的system32\drivers目錄中)Yariv Kaplan的主頁:http://www.internals.com

實戰DeviceIoControl 之七:在Windows 9X中讀寫磁盤扇區

在Windows NT/2K/XP中,直接用CreateFile打開名稱類似於"\\.\A:"的”文件”,就可以與設備驅動打交道,通過ReadFile/WriteFile以絕對地址方式訪問磁盤了。但Windows 9X不支持這樣的簡單方法。本文介紹一種在Windows 9X中實現磁盤直接訪問的方法:利用系統的vwin32.vxd,通過DeviceIoControl調用DOS INT21 7305H與440DH功能來完成。該調用支持FAT12、FAT16和FAT32,適用於Windows 95 SR2以及更高版本。

先來了解一下DOS INT21 7305H功能的入口參數:

AX -- 功能號7305H DS:BX -- 讀寫扇區的信息結構 CX -- 必須爲-1 DL -- 驅動器號: 1=A:, 2=B:, 3=C:, ... SI -- 讀寫標誌: 最低位0=讀, 1=寫 

若調用成功,清除C標誌;否則設置C標誌。

DS:BX指向一個結構,此結構定義如下:

DISKIO STRUC dwStartSector dd ? ; 起始扇區 wSector dw ? ; 扇區數 lpBuffer dd ? ; 數據緩衝區地址 DISKIO ENDS 

在寫操作下,需要“鎖定”驅動器。DOS INT21 440DH的4AH/6AH功能可實現邏輯驅動器的加鎖/解鎖。其入口參數爲:

AX -- 功能號440DH BH -- 鎖的級別,0-3級,直接寫扇區用1 BL -- 驅動器號: 1=A:, 2=B:, 3=C:, ... CH -- 0x08 CL -- 0x4A DX -- 0 
AX -- 功能號440DH BL -- 驅動器號: 1=A:, 2=B:, 3=C:, ... CH -- 0x08 CL -- 0x6A 

以上兩個調用,若調用成功,清除C標誌;否則設置C標誌。

通過IOCTL碼VWIN32_DIOC_DOS_DRIVEINFO等調用上述中斷。實現絕對磁盤讀寫的關鍵代碼如下:

// INT21的IOCTL碼 #define VWIN32_DIOC_DOS_IOCTL 1 #define VWIN32_DIOC_DOS_DRIVEINFO 6 // 寄存器組 typedef struct _DIOC_REGISTERS { DWORD reg_EBX; DWORD reg_EDX; DWORD reg_ECX; DWORD reg_EAX; DWORD reg_EDI; DWORD reg_ESI; DWORD reg_Flags; } DIOC_REGISTERS, *PDIOC_REGISTERS; // IO參數(注意字節對齊方式) #pragma pack(1) typedef struct _DISKIO { DWORD dwStartSector; // 起始扇區 WORD wSectors; // 扇區數 void* pBuffer; // 緩衝區指針 } DISKIO, *PDISKIO; #pragma pack() BOOL AbsDiskRead( BYTE nDiskNumber, // 盤號, 1=A:, 2=B:, 3= C:, ...  DWORD dwStartSector, // 起始扇區 WORD wSectors, // 扇區數 void* pBuffer) // 數據緩衝區指針 { HANDLE hDevice; DIOC_REGISTERS regs; DISKIO dio; DWORD dwOutBytes; BOOL bResult; // 打開設備,獲得VxD句柄 hDevice = CreateFile("\\\\.\\vwin32", // 設備路徑 GENERIC_READ | GENERIC_WRITE, // 讀寫方式 FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享方式 NULL, // 默認的安全描述符 OPEN_EXISTING, // 創建方式 FILE_ATTRIBUTE_NORMAL, // 文件屬性 NULL); // 不需參照模板文件 if(hDevice == INVALID_HANDLE_VALUE) { return FALSE; } // 填充DISKIO參數結構 dio.dwStartSector = dwStartSector; dio.wSectors = wSectors; dio.pBuffer = pBuffer; // 填充寄存器組--中斷入口參數  memset(&regs, 0, sizeof(DIOC_REGISTERS)); regs.reg_EAX = 0x7305; // AX=0x7305 regs.reg_EBX = (DWORD)&dio; // EBX=DS:BX=參數指針 regs.reg_ECX = 0xffff; // CX=-1 regs.reg_EDX = nDiskNumber; // DL=盤號 regs.reg_ESI = 0; // SI=0 -- 讀操作 // 用VWIN32_DIOC_DOS_DRIVEINFO讀磁盤 dwOutBytes = 0; bResult = DeviceIoControl(hDevice, // 設備句柄 VWIN32_DIOC_DOS_DRIVEINFO, // INT21 &regs, sizeof(regs), // 輸出數據緩衝區與長度 &regs, sizeof(regs), // 輸出數據緩衝區與長度 &dwOutBytes, // 輸出數據長度 NULL); // 用同步I/O // 確定DeviceIoControl與INT21都無錯誤  bResult = bResult && !(regs.reg_Flags & 1); CloseHandle(hDevice); return bResult; } BOOL AbsDiskWrite( BYTE nDiskNumber, // 盤號, 1=A:, 2=B:, 3= C:, ...  DWORD dwStartSector, // 起始扇區 WORD wSectors, // 扇區數 void* pBuffer) // 數據緩衝區指針 { HANDLE hDevice; DIOC_REGISTERS regs; DISKIO dio; DWORD dwOutBytes; BOOL bResult; // 打開設備,獲得VxD句柄 hDevice = CreateFile("\\\\.\\vwin32", // 設備路徑 GENERIC_READ | GENERIC_WRITE, // 讀寫方式 FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享方式 NULL, // 默認的安全描述符 OPEN_EXISTING, // 創建方式 FILE_ATTRIBUTE_NORMAL, // 文件屬性 NULL); // 不需參照模板文件 if(hDevice == INVALID_HANDLE_VALUE) { return FALSE; } // 填充DISKIO參數結構 dio.dwStartSector = dwStartSector; dio.wSectors = wSectors; dio.pBuffer = pBuffer; // 填充寄存器組--中斷入口參數  memset(&regs, 0, sizeof(DIOC_REGISTERS)); regs.reg_EAX = 0x7305; // AX=0x7305 regs.reg_EBX = (DWORD)&dio; // EBX=DS:BX=參數指針 regs.reg_ECX = 0xffff; // CX=-1 regs.reg_EDX = nDiskNumber; // DL=盤號 regs.reg_ESI = 0x6001; // SI=0x6001 -- 普通寫操作 // 用VWIN32_DIOC_DOS_DRIVEINFO寫磁盤 dwOutBytes = 0; bResult = DeviceIoControl(hDevice, // 設備句柄 VWIN32_DIOC_DOS_DRIVEINFO, // INT21 &regs, sizeof(regs), // 輸出數據緩衝區與長度 &regs, sizeof(regs), // 輸出數據緩衝區與長度 &dwOutBytes, // 輸出數據長度 NULL); // 用同步I/O // 確定DeviceIoControl與INT21都無錯誤  bResult = bResult && !(regs.reg_Flags & 1); CloseHandle(hDevice); return bResult; } BOOL LockVolume( BYTE nDiskNumber) // 盤號, 1=A:, 2=B:, 3=C:, ...  { HANDLE hDevice; DIOC_REGISTERS regs; DWORD dwOutBytes; BOOL bResult; // 打開設備,獲得VxD句柄 hDevice = CreateFile("\\\\.\\vwin32", // 設備路徑 GENERIC_READ | GENERIC_WRITE, // 讀寫方式 FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享方式 NULL, // 默認的安全描述符 OPEN_EXISTING, // 創建方式 FILE_ATTRIBUTE_NORMAL, // 文件屬性 NULL); // 不需參照模板文件 if(hDevice == INVALID_HANDLE_VALUE) { return FALSE; } // 填充寄存器組--中斷入口參數  memset(&regs, 0, sizeof(DIOC_REGISTERS)); regs.reg_EAX = 0x440D; // AX=0x440D regs.reg_EBX = 0x0100 | nDiskNumber; // BH=鎖的級別,BL=盤號 regs.reg_ECX = 0x084A; regs.reg_EDX = 0; // 用VWIN32_DIOC_DOS_DRIVEINFO讀磁盤 dwOutBytes = 0; bResult = DeviceIoControl(hDevice, // 設備句柄 VWIN32_DIOC_DOS_IOCTL, // INT21 &regs, sizeof(regs), // 輸入數據緩衝區與長度 &regs, sizeof(regs), // 輸出數據緩衝區與長度 &dwOutBytes, // 輸出數據長度 NULL); // 用同步I/O // 確定DeviceIoControl與INT21都無錯誤  bResult = bResult && !(regs.reg_Flags & 1); CloseHandle(hDevice); return bResult; } BOOL UnlockVolume( BYTE nDiskNumber) // 盤號, 1=A:, 2=B:, 3=C:, ...  { HANDLE hDevice; DIOC_REGISTERS regs; DWORD dwOutBytes; BOOL bResult; // 打開設備,獲得VxD句柄 hDevice = CreateFile("\\\\.\\vwin32", // 設備路徑 GENERIC_READ | GENERIC_WRITE, // 讀寫方式 FILE_SHARE_READ | FILE_SHARE_WRITE, // 共享方式 NULL, // 默認的安全描述符 OPEN_EXISTING, // 創建方式 FILE_ATTRIBUTE_NORMAL, // 文件屬性 NULL); // 不需參照模板文件 if(hDevice == INVALID_HANDLE_VALUE) { return FALSE; } // 填充寄存器組--中斷入口參數  memset(&regs, 0, sizeof(DIOC_REGISTERS)); regs.reg_EAX = 0x440D; // AX=0x440D regs.reg_EBX = nDiskNumber; // BL=盤號 regs.reg_ECX = 0x086A; // 用VWIN32_DIOC_DOS_DRIVEINFO讀磁盤 dwOutBytes = 0; bResult = DeviceIoControl(hDevice, // 設備句柄 VWIN32_DIOC_DOS_IOCTL, // INT21 &regs, sizeof(regs), // 輸入數據緩衝區與長度 &regs, sizeof(regs), // 輸出數據緩衝區與長度 &dwOutBytes, // 輸出數據長度 NULL); // 用同步I/O // 確定DeviceIoControl與INT21都無錯誤  bResult = bResult && !(regs.reg_Flags & 1); CloseHandle(hDevice); return bResult; } 

下面的例子,從A盤的0扇區開始,讀取10個扇區的數據,並保存在文件中:

 unsigned char buf[512 * 10]; if (AbsDiskRead(1, 0, 10, buf)) { FILE* fp = fopen("a.dat", "w+b"); fwrite(buf, 512, 10, fp); fclose(fp); } 

下面的例子,讀取D驅動器的第8888扇區,然後寫回去:

 unsigned char buf[512]; LockVolume(4); if (AbsDiskRead(4, 8888, 1, buf)) { ... ... if (AbsDiskWrite(4, 8888, 1, buf)) { ... ... } } UnlockVolume(4); 

在寫方式下,SI寄存器的位0設置爲1,位15-13在磁盤的不同區域需要有不同的值:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章