附錄六 實體磁碟與邏輯磁碟


簡介

眾所皆知,一個實體磁碟可以再由分割程式最多分割成三個主要分割和一個延伸分割。您可以在 Windows XP 作業系統中,把滑鼠游標移到「我的電腦」上,再按下滑鼠右鍵,選「管理」,然後在彈出的視窗左半部,點選「磁碟管理」,視窗右半部就會出現下圖的畫面,其實下圖是小木偶的桌上型電腦的硬碟資料。

讀者可以看得出來,小木偶共有三個實體磁碟 ( physical disk),以紅框標出,分別是:磁碟 0、磁碟 1、磁碟 2。磁碟 0 又被分割成三個主要分割與一個延伸分割。主要分割上半部以深藍色表示,磁碟標籤 ( volumn ) 分別是「MS-DOS_6_20」、「XP_PRO」、「XP64_PRO」。延伸分割以綠框框住,又被分割為三個邏輯磁碟,每個邏輯磁碟上半部以藍色表示,其中一個的磁碟標籤是「DATA_1」,另外兩個被 Windows XP 認為是「不明的磁碟分割」,因為這兩個分割是「Linux」作業系統所佔據。

每個分割均是為了儲存資料、檔案而劃分,而每種作業系統在資料、檔案的安排方式有所不同,這種在磁碟中安排存放資料或檔案的方式稱為檔案系統 ( file system ),例如 DOS 以 FAT12、FAT16 方式安排並存取檔案 ( 有關 FAT12、FAT16 請參考組合語言的軟碟片硬碟(2)FAT );Windows 9x/Me 可以使用 FAT12、FAT16 和 FAT32 三種檔案系統;Windows NT/XP 除了上面所說的 FAT12、FAT16、FAT32 之外,還可使用 NTFS;Linux 則常使用 EXT2、EXT3 檔案系統。

在 Windows 作業系統堙A每個主要分割和延伸分割堛瘍瓡頨牬迣ㄦ|被賦予一個磁碟機代號,例如:「C:」、「D:」……等等,當然如果是 Windows 不認得的分割或是 Windows 不認得的檔案系統,就不會被賦予磁碟機代號。像「C:」、「D:」……等這些磁碟機代號所代表的磁碟,就叫做「邏輯磁碟」( logical driver )。而在 XP 以及更後期的 Windows 作業系統的邏輯磁碟,又有另一層不同的意義,那就是動態磁碟。XP 可以把數個分割合併變成一個邏輯磁碟,而僅用一個磁碟機代號表示。每個分割均為連續的磁區,在 MSDN 中稱為「extent」,數個「extent」可合併為一個邏輯磁碟,這些「extent」可能分布在不同實體磁碟,也可以不是連續的磁區 ( 當然同一個「extent」的磁區是連續的 )。

由上面的分析可知,一個實體磁碟可以有一個以上的邏輯磁碟,而本章所要討論的是列出實體磁碟所包含的邏輯磁碟。


原理

在這一章堙A小木偶將發展出一個副程式,GetLogicalDriveFromPhysicalDrive,能把電腦上的每一個實體磁碟所包含的邏輯磁碟顯示出來。

在 Win32 API 堙A有個叫做 DeviceIoControl 的 API 可以輸入邏輯磁碟代號而傳回此邏輯磁碟是位於哪個實體磁碟上;但是似乎是沒有傳入實體磁碟,而得到該實體磁碟包含哪些邏輯磁碟。小木偶的想法很簡單,那就是利用 Win32 另一個 API,GetLogicalDriveStrings,找出所有的邏輯磁碟機代號,再把這些邏輯磁碟代號一一代入 DeviceIoControl 堙A就可以得到每個邏輯磁碟屬於哪一個實體磁碟,再於一塊記憶體中,依據實體磁碟編號分門別類把此邏輯磁碟存入。先說一說 GetLogicalDriveFromPhysicalDrive 會呼叫的 Win32 API。

GetLogicalDriveStrings API

GetLogicalDriveStrings 可以把電腦上的所有邏輯磁碟,包含軟碟機、硬碟機、光碟機、隨身碟等等,存入到指定的位址堙C它的原型是:

        INVOKE  GetLogicalDriveStrings,nBufferLength,lpBuffer

先說第二個參數,lpBuffer,是一個位址指標,指向一個緩衝區域,GetLogicalDriveStrings 將會把所有的邏輯磁碟代號填入此位址所指的緩衝區堙C第一個參數,nBufferLength,是緩衝區的大小。這兩個參數必須在呼叫 GetLogicalDriveStrings 前就已設定好。如果成功的取得邏輯磁碟代號,則 EAX 為邏輯磁碟代號的總長度,但不包含最後的字元,NULL;如果所 nBufferLength 所指定的緩衝區不夠大,EAX 會傳回所需緩衝區大小,此大小卻又包含 NULL。如果 GetLogicalDriveStrings 無法正常運作,則 EAX 傳回 FALSE。

例如小木偶的電腦在執行完「INVOKE GetLogicalDriveStrings,104,12f76ch」後,緩衝區 12F76Ch 的內容變為:

0012F760                                      43 3A 5C 00              C:\.
0012F770  44 3A 5C 00 45 3A 5C 00 46 3A 5C 00 47 3A 5C 00  D:\.E:\.F:\.G:\.
0012F780  48 3A 5C 00 49 3A 5C 00 4A 3A 5C 00 4B 3A 5C 00  H:\.I:\.J:\.K:\.
0012F790  4D 3A 5C 00 4E 3A 5C 00 00                       M:\.N:\..

而返回值 EAX 變為 2Ch。

GetDriveType API

這個 API 決定某個邏輯磁碟是屬於哪一種類型的儲存裝置:removable, fixed, CD-ROM, RAM disk, or network drive。其原型為:

        INVOKE  GetDriveType,lpRootPathName

lpRootPathName 是指向邏輯磁碟根目錄字串的位址,以 NULL 結尾,例如「"C:\",0」。反斜線「\」是必要的。在呼叫 GetDriveType 之前,必須先設定好 lpRootPathName,如果 lpRootPathName 為 0,那麼 GetDriveType 會傳回目前邏輯磁碟的儲存裝置類型。返回值 EAX 可能是下面幾項,右欄代表其意義:

返回值數值意  義
DRIVE_UNKNOWN0無法識別的裝置
DRIVE_NO_ROOT_DIR1無此根目錄,例如輸入的邏輯磁碟錯誤,以致無此根目錄
DRIVE_REMOVABLE2可卸除的儲存裝置,如軟碟、USB 隨身碟 ( thumb drive )、讀卡機 ( flash card reader ) 等等
DRIVE_FIXED3硬碟或快閃碟 ( flash drive )
DRIVE_REMOTE4網路磁碟機
DRIVE_CDROM5光碟機
DRIVE_RAMDISK6RAM disk

CreateFile API

第十一章曾經提到 CreateFile,那時是以它開啟檔案,原型為:

CreateFile(
 LPCTSTR                lpFileName,
 DWORD                  dwDesiredAccess,
 DWORD                  dwShareMode,
 LPSECURITY_ATTRIBUTES  lpSecurityAttributes,
 DWORD                  dwCreationDisposition,  // how to create
 DWORD                  dwFlagsAndAttributes,   // file attributes
 HANDLE                 hTemplateFile
);

但 CreateFile 的功能不僅僅是這樣,它還可以開啟實體磁碟與邏輯磁碟 ( 在 MSDN 堙A是說開啟實體磁碟與磁碟標籤,原文是「physical disks and volumes」,但是磁碟標籤指的應該就是邏輯磁碟 )。一般而言,在 Win32 系統中,尤其是 Windows NT/XP 以上的作業系統中,開啟實體磁碟是有限制的。這是因為這種做法具有危險性,假如程式不當的寫入分割區或是重要的磁區,會造成作業系統無法辨認,因此在撰寫此類程式實應特別小心。除此之外,系統對開啟實體磁碟與邏輯磁碟也有一些限制:

  1. 必須具有管理員權限的程式才能執行。
  2. dwCreationDisposition 參數必須具有 OPEN_EXISTING 位元。
  3. 當開啟邏輯磁碟或軟碟時,dwShareMode 參數必須具有 FILE_SHARE_WRITE 位元。

當開啟實體磁碟時,lpFileName 指向以 NULL 結尾、形如「\\.\PhysicalDriveX」的字串,例如:

lpFileName意  義
"\\.\PhysicalDrive0",0開啟第一個實體磁碟
"\\.\PhysicalDrive2",0開啟第三個實體磁碟

當開啟邏輯磁碟時,lpFileName 指向以 NULL 結尾、形如「\\.\X:」的字串,例如:

lpFileName意  義
"\\.\A:",0開啟 A: 磁碟,即第一個軟碟
"\\.\C:",0開啟 C: 磁碟,即第一個邏輯磁碟

一旦成功開啟實體磁碟或邏輯磁碟後,CreateFile 的傳回值稱為直接存取儲存裝置代碼 ( direct access storage device (DASD) handle ),我們可以利用此 DASD 代碼呼叫 DeviceIoControl API。

DeviceIoControl API

DeviceIoControl 能傳入許多不同的控制碼,對某個裝置作相對應的操作。其原型為:

BOOL WINAPI DeviceIoControl(
  __in         HANDLE       hDevice,
  __in         DWORD        dwIoControlCode,
  __in_opt     LPVOID       lpInBuffer,
  __in         DWORD        nInBufferSize,
  __out_opt    LPVOID       lpOutBuffer,
  __in         DWORD        nOutBufferSize,
  __out_opt    LPDWORD      lpBytesReturned,
  __inout_opt  LPOVERLAPPED lpOverlapped
);

第一個參數,hDevice,是由 CreateFile 傳回的代碼。第二個參數,dwIoControlCode,是控制碼,不同的控制碼能對裝置作不同的操作,也影響 lpInBuffer、nInBufferSize、lpOutBuffer 和 nOutBufferSize 所代表的意義。lpBytesReturned 指向一變數位址,DeviceIoControl 會把傳到緩衝區的資料長度存入此變數中,以位元組為單位。如果緩衝區太小以致於無法容納傳來的任何一筆資料,lpBytesReturned 所指的變數會變為 0,可以呼叫 GetLastError,而傳回 ERROR_INSUFFICIENT_BUFFER;如果緩衝區太小,但是可以容納數筆資料,lpBytesReturned 所指的變數會變為實際得到的資料長度,可以呼叫 GetLastError,而傳回 ERROR_MORE_DATA。lpBytesReturned 與 lpOverlapped 不能同時為零。

事實上,在 MSDN 堙A把控制碼分成好幾項,例如有「磁碟管理控制碼」( Disk Management Control Codes )、「邏輯磁碟控制碼」( Volume Management Control Codes )、「通訊設備控制碼」( Communications Control Codes ) 等等。每一項又包含許多控制碼,例如磁碟管理控制碼又有 IOCTL_DISK_CREATE_DISK、IOCTL_DISK_GET_DRIVE_GEOMETRY_EX、IOCTL_DISK_GET_PARTITION_INFO_EX……等控制碼。因此控制碼太多了,所以小木偶僅僅藉由底下三個控制碼說明 DeviceIoControl 的用法。

IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS

IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS 屬於「邏輯磁碟控制碼」的一種,它的目的是取某個邏輯磁碟在哪一個實體磁碟上。呼叫方式是:

BOOL DeviceIoControl(
     (HANDLE) hDevice,                        // handle to device
     IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,    // dwIoControlCode
     NULL,                                    // lpInBuffer
     0,                                       // nInBufferSize
     (LPVOID) lpOutBuffer,                    // output buffer
     (DWORD) nOutBufferSize,                  // size of output buffer
     (LPDWORD) lpBytesReturned,               // number of bytes returned
     (LPOVERLAPPED) lpOverlapped              // OVERLAPPED structure
);

hDevice 是經由呼叫 CreateFile 所得的裝置代碼,因為是取得邏輯磁碟的裝置代碼,所以 lpFileName 指向類似「"\\.\C:",0」的字串,而 dwShareMode 參數必須具有 FILE_SHARE_WRITE 位元、dwCreationDisposition 參數必須具有 OPEN_EXISTING 位元。

lpOutBuffer 指向一個稱為 VOLUME_DISK_EXTENTS 的結構體,如果 DeviceIoControl 成功執行,作業系統會把正確的資料填入此結構體堙C此結構體的成員為:

VOLUME_DISK_EXTENTS     STRUCT
NumberOfDiskExtents     DWORD           ?
dwUnknown               DWORD           ?       ;MSDN無此欄位
Extents                 DISK_EXTENT     ANYSIZE_ARRAY DUP (<>)
VOLUME_DISK_EXTENTS     ENDS

這堛 VOLUME_DISK_EXTENTS 與 MSDN 所列的不同,MSDN 中的 VOLUME_DISK_EXTENTS 沒有 dwUnknown 欄位,這個欄位是小木偶自己加上去的。但經過我實際操作觀察,VOLUME_DISK_EXTENTS 結構體應該是像上面這樣才能正確執行,否則不能正確執行。但網路上除了 Air 喬丹的部落格有與我類似的狀況,其餘均沒有發現,讓我覺得很納悶。NumberOfDiskExtents 是指此邏輯磁碟有多少個「extent」組成,如果使用動態磁碟,NumberOfDiskExtents 可能會超過 1,DeviceIoControl 會傳回 ERROR_MORE_DATA,主程式應該繼續呼叫 DeviceIoControl,得到此邏輯磁碟的所有資料。第三個欄位,Extents,也是一個結構體,DISK_EXTENT,所組成的陣列,但是查 WINDOWS.INC 卻發現 ANYSIZE_ARRAY 為 1,亦即此陣列僅由一個元素組成。DISK_EXTENT 結構體的成員是:

DISK_EXTENT     STRUCT
DiskNumber      DWORD           ?
dwUnknown       DWORD           ?       ;MSDN無此欄位
StartingOffset  LARGE_INTEGER   <>
ExtentLength    LARGE_INTEGER   <>
DISK_EXTENT     ENDS

第一個欄位,DiskNumber,就是該邏輯磁碟在哪一個實體磁碟中,如果是「0」,表示在「\\.\PhysicalDrive0」實體磁碟堙C第二個欄位,dwUnknown,也和 VOLUME_DISK_EXTENTS 的 dwUnknown 一樣,不知何故,與我實驗不符。第三、第四個欄位是 64 位元長的整數,分別表示此邏輯磁碟從實體磁碟的第幾個位元組開始,而長度為多少位元組。它們的資料形態為 LARGE_INTEGER,可以在 WINDOWS.INC 塈鋮魽G

LARGE_INTEGER       UNION
    STRUCT
        LowPart     DWORD   ?
        HighPart    DWORD   ?
    ENDS
    QuadPart        QWORD   ?
LARGE_INTEGER       ENDS

再回到 DeviceIoControl 的第六個參數,nOutBufferSize,表示緩衝區大小,此處填上「SIZEOF VOLUME_DISK_EXTENTS」即可,以位元組為單位,必須在呼叫前推入堆疊。第七個參數,lpBytesReturned,指向某個變數,以獲得實際上從 DeviceIoControl 傳回的資料長度,以位元組為單位。最後一個參數,lpOverlapped,它是一個位址,指向 OVERLAPPED 結構體。如果 lpBytesReturned 為 NULL,就必須設定 lpOverlapped 為某個 OVERLAPPED 結構體的位址;如果 lpBytesReturned 已設為 某個變數的位址,lpOverlapped 就必須為 NULL。如果 DeviceIoControl 執行成功,傳回值為非零;若失敗,傳回值為 0,可以呼叫 GetLastError,如果 GetLastError 傳回 ERROR_MORE_DATA,就表示緩衝區太小,不足以容納傳回的資料。

IOCTL_DISK_GET_DRIVE_GEOMETRY_EX

IOCTL_DISK_GET_DRIVE_GEOMETRY_EX 屬於「磁碟管理控制碼」的一種,用來取得實體磁碟的資料,如磁柱 ( cylinders ) 總數、每磁柱所含磁軌 ( tracks ) 數,每磁軌所含磁區 ( sectors ) 數、每磁區所含位元組 ( bytes ) 數,其原型為:

BOOL DeviceIoControl(
     (HANDLE) hDevice,                 // handle to volume
     IOCTL_DISK_GET_DRIVE_GEOMETRY_EX, // dwIoControlCode
     NULL,                             // lpInBuffer
     0,                                // nInBufferSize
     (LPVOID) lpOutBuffer,             // output buffer
     (DWORD) nOutBufferSize,           // size of output buffer
     (LPDWORD) lpBytesReturned,        // number of bytes returned
     (LPOVERLAPPED) lpOverlapped       // OVERLAPPED structure
);

hDevice 是呼叫 CreateFile 傳回來的直接存取儲存裝置代碼,呼叫 CreateFile 時,lpFileName 應指向類似「\\.\PhysicalDriveX」字串的位址,請參考前面的說明。lpOutBuffer 指向 DISK_GEOMETRY_EX 結構體的位址,如果 DeviceIoControl 執行正確,作業系統會把資料填入此結構體內,這個結構體的欄位是:

DISK_GEOMETRY_EX    STRUCT
Geometry            DISK_GEOMETRY   <>
DiskSize            LARGE_INTEGER   <>
DataOne             BYTE            ?
DISK_GEOMETRY_EX    ENDS

第二個欄位,DiskSize,是一個 64 位元的正整數,表示磁碟大小,以位元組為單位。Geometry 是一個 DISK_GEOMETRY 結構體,各欄位是:

DISK_GEOMETRY       STRUCT
Cylinders           LARGE_INTEGER   <>
MediaType           MEDIA_TYPE      ?
TracksPerCylinder   DWORD           ?       ;每磁柱所含磁軌數
SectorsPerTrack     DWORD           ?       ;每磁軌所含磁區數
BytesPerSector      DWORD           ?       ;每磁區所含位元組數
DISK_GEOMETRY       ENDS

Cylinders 是一個 64 位元的正整數,表示磁柱總數。MediaType 是一個 32 位元的正整數,代表儲存媒體的類型,如軟碟片、磁光機或硬碟等等,但大部分均已淘汰,現在常用的大概只剩下 RemovableMedia 和 FixedMedia,分別表示隨身碟和硬碟。其他的欄位我想就不必說明了。

再回到 DeviceIoControl 的第六個參數,nOutBufferSize,表示緩衝區大小,以位元組為單位,應該填入「SIZEOF DISK_GEOMETRY_EX」。lpBytesReturned 指向一個變數的位址,DeviceIoControl 會把傳回的資料長度填入此變數堙A傳回的資料長度單位是位元組。如果 nOutBufferSize 所指定的大小太小,則呼叫失敗,lpBytesReturned 為 0,可以呼叫 GetLastError,而傳回 ERROR_INSUFFICIENT_BUFFER。如果 lpOverlapped 為 NULL,則 lpBytesReturned 不可為 NULL。如果 DeviceIoControl 執行成功,傳回非零值;如果失敗,傳回 0,呼叫 GetLastError 得到更多資訊。

IOCTL_DISK_GET_LENGTH_INFO

IOCTL_DISK_GET_LENGTH_INFO 屬於「磁碟管理控制碼」的一種,用於取得實體磁碟或邏輯磁碟的大小,以位元組為單位。呼叫 DeviceIoControl 時的各參數為:

BOOL DeviceIoControl(
  (HANDLE) hDevice,              // handle to device
  IOCTL_DISK_GET_LENGTH_INFO,    // dwIoControlCode
  NULL,                          // lpInBuffer
  0,                             // nInBufferSize
  (LPVOID) lpOutBuffer,          // output buffer
  (DWORD) nOutBufferSize,        // size of output buffer
  (LPDWORD) lpBytesReturned,     // number of bytes returned
  (LPOVERLAPPED) lpOverlapped    // OVERLAPPED structure
);

其中 hDevice 是邏輯磁碟代碼或實體磁碟代碼,呼叫 CreateFile 可傳回這兩種代碼。lpOutBuffer 指向 GET_LENGTH_INFORMATION 結構體,此結構體只有一個欄位:

GET_LENGTH_INFORMATION  STRUCT
LengthOfDev             LARGE_INTEGER   <>
GET_LENGTH_INFORMATION  ENDS

在 MSDN 這個欄位稱為 Length,但是與 MASM 的保留字相同,因此小木偶改為 LengthOfDev,它是 64 位元的正整數。如果 hDevice 是邏輯磁碟,則 LengthOfDev 是邏輯磁碟大小;如果 hDevice 是實體磁碟,則 LengthOfDev 是實體磁碟大小。均以位元組為單位。其他參數與前面相同,請自行參考。

GetLogicalDriveFromPhysicalDrive 的流程

說完 GetLogicalDriveFromPhysicalDrive 用到的 API 後,小木偶先給大家看看 GetLogicalDriveFromPhysicalDrive 的原始碼,然後再講解流程。底下就是原始碼:

;-----------------------------------------------------------------------------------------------;001
;此副程式,GetLogicalDriveFromPhysicalDrive,能取每個實體硬碟中所含的邏輯磁碟,並傳回給主程式
;輸入:lpData-要傳回的資料位址,如果GetLogicalDriveFromPhysicalDrive正確執行,會在此位址填上
;              格式為「'\\.\PhysicalDrive0',0,'C:',0,'D:',0,
;                      '\\.\PhysicalDrive1',0,'E:',0,0」
;                      表示第一個實體硬碟(即\\.\PhysicalDrive0)有兩個邏輯磁碟,C、D
;                      第二個實體硬碟(即\\.\PhysicalDrive1)僅一個邏輯磁碟,E
;                      以「0,0」為結尾
;              如果主程式傳來的lpData為零,傳回值EAX為所需容納的資料大小(以位元組為單位)
;輸出:EAX-若成功,EAX為傳回的資料大小(以位元組為單位),且lpData所指位址會填入正確的資料       ;010
;           若失敗,EAX=0
GetLogicalDriveFromPhysicalDrive    PROC    USES esi edi ebx, lpData:DWORD
                LOCAL   aryLogicalDrive[104]:BYTE       ;每個邏輯磁碟機名稱為「'C:\',0」,最多26個
                LOCAL   szDeviceName[8]:BYTE            ;'\\.\C:',0
                LOCAL   vde:VOLUME_DISK_EXTENTS
                LOCAL   idx:DWORD       ;呼叫GetLogicalDriveStrings得到邏輯儲存裝置名的陣列索引
                LOCAL   nOut:DWORD      ;呼叫DeviceIoControl傳回讀取多少位元組
                LOCAL   nData:DWORD     ;hMem內所記載的重要資料大小,亦即要傳回的資料大小,以位元組為單位
                LOCAL   nPhysicalDriver:DWORD   ;實體硬碟個數
                LOCAL   hDevice,hMem:HANDLE                                                     ;020
                LOCAL   szPhyDrive[20]:BYTE
                sub     ecx,ecx
                cld
                mov     idx,ecx
                mov     nData,ecx                                                               ;025
                mov     nPhysicalDriver,ecx
        ;初始化szPhyDrive字串,亦即填入"\\.\PhysicalDrive0",0
                lea     edi,szPhyDrive
                mov     DWORD PTR [edi],5c2e5c5ch
                mov     DWORD PTR [edi+4],73796850h                                             ;030
                mov     DWORD PTR [edi+8],6c616369h
                mov     DWORD PTR [edi+12],76697244h
                mov     DWORD PTR [edi+16],3065h
        ;初始化szDeviceName字串,亦即填入"\\.\",?,":",0
                lea     edi,szDeviceName                                                        ;035
                mov     DWORD PTR [edi],5c2e5c5ch
                mov     WORD PTR [edi+5],3ah
        ;預留2KB空間存放要傳回的資料(每個邏輯磁碟佔3個位元組{如「C:」,0},假設每個實體硬碟都有 26
        ;個邏輯磁區,故佔26*3=78位元組,再加上「"\\.\PhysicalDrive1",0」共19個位元組,故每個實體硬
        ;碟佔97個位元組,假設有20個實體硬碟,共需要1940個位元組,但再加上最後一個0,有971個位元組,
        ;為求保險,向系統要求2KB)
                INVOKE  GlobalAlloc,GPTR,2048
                mov     hMem,eax

                INVOKE  GetLogicalDriveStrings,SIZEOF aryLogicalDrive,ADDR aryLogicalDrive      ;045
next_drv:       mov     edx,idx
                lea     esi,aryLogicalDrive
                shl     edx,2
                add     esi,edx
                mov     al,[esi]        ;AL=邏輯儲存裝置名稱,如"C"、"D"等等                   ;050
                cmp     al,0
                jz      finished
                lea     edi,szDeviceName
                mov     [edi+4],al
                INVOKE  GetDriveType,esi                                                        ;055
        ;若EAX=DRIVE_FIXED,則此邏輯磁碟可能在硬碟或隨身碟(flash driver)上
        .IF eax==DRIVE_FIXED
                INVOKE  CreateFile,ADDR szDeviceName,GENERIC_READ,FILE_SHARE_READ or \
                        FILE_SHARE_WRITE,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
                cmp     eax,INVALID_HANDLE_VALUE                                                ;060
                je      error
                mov     hDevice,eax
                INVOKE  DeviceIoControl,hDevice,IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,0,0,ADDR vde,\
                        SIZEOF VOLUME_DISK_EXTENTS,ADDR nOut,0
                INVOKE  CloseHandle,hDevice                                                     ;065
            ;計算應該把此邏輯磁碟存在hMem的哪個位址,此位址前幾個位元組為"\\.\PhysicalDrive",若
            ;不是的話,則表示此邏輯磁碟為尚未被此副程式處理過的實體硬碟
                mov     eax,vde.Extents.DiskNumber
                sub     edx,edx
                mov     bl,al                                                                   ;070
                mov     ecx,97  ;Windows中,每個實體硬碟最多26個邏輯磁碟,每個邏輯磁碟佔3位元組
                mov     edi,hMem;(如「C:\,0」),再加上「\\.\PhysicalDrive?",0」,共97位元組
                mul     ecx
                lea     esi,szPhyDrive
                add     edi,eax                                                                 ;075
            ;找到新的實體硬碟,把szPhyDrive字串存入EDI所指位址
            .IF BYTE PTR [edi]==0
                mov     ecx,19
                rep     movsb
                add     [edi-2],bl                                                              ;080
                inc     nPhysicalDriver
                jmp     save_driver
            .ENDIF
            ;搜尋hMem裡,此實體硬碟"\\.\PhysicalDrive?",0,'C:',0,'D:',0,0,0……後哪一個位址是空著的
                mov     al,0                                                                    ;085
                mov     ecx,97
@@:             repne   scasb
                cmp     BYTE PTR [edi],0
                jne     @b
            ;取得邏輯磁碟名稱,存於AL                                                           ;090
save_driver:    lea     edx,szDeviceName
                mov     ah,':'
                mov     al,[edx+4]
                stosw
                xor     al,al                                                                   ;095
                stosb
        .ENDIF
                inc     idx
                jmp     next_drv
                                                                                                ;100
;已在hMem記錄了實體硬碟包含哪些邏輯磁碟,現在要把它傳給主程式指定的記憶體了
finished:       mov     edi,lpData
                sub     edx,edx
                mov     idx,edx
   .WHILE idx<=20                                                                               ;105
                mov     eax,97
                sub     edx,edx
                mov     esi,hMem
                mul     idx
                add     esi,eax ;ESI=在hMem堙A某個實體硬碟的開始位址                          ;110
       .IF BYTE PTR [esi]=="\"
                mov     ecx,19
                add     nData,ecx
            .IF edi==0
                add     esi,ecx                                                                 ;115
            .ELSE
                rep     movsb
            .ENDIF
                xor     ebx,ebx ;EBX=邏輯磁碟個數
next_logic_drv: cmp     BYTE PTR [esi],0                                                        ;120
                jz      finish_the_phy
                mov     ecx,3
                add     nData,ecx
            .IF edi==0
                add     esi,ecx                                                                 ;125
            .ELSE
                lodsw
                stosw
                lodsb
                stosb                                                                           ;130
            .ENDIF
                inc     ebx
                cmp     ebx,26
                jb      next_logic_drv
       .ENDIF                                                                                   ;135
finish_the_phy: inc     idx
   .ENDW
            .IF edi!=0
                mov     BYTE PTR [edi],0
            .ENDIF                                                                              ;140
                INVOKE  GlobalFree,hMem
                mov     eax,nData
                inc     eax
                jmp     exit
error:          INVOKE  GlobalFree,hMem                                                         ;145
                xor     eax,eax
exit:           ret
GetLogicalDriveFromPhysicalDrive    ENDP
;-----------------------------------------------------------------------------------------------;149

再來要說一說 GetLogicalDriveFromPhysicalDrive 的流程:

  1. GetLogicalDriveFromPhysicalDrive 副程式一開始,是初始化各個區域變數。

  2. 第 38∼43 行,是建立一個記憶體區塊,此記憶體區塊位址是在 hMem 變數堙A此記憶體區塊將依照每個實體磁碟內的邏輯磁碟來分配。但是在程式一開始時,我們不知道每個實體磁碟有幾個邏輯磁碟,所以假設最多 26 個邏輯磁碟 ( 事實上,Windows 作業系統就只能辨認 A: 到 Z: 共 26 個邏輯磁碟,這些邏輯磁碟包含了軟碟、硬碟、光碟、隨身碟、網路磁碟等等 )。每個邏輯磁碟佔了 3 個位元組,例如「"C:",0」,再加上「"\\.\PhysicalDrive1",0」共19個位元組,故每個實體硬碟佔 97 個位元組。假設有 20 個實體硬碟,共需要 1940 個位元組,所以向系統要求 2KB。

    GetLogicalDriveFromPhysicalDrive 執行近結束時,hMem 的內容會是像下面這樣:

    00146068  5C 5C 2E 5C 50 68 79 73 69 63 61 6C 44 72 69 76  \\.\PhysicalDriv
    00146078  65 30 00 43 3A 00 45 3A 00 48 3A 00 49 3A 00 00  e0.C:.E:.H:.I:..
    00146088  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00146098  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    001460A8  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    001460B8  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    001460C8  00 5C 5C 2E 5C 50 68 79 73 69 63 61 6C 44 72 69  .\\.\PhysicalDri
    001460D8  76 65 31 00 46 3A 00 47 3A 00 4A 3A 00 00 00 00  ve1.F:.G:.J:....
    001460E8  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    001460F8  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00146108  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00146118  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  ................
    00146128  00 00 5C 5C 2E 5C 50 68 79 73 69 63 61 6C 44 72  ..\\.\PhysicalDr
    00146138  69 76 65 32 00 44 3A 00 00 00 00 00 00 00 00 00  ive2.D:.........
                             hMem 的內容

    由上面 hMem 的內容來看,小木偶想法很簡單,先為每個實體磁碟預留 97 個位元組,再決定各個邏輯磁碟位於哪個實體磁碟中,然後計算該邏輯磁碟應該填入哪一個實體磁碟所屬的區塊,再稍加整理刪去多餘的空間即可。

  3. 第 44∼45 行是呼叫 GetLogicalDriveStrings,得到邏輯磁碟的根目錄名填入 aryLogicalDrive 堶情AaryLogicalDrive 是一個區域陣列,此陣列共 104 位元組 ( 26×4=104 )。當 GetLogicalDriveStrings 執行完後,aryLogicalDrive 內容變為:

    0012F760                                      43 3A 5C 00              C:\.
    0012F770  44 3A 5C 00 45 3A 5C 00 46 3A 5C 00 47 3A 5C 00  D:\.E:\.F:\.G:\.
    0012F780  48 3A 5C 00 49 3A 5C 00 4A 3A 5C 00 4B 3A 5C 00  H:\.I:\.J:\.K:\.
    0012F790  4D 3A 5C 00 4E 3A 5C 00 00                       M:\.N:\..
  4. 第 46 行開始,是以 idx 變數為索引值,亦即 idx 由 0 開始每次增加一,檢驗每個邏輯磁碟位於哪一個實體磁碟堙C

    1. 在第 46∼49 行計算邏輯磁碟根目錄名的位址,存於 ESI,當 idx 為 0 時,ESI 指向「"C:\",0」;當 idx 為 1 時,ESI 指向「"D:\",0」……依此類推。
    2. 第 50∼52 行,檢查 ESI 所指的邏輯磁碟根目錄名是否為「0」,如果是,表示已經處理完所有的邏輯磁碟名稱,跳到下一階段,「把資料傳回給主程式指定的記憶體」,即程式第 101 行以後。
    3. 如果 ESI 所指邏輯磁碟根目錄不為「0」,得到邏輯磁碟代號,存於 AL ( 第 50 行 ),並存於 szDeviceName 字串堙A以待 d 步驟之用。接著以存於 ESI 內的位址為參數,呼叫 GetDriveType,得到邏輯磁碟的儲存裝置類型,如果是硬碟或隨身碟才繼續 d 步驟;如果不是,則跳到第 98 行。
    4. 第 58∼65 行,以 szDeviceName 呼叫 CreateFile 取得裝置代碼,szDeviceName 字串是類似「"\\.\C:",0」的字串,其中的邏輯磁碟名稱早已在第 54 行備妥了。接著以 IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS 呼叫 DeviceIoControl,在 vde.Extents.DiskNumber 就存有該邏輯磁碟所在的實體磁碟,將其存於 EAX ( 第 68 行 )。
    5. 第 66∼75 行,計算該實體磁碟應該在哪一個位址上,並存於 EDI 堙AEDI 所指的記憶體位址位於 hMem 堙C例如如果是實體磁碟 2,所得 EDI 為 014612A,指向「hMem 的內容」的紫色位址。
    6. 第 76∼83 行,檢查 EDI 所指位址的內容是否為「0」,如果是零,表示之前處理過的邏輯磁碟並無任何一個屬於此實體磁碟,把「"\\.\PhysicalDrive?",0」字串填入 EDI 所指位址;如果不為「0」,直接跳到步驟 g。
    7. 第 84∼89 行的程式碼是搜尋空的欄位,以填入邏輯磁碟名稱。填入邏輯磁區的程式碼在第 90∼97 行。

  5. 此一階段程式的工作是把在 hMem 所指位址的記憶體內容傳回到主程式指定的位址,此位址是以參數,lpData,傳給 GetLogicalDriveFromPhysicalDrive 的,在第 102 行,指定 EDI 為 lpData。此段程式仍以 idx 為索引,所以先將其清空,因為之前假設最多系統接了 20 個實體磁碟,所以 idx 在小於 20 時,才做接下來的步驟,因此把接下來的步驟放在一個「.WHILE」和「.ENDW」迴圈堙C接下來的步驟,小木偶也細分幾段說明:

    1. 第 106∼110 行的程式碼是計算某個實體磁碟區塊的起始位址,並將結果存於 ESI 堙C
    2. 第 111∼118 行是把「"\\.\PhysicalDrive?",0」字串填入 EDI 所指的位址堙C但是如果 EDI 為零,表示 GetLogicalDriveFromPhysicalDrive 只需要計算所需容納的記憶體區塊大小;如果 EDI 不為零,那就要實際把該字串移入主程式在 lpData 指定的位址內。
    3. 第 119∼134 行是把某個實體磁碟內所含的邏輯磁碟,一個一個的填到主程式指定的位址內,跟步驟 b 一樣,如果 EDI 為零,表示只需要計算所需容納的記憶體區塊大小;如果 EDI 不為零,那就要實際把邏輯磁碟的名稱移入主程式指定的位址內。此外,Windows 系統最多也僅能辨認 26 個邏輯磁碟,所以利用 EBX 去計算邏輯磁碟個數,如果超過就可以停止了。
  6. 第 138∼140 行,如果 EDI 不為零,表示必須傳回各個實體磁碟所含的邏輯磁碟名稱,到此為止幾乎完成,最後再填入一個位元組的零,使其變成兩個「0」,作為資料的結尾。第 141 行,釋放 hMem 記憶體區塊。第 142∼143 行,因為多了一個位元組的「0」,所以所需記憶體長度再增加一,並存於 EAX 堙A作為傳回值。


範例

底下小木偶就以一個例子,說明如何呼叫 GetLogicalDriveFromPhysicalDrive。我想還是使用第 20 章的方法建立 HD.DLL 動態連結程式庫。HD.ASM 的原始碼如下:

        .586
        .MODEL  FLAT,STDCALL
        OPTION  CASEMAP:NONE

INCLUDE         WINDOWS.INC
INCLUDE         KERNEL32.INC
INCLUDELIB      KERNEL32.LIB

IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS    EQU     560000h

DISK_EXTENT     STRUCT
DiskNumber      DWORD           ?
dwUnknown       DWORD           ?               ;MSDN無此欄位
StartingOffset  LARGE_INTEGER   <>
ExtentLength    LARGE_INTEGER   <>
DISK_EXTENT     ENDS

VOLUME_DISK_EXTENTS     STRUCT
NumberOfDiskExtents     DWORD           ?
dwUnknown               DWORD           ?       ;MSDN無此欄位
Extents                 DISK_EXTENT     ANYSIZE_ARRAY DUP (<>)
VOLUME_DISK_EXTENTS     ENDS

;***************************************************************************************************
.CODE
;---------------------------------------------------------------------------------------------------
;進入/離開副程式
DLLEntry        PROC    hInstDLL,dwReason,dwReserved
                mov     eax,TRUE        ;不需事前處理或事後清理,故僅返回TRUE
                ret
DLLEntry        ENDP
;---------------------------------------------------------------------------------------------------
;此副程式,GetLogicalDriveFromPhysicalDrive,能取每個實體硬碟中所含的邏輯磁碟,並傳回給主程式
;輸入:lpData-要傳回的資料位址,如果GetLogicalDriveFromPhysicalDrive正確執行,會在此位址填上
;              格式為「'\\.\PhysicalDrive0',0,'C:',0,'D:',0,
;                      '\\.\PhysicalDrive1',0,'E:',0,0」
;                      表示第一個實體硬碟(即\\.\PhysicalDrive0)有兩個邏輯磁碟,C、D
;                      第二個實體硬碟(即\\.\PhysicalDrive1)僅一個邏輯磁碟,E
;                      以「0,0」為結尾
;              如果主程式傳來的lpData為零,傳回值EAX為所需容納的資料大小(以位元組為單位)
;輸出:EAX-若成功,EAX為傳回的資料大小(以位元組為單位),且lpData所指位址會填入正確的資料
;           若失敗,EAX=0
GetLogicalDriveFromPhysicalDrive    PROC    USES esi edi ebx, lpData:DWORD
                LOCAL   aryLogicalDrive[104]:BYTE       ;每個邏輯磁碟機名稱為「'C:\',0」,最多26個
                LOCAL   szDeviceName[8]:BYTE            ;'\\.\C:',0
                LOCAL   vde:VOLUME_DISK_EXTENTS
                LOCAL   idx:DWORD       ;呼叫GetLogicalDriveStrings得到邏輯儲存裝置名的陣列索引
                LOCAL   nOut:DWORD      ;呼叫DeviceIoControl傳回讀取多少位元組
                LOCAL   nData:DWORD     ;hMem內所記載的重要資料大小,亦即要傳回的資料大小,以位元組為單位
                LOCAL   nPhysicalDriver:DWORD   ;實體硬碟個數
                LOCAL   hDevice,hMem:HANDLE
                LOCAL   szPhyDrive[20]:BYTE
                sub     ecx,ecx
                cld
                mov     idx,ecx
                mov     nData,ecx
                mov     nPhysicalDriver,ecx
        ;初始化szPhyDrive字串,亦即填入"\\.\PhysicalDrive0",0
                lea     edi,szPhyDrive
                mov     DWORD PTR [edi],5c2e5c5ch
                mov     DWORD PTR [edi+4],73796850h
                mov     DWORD PTR [edi+8],6c616369h
                mov     DWORD PTR [edi+12],76697244h
                mov     DWORD PTR [edi+16],3065h
        ;初始化szDeviceName字串,亦即填入"\\.\",?,":",0
                lea     edi,szDeviceName
                mov     DWORD PTR [edi],5c2e5c5ch
                mov     WORD PTR [edi+5],3ah
        ;預留2KB空間存放要傳回的資料(每個邏輯磁碟佔3個位元組{如「C:」,0},假設每個實體硬碟都有 26
        ;個邏輯磁區,故佔26*3=78位元組,再加上「"\\.\PhysicalDrive1",0」共19個位元組,故每個實體硬
        ;碟佔97個位元組,假設有20個實體硬碟,共需要1940個位元組,但再加上最後一個0,有971個位元組,
        ;為求保險,向系統要求2KB)
                INVOKE  GlobalAlloc,GPTR,2048
                mov     hMem,eax

                INVOKE  GetLogicalDriveStrings,SIZEOF aryLogicalDrive,ADDR aryLogicalDrive
next_drv:       mov     edx,idx
                lea     esi,aryLogicalDrive
                shl     edx,2
                add     esi,edx
                mov     al,[esi]        ;AL=邏輯儲存裝置名稱,如"C"、"D"等等
                cmp     al,0
                jz      finished
                lea     edi,szDeviceName
                mov     [edi+4],al
                INVOKE  GetDriveType,esi
        ;若EAX=DRIVE_FIXED,則此邏輯磁碟可能在硬碟或隨身碟(flash driver)上
        .IF eax==DRIVE_FIXED
                INVOKE  CreateFile,ADDR szDeviceName,GENERIC_READ,FILE_SHARE_READ or \
                        FILE_SHARE_WRITE,0,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0
                cmp     eax,INVALID_HANDLE_VALUE
                je      error
                mov     hDevice,eax
                INVOKE  DeviceIoControl,hDevice,IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS,0,0,ADDR vde,\
                        SIZEOF VOLUME_DISK_EXTENTS,ADDR nOut,0
                INVOKE  CloseHandle,hDevice
            ;計算應該把此邏輯磁碟存在hMem的哪個位址,此位址前幾個位元組為"\\.\PhysicalDrive",若
            ;不是的話,則表示此邏輯磁碟為尚未被此副程式處理過的實體硬碟
                mov     eax,vde.Extents.DiskNumber
                sub     edx,edx
                mov     bl,al
                mov     ecx,97  ;Windows中,每個實體硬碟最多26個邏輯磁碟,每個邏輯磁碟佔3位元組
                mov     edi,hMem;(如「C:\,0」),再加上「\\.\PhysicalDrive?",0」,共97位元組
                mul     ecx
                lea     esi,szPhyDrive
                add     edi,eax
            ;找到新的實體硬碟,把szPhyDrive字串存入EDI所指位址
            .IF BYTE PTR [edi]==0
                mov     ecx,19
                rep     movsb
                add     [edi-2],bl
                inc     nPhysicalDriver
                jmp     save_driver
            .ENDIF
            ;搜尋hMem裡,此實體硬碟"\\.\PhysicalDrive?",0,'C:',0,'D:',0,0,0……後哪一個位址是空著的
                mov     al,0
                mov     ecx,97
@@:             repne   scasb
                cmp     BYTE PTR [edi],0
                jne     @b
            ;取得邏輯磁碟名稱,存於AL
save_driver:    lea     edx,szDeviceName
                mov     ah,':'
                mov     al,[edx+4]
                stosw
                xor     al,al
                stosb
        .ENDIF
                inc     idx
                jmp     next_drv

;已在hMem記錄了實體硬碟包含哪些邏輯磁碟,現在要把它傳給主程式指定的記憶體了
finished:       mov     edi,lpData
                sub     edx,edx
                mov     idx,edx
   .WHILE idx<=20
                mov     eax,97
                sub     edx,edx
                mov     esi,hMem
                mul     idx
                add     esi,eax ;ESI=在hMem堙A某個實體硬碟的開始位址
       .IF BYTE PTR [esi]=="\"
                mov     ecx,19
                add     nData,ecx
            .IF edi==0
                add     esi,ecx
            .ELSE
                rep     movsb
            .ENDIF
                xor     ebx,ebx ;EBX=邏輯磁碟個數
next_logic_drv: cmp     BYTE PTR [esi],0
                jz      finish_the_phy
                mov     ecx,3
                add     nData,ecx
            .IF edi==0
                add     esi,ecx
            .ELSE
                lodsw
                stosw
                lodsb
                stosb
            .ENDIF
                inc     ebx
                cmp     ebx,26
                jb      next_logic_drv
       .ENDIF
finish_the_phy: inc     idx
   .ENDW
            .IF edi!=0
                mov     BYTE PTR [edi],0
            .ENDIF
                INVOKE  GlobalFree,hMem
                mov     eax,nData
                inc     eax
                jmp     exit
error:          INVOKE  GlobalFree,hMem
                xor     eax,eax
exit:           ret
GetLogicalDriveFromPhysicalDrive    ENDP
;***************************************************************************************************
END     DLLEntry

將上面存成 HD.ASM 後,再建立一個 HD.DEF 純文字檔,內容如下:

EXPORTS
        GetLogicalDriveFromPhysicalDrive=GetLogicalDriveFromPhysicalDrive

然後開啟「命令提示字元」,輸入下面指令:

E:\HomePage\SOURCE\Win32\HD_Info>ml /c hd.asm [Enter]
Microsoft (R) Macro Assembler Version 6.14.8444
Copyright (C) Microsoft Corp 1981-1997.  All rights reserved.

 Assembling: hd.asm

***********
ASCII build
***********


E:\HomePage\SOURCE\Win32\HD_Info>link /DLL /SUBSYSTEM:WINDOWS /DEF:hd.def hd.obj [Enter]

Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

   Creating library hd.lib and object hd.exp

E:\HomePage\SOURCE\Win32\HD_Info>

到此,已建立好 HD.DLL 動態連結程式庫了。接著再建立 LOGICAL.ASM,當做測試 HD.DLL 的測試檔,LOGICAL.ASM 原始碼如下:

        .586
        .MODEL  FLAT,STDCALL
        OPTION  CASEMAP:NONE

INCLUDE         WINDOWS.INC
INCLUDE         KERNEL32.INC
INCLUDE         USER32.INC
INCLUDELIB      KERNEL32.LIB
INCLUDELIB      USER32.LIB
INCLUDELIB      HD.LIB

GetLogicalDriveFromPhysicalDrive    PROTO   lpData:DWORD
;*********************************************************************
.DATA
hMem    HANDLE  ?
szData  DB      256 DUP (0)
szTitle DB      '邏輯磁碟與實體磁碟',0
;*********************************************************************
.CODE
;---------------------------------------------------------------------
start:  INVOKE  GetLogicalDriveFromPhysicalDrive,0
        inc     eax
        INVOKE  GlobalAlloc,GPTR,eax
        mov     hMem,eax
        INVOKE  GetLogicalDriveFromPhysicalDrive,hMem
        mov     edi,OFFSET szData
        mov     esi,hMem
nxtphy: cmp     DWORD PTR [esi],5c2e5c5ch       ;"\\.\"
        jne     error
        add     esi,4
        mov     ecx,14
        rep     movsb
@@:     inc     esi
        cmp     BYTE PTR [esi],'\'
        je      cr_lf
        cmp     BYTE PTR [esi],0
        je      endstr
        mov     al,' '
        stosb
        lodsw
        stosw
        jmp     @b
cr_lf:  mov     ax,0a0dh
        stosw
        jmp     nxtphy
error:
endstr: mov     al,0
        stosb
        INVOKE  MessageBox,0,OFFSET szData,OFFSET szTitle,MB_OK or \
                MB_ICONINFORMATION
        INVOKE  GlobalFree,hMem
        INVOKE  ExitProcess,NULL
;*********************************************************************
END     start

在「命令提示字元」,輸入下面指令以組譯並連結:

E:\HomePage\SOURCE\Win32\HD_Info>ml logical.asm [Enter]
Microsoft (R) Macro Assembler Version 6.14.8444
Copyright (C) Microsoft Corp 1981-1997.  All rights reserved.

 Assembling: logical.asm

***********
ASCII build
***********

Microsoft (R) Incremental Linker Version 5.12.8078
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.

/SUBSYSTEM:WINDOWS
"logical.obj"
"/OUT:logical.exe"

E:\HomePage\SOURCE\Win32\HD_Info>logical [Enter]

執行結果如下: