Ch 32 硬碟 (1) 分割區

現在一顆硬碟容量幾乎都在 80G 以上,這麼大的硬碟管理上是不太容易。小木偶習慣上是把他分成好幾個分割,再裝入好幾種作業系統,如 DOS 6.20、Windows 98 SE、Windows XP、Mandrake Linux 使用。曾經有一度,也安裝過 OS/2 Warp T 3.0 使用過,可惜因軟體太少,所以無奈地再度使用 Windows 作業系統。

每個硬碟最多可以分割成三個主要分割 ( primary partition ) 及一個擴充分割 ( extended partition ),而擴充分割還可以再分成數個邏輯硬碟。以 DOS/Windows 9x/Me 的觀點來看,每個主要分割就是一個邏輯硬碟,再加上擴充分割內的邏輯硬碟,這些邏輯硬碟就是您在 DOS/Windows 所看見的 C:、D:、E:……等這些硬碟機名稱。

例如您把剛剛買來的硬碟裝在電腦上,以 Windows 98 SE 開機光碟開機,執行 FDISK 後,必定會先要求建立一個主要分割,如果您的主要分割並不佔有所有的硬碟容量,FDISK 會建議您建立擴充分割,待擴充分割建立好後,會再要求您建立邏輯硬碟 ( 在擴充分割堛瘍瓡韏w碟也稱為邏輯分割 ),如果您建立好兩個邏輯硬碟,那麼您就有三個邏輯硬碟機,在主要分割的是 C:,另外兩個就是在擴充分割的兩個邏輯硬碟,分別是 D:、E:。因此您雖然只買了一顆實體硬碟,但您卻有 C:、D:、E: 三個邏輯硬碟,

不過用微軟的分割工具,似乎只能建立一個主要分割,這可能是微軟商業上的考慮,所以只有藉助其他的軟體,例如 SPFDISK、OS/2 的啟動管理或 Linux 的分割工具,才可以建立多個主要分割。本章及下一章將要介紹這些分割區與邏輯硬碟在實體硬碟上如何安排。本章先介紹硬碟的構造 ( 包含幾何結構與 LBA 邏輯磁區 ),並實作一個硬碟磁區觀察程式,它可以觀看硬碟的每一部份,下一章再介紹 FAT 檔案系統。


硬碟幾何結構

硬體幾何結構

一顆硬碟的外觀重要的元件有電源插座、IDE 排線、跳針 ( jumper,設定 MASTER 或 SLAVE ),以及一片包含密密麻麻電子元件的電路板,負責控制硬碟運作。而儲存資料的地方則是在硬碟內部的數片含有磁性物質的金屬圓形板上,這些金屬圓形板稱為磁盤 ( platters ),它們是以金屬合金或其他堅硬物質所製成的基板,上面塗上磁性物質用以儲存資料。數片磁盤以一個稱為主承軸的軸心,安裝於一個密閉無塵室內,主承軸上連接電動機,以高速帶動磁盤旋轉。一般一個磁盤有正反兩面,各有一個磁頭負責讀寫,至於要讀寫磁盤那個部份,則是由磁頭傳動組及磁盤轉動到那堥M定。請參閱下圖中的下半圖:

硬碟內部構造

一個磁盤可分為正反兩面,每一面分成許多大大小小的同心圓,每個同心圓都稱為『磁軌』( track ),每個磁軌又可分成許多小的弧形區域,稱為『磁區』( sector )。請參考圖中標有『01(00)』、『02(01)』、『03(02)』……『3F(3E)』等十六進位數字的弧形區域,就是磁區,這些數字表示磁區編號,磁區編號由一開始,這跟一般電腦的習慣不同。而由『01』、『02』、『03』…到『3F』等磁區所組成的一個圓,即為一個磁軌,磁軌由最外面,也就是半徑最大的磁軌開始編號,所以由『01』、『02』、『03』…到『3F』磁區所組成的磁軌稱為第零軌。不同硬碟的一個磁軌所含的磁區數不同,每一面的磁軌數也不相同,上面只是舉個例子方便說明而已。儘管每個硬碟磁區、磁軌數有多有少,但是都是從最外圈磁軌的第一號磁區開始記錄資料的。

() 內綠色的數值是另一種方式的磁區編號,此種編號方式稱為 LBA 邏輯磁區編號。它是由零開始一直到最大磁區,不分磁軌、磁面。

一般而言,硬碟大約由兩個或兩個以上的磁盤組成,當某一面的某個磁軌已記錄滿資料後,還有新資料要寫入時,卻不是寫入同一面的下一個內圈磁軌,而是下一面的相對應的磁軌上。例如上圖中第零軌第零面 ( 也就是編號 01∼3F 或 LBA 00∼3E 磁區所組成的磁軌 ) 已寫滿資料後,若有新資料時,是寫在其背面的磁軌上,即第零軌第一面的磁區上,也就是 LBA 編號 3F∼7D 的磁區上 ( 因為在背面,所以圖中並未表示出來 )。同樣地,若是這一軌也已寫滿,那麼就會寫在下一張磁盤的相對位置上,也就是第零軌第二面的磁區上 ( LBA 編號 BD∼FB 所組成的磁軌上 )。就這樣,每一面磁盤上的磁軌由上往下,形成許多半徑大小不等的假想空心圓柱,稱為磁柱 ( cylinder ),您可以想像磁柱是許多磁軌疊在一起的假想圓柱,就像圖中的上半圖。

您也許會問:『為什麼要這樣安排?為何不一面寫完再換另一面呢?』因為如果這樣安排的話,變成一個磁軌寫完就得花時間移動磁頭到隔壁的內圈磁軌,而磁頭的移動得靠步進電動機的機械移動,要花較多的時間,所以才設計成每磁柱寫完,才移到下一磁柱。

於是我們可以說,硬碟讀寫時,以磁柱 ( cylinder )、磁頭 ( head )、磁區 ( sector ) 這三個參數來表示所讀寫的資料位於硬碟的那一個位置。這就是有名的 C、H、S 表示法。小木偶整理如下:磁柱由最外面開始,由零開始編號一直到最大磁柱。每個磁柱再分為好幾個磁頭,因為磁盤每一面都有一個磁頭負責讀寫,所以以磁頭數代表面,磁頭也是從零開始編號。磁柱上的每一面再細分成數個磁區,磁區是從一開始編號,這點很奇怪,與電腦上的習慣不同。每個磁區都可存入 512 個位元組的資料,這點卻是每顆硬碟都是如此。

而有另一種編號方式,稱之為 LBA 邏輯編號,若以此方式編號,則是以磁區為單位,不分磁柱、磁頭,而由 0 開始,依序由 0、1、2、3……一直到最大磁區為止。LBA 編號與 CHS 之間的換算,下面會提到。

MBR 與分割表

底下要介紹作業系統如何使用硬碟。剛剛買來的硬碟必定要經過分割、格式化之後才能存放資料。而這分割後的資料記載著每個分割區大小、由那個磁柱到那個磁柱,都記錄在一個稱為分割表 ( partition table )的地方,您可以想像分割表是很重要的資料,如果損壞了,大概就找不著您要的資料了。這麼重要的資料,電腦業界規定都存放於硬碟的第零磁柱、第零磁頭、第一磁區,也就是最前面的磁區堙A這個磁區稱為主啟動記錄 ( 即 MBR,master boot record )。在 MBR 堸ㄓF分割表外,還有啟動程式。前面提過,每個磁區都佔 512 個位元組,MBR 磁區中的前 446 個位元組 ( 0∼445 ) 用來存放啟動資料與程式,而接下來的 64 個位元組就是分割表,最後一個字組 ( 2 個位元組 ) 是 AA55H,作為識別之用。

分割表共有 64 個位元組,恰好可以描述四個分割區的性質 ( 即三個主要分割與一個擴充分割 ),所以每個分割用 16 個位元組來表示這個分割區的性質,說明如下:

偏移位址 大小
( 位元組 )
名  稱 說  明
00H 1 啟動狀態 80H 表示可啟動
00H 表示不可啟動
01H 1 起始磁頭  
02H 2 起始磁柱與
起始磁區 
起始磁區佔用 6 個位元,而起始磁柱佔用 10 個位元,分配情形是:

起始磁區: 這兩個位元組中,低位元組的 0∼5 位元為起始磁區
起始磁柱: 這兩個位元組的高位元組為起始磁柱的 0∼7 位元,這兩個位元組中,低位元組的 6∼7 位元為起始磁柱的最高兩個位元 ( 第 8∼9 位元 )
04H 1 檔案系統 06H、0EH:FAT16
0BH、0CH:FAT32
05H、0FH:擴充分割
07H:NTFS 或 OS/2 HPFS
其餘參閱Andries Brouwer 所寫的 Partition types
05H 1 結束磁頭  
06H 2 結束磁柱
與磁區 
同起始磁柱與磁區
08H 4 分割區相
對位置 
距離 MBR 的磁區數,亦即此分割開始的 LBA 磁區編號或隱藏磁區數
0CH 4 磁區數 該分割磁區總數

某些檔案系統,用兩個識別碼來表示,例如 0BH、0CH 都是表示 FAT32,06H、0EH 都表示 FAT16,05H、0FH 都表示擴充分割,這是牽涉到硬碟容量的關係。早期硬碟容量較小,用 C、H、S 來定址磁區沒有問題,但是當硬碟容量超過 8.4G 時,就必須用 LBA 定址磁區。0BH、06H、05H 分別表示 FAT32、FAT16、擴充分割的 C、H、S 版;而 0CH、0EH、0FH 則表示用 LBA 定址磁區。( 參考後面的延伸 INT 13H )

當用 LBA 定址磁區時,分割表內的起始或結束磁柱、磁頭、磁區就沒有用了,必須參考分割區相對位置那一欄的數值,才能找到真正磁區。

擴充分割表

擴充分割內可以分成許多邏輯硬碟,端視硬碟容量的大小,所以無法用固定的長度記錄各個邏輯硬碟的起始位置或大小等等,因此電腦業界的做法是用數個稱為擴充分割表 ( extended partition table ) 的表格記錄這些資料。

擴充分割內的第一個磁區內含第一個擴充分割表,她的格式不僅和分割表相似,而且也都是存放在從該磁區的第 446 個位元組開始,往後算,總共 64 個位元組的資料堙C同樣也分成四等分,但是只用到前面兩份,後面兩份均填入零。第一個 16 位元組堶惟珧O載的資料是指這個邏輯硬碟的資料,而它的起始磁柱、磁頭、磁區,結束磁柱、磁頭、磁區或是分割區相對位置都是相對於擴充分割表所在位置,並非實體硬碟的 MBR。第二個 16 位元組堜珧O載的資料則是指下一個擴充分割表的資料,同樣的,其起始磁柱、磁頭、磁區,結束磁柱、磁頭、磁區或是分割區相對位置也都是相對於前一個擴充分割表所在位置。

像這樣,就形成一個鏈狀的擴充分割表,每個擴充分割表的第一個 16 位元組表示這個邏輯硬碟的資料,而第二個 16 位元組則表示下一個擴充分割表的資料。但是,如果是最後一個邏輯硬碟的話,那麼只用到一份,後面三份均填入零,因此只要是第二份全部為零,系統就知道已經是最後一個邏輯硬碟了。


INT 13 BIOS 服務程式

INT 13H 是 BIOS 提供的服務程式,它是用來提供軟碟、硬碟服務之用,例如格式化、讀取等等…,早期也有許多人利用 INT 13H 格式化不正常磁區/磁軌達到保護的目的。

INT 13H/AH=02H:讀取磁區

早期硬碟不大,是以 AH=02/INT 13H 讀取硬碟上磁區的資料。AL 表示想要讀取的磁區數,不可為零。DL 表示想要讀取的硬碟機名稱,如果是軟碟,0 表示 A:,1 表示 B:;如果是硬碟,則用 80H 代表第一個實體硬碟,81H 代表第二個實體硬碟。ES:BX 指向讀取後的資料存於記憶體何處。DH 是指所要讀取的磁頭編號,CX 表示磁柱及磁區編號,CL 較低的 6 個位元,即 0∼5 位元,表示所要讀取的磁區編號,而較高的兩個位元以及 CH 的八個位元,合起來共 10 個位元表示磁柱編號 ( CH 的八個位元是磁柱編號的較低位元,CL 的 6、7 兩個位元是磁柱編號的較高位元 )。請參考下圖:

暫存器與CHS參數

由上圖可知,用 10 個位元表示磁柱編號,最多可以有 1024 種編號 ( 210=1024 ),每個編號代表一磁柱,這編號由 0∼1023 共 1024 個磁柱。同理,磁頭最多可以有 0∼127 共 128 種;而磁區使用 6 個位元,所以有 64 種選擇,即 0∼63,但是磁區都由編號一開始,所以磁區最多只有 1∼63 個,即 63 個磁區。綜合以上說明︰磁柱最多為 1024 個,磁頭最多為 256 個,磁區最多為 63 個,而每磁區都固定有 512 個位元組,因此一顆硬碟最多只能容納︰

1024*256*63*512=8455716864

個位元組,亦即 8.4 GB ( 8.4GB 是以 1G=109來算,假如依電腦二進位算法,應該是 8455716864÷1024÷1024÷1024=8,即 8GB )。當然到了現在 ( 民國 92 年左右之後 ) 硬碟已經超過此容量了,所以 IBM 與微軟共同製定新的 INT 13H Extension 中斷服務程式 ( 延伸 INT 13H ),以解決此一問題。

這個新的延伸 INT 13H 改用 LBA 邏輯定址法 ( Logical Block Addressing ),不再用 CHS 參數來指定讀取的磁區,而用線性磁區編號指定磁區。舊的 INT 13H 資料甚多且已過時,所以小木偶就不做介紹,此章為了配合大硬碟,所以介紹了延伸 INT 13H 的幾個功能,其餘可查閱 Ralf Brown's Interrupt List

延伸 INT 13H/AH=41H:INSTALLATION CHECK

首先要介紹的是檢查 BIOS 是否支援延伸 INT 13H,呼叫前 AH=41H,BX=55AAH 表示檢查是否支援延伸 INT 13H,DL 表示硬碟機編號,80H 表示第一部實體硬碟機,81H 表示第二部實體硬碟機。返回時,若進位旗標被設定,表示不支援;若進位旗標被清除,表示支援延伸 INT 13H,AH 還表示延伸 INT 13H 的版本。

延伸 INT 13H/AH=42H:延伸讀取

當 AH=42H 時的功能是延伸讀取,使用此功能時,DL 是硬碟編號,和上面一樣,從 80H 開始,DS:DI 指向一個稱為 disk address packet 的結構體,此結構體內容為︰

disk_address_packet     STRUC
size_of_packet          db      ?       ;此結構體大小
reserved                db      ?       ;保留,必設為 0
number_of_block         dw      ?       ;要讀取的磁區數
buffer_offset           dw      ?       ;讀取磁區後,資料存放偏移位址
buffer_segment          dw      ?       ;資料存放的段位址
LBA_number              dq      ?       ;指定要讀取的磁區編號
flat_address            dq      ?       ;指定平坦模式時,存放位址
disk_address_packet     ENDS

在呼叫 AH=42H 功能之前,必須先設定好 disk address packet 結構體內的各個欄位。size_of_packet 欄位是結構體大小,其數值不是 10H 就是 18H,端視 buffer_offset、buffer_segment 是否可用。在 16 位元作業系統中這兩個欄位就是一般所慣用的 Segment:Offset 定址方式,此時不須用到最後一個欄位,所以 size_of_packet 為 10H 個位元組;但如果在平坦模式之下使用,則 buffer_offset、buffer_segment 這兩個欄位都是 0FFFFH,而此時位址須填入最後一欄的 flat_address 內,故 size_of_packet 為 18H 個位元組。LBA_number 則是要讀取的磁區編號,這個磁區編號也就是 LBA 邏輯磁區編號,從 0 開始一直到硬碟的最大磁區,其計算方式為

( 磁柱編號*每磁柱的磁頭數 + 磁頭編號 ) * 每軌磁區數 + 磁區編號 - 1

如果返回時,若進位旗標被清除,表示讀取成功;若進位旗標被設定,表示讀取錯誤,此時 disk_address_packet 結構體的 number_of_block 會被 BIOS 設定為實際讀取的磁區數。

延伸 INT 13H/AH=48H:獲得實體硬碟的資料參數

要取得一顆實體硬碟的資料參數,可用 AH=48H 功能,DL 表示要取得資料的硬碟編號,DS:SI 指向取得資料後的資料填放位址,此位址存放一個稱為 drive parameter 的結構體。drive_parameter 結構體欄位如下:

drive_parameter         STRUC
size_of_drive_parameter dw      ?       ;drive parameter 大小
information_flags       dw      ?       ;
cylinder                dd      ?       ;磁柱總數
head                    dd      ?       ;磁頭數
sector_per_track        dd      ?       ;每軌磁區數
total_sectors           dq      ?       ;磁區總數
byte_per_sector         dw      ?       ;每磁區所含位元組數
drive_parameter         ENDS

在呼叫 AH=48H 功能之前,除了必須先指定 DL、DS:SI 之外,還得先設定 drive_parameter 結構體的 size_of_drive_parameter 欄位,這個欄位的數值表示 drive_parameter 結構體的大小,可以有 1AH、1EH、42H 三種選擇,視您想獲得的資料詳盡程度與延伸 INT 13H 的版本決定,一般而言,1AH 大約就夠用了。假如呼叫成功,進位旗標會被清除,且 AH=0,DS:SI 所指的 drive parameter 結構體會填上適當的數值;若呼叫失敗,進位旗標會被設定,AH 內會被填入錯誤代碼。


寫個程式,可讀取硬碟中任意磁區

既然瞭解了硬碟構造,磁柱、磁頭、磁區劃分,以及用延伸 INT 13H 讀取磁區的方法,不寫自行個程式來觀察硬碟中藏了那些內容,就有點兒對不起自己的大腦。底下這個程式稱為 HDSV2 ( Hard Disk Sector Viewer 第 0.30 版 ),執行後畫面如下:

HDSV 執行畫面
上圖中,是小木偶的第零顆硬碟的 MBR 磁區的第 100H∼1FFH,注意到 1BEH∼1FDH ( 紫色框起來的部份 ) 這 64 個位元組就是分割表,接著分割表後的兩個位元組就是 0AA55H 識別碼。

HDSV2.COM 使用延伸 INT 13H,現今電腦的 BIOS 幾乎都支援了,所以在 Win 9x/Me 或 DOS 系統都應該可以使用,但是在 Win 2K/XP 系統堙A禁止低階呼叫,所以無法使用。( 小木偶在 DOS 6.20 及 Win 98SE 使用過,沒有問題,在 DOS 中得先執行中文系統,否則看不見中文字;但在 Win XP 堛瑤T無法使用 )

HDSV2 原始碼

HDSV2.ASM 會用到一些結構體,這些結構體不隨程式改變,也可加入 MYASMINC.INC 內:

hdi             struc
hd_info_size    dw      ?       ;硬碟資訊結構體大小
hd_info_flag    dw      ?       ;information flags
total_cylinder  dd      ?       ;磁柱總數
head_p_cylinder dd      ?       ;磁頭數
sector_p_track  dd      ?       ;每軌所含磁區數
total_sector    dq      ?       ;磁區總數
byte_p_sector   dw      ?       ;每磁區所含位元組數
hdi             ends

hdpkt           struc           ;Hard Disk Packet
size_hdpkt      db      ?       ;讀取/寫入控制區塊大小
reserved        db      ?       ;保留,必須為零
sector_to_trans dw      ?       ;讀取/寫入磁區數
buffer_offset   dw      ?       ;讀取/寫入時的記憶體偏移位址
buffer_segment  dw      ?       ;讀取/寫入時的記憶體區段位址
sector_sn_l     dd      ?       ;欲讀取/寫入的起始磁區編號,低雙字組
sector_sn_h     dd      ?       ;欲讀取/寫入的起始磁區編號,高雙字組
hdpkt           ends

chs             struc
cylinder        dd      ?
head            dd      ?
sector          dd      ?
chs             ends

下面是 HDSV2.ASM 的內容:

;硬碟中任意磁區觀察程式。
;利用堆疊傳遞參數呼叫副程式。
;組譯方法:     ML HDSV2.ASM
;得到 HDSV2.COM,總長度共 1902 位元組

                .model  tiny,stdcall
                .386

include         myasminc.inc
includelib      myasmlib.lib

input_color     equ     yellow_on_black
normal_color    equ     lime_on_black
title_color     equ     yellow_on_navy
software_color  equ     white_on_black
driver_color    equ     aqua_on_black 

;***********************************************************
code    segment para    public  'code'  use16
        assume  cs:code,ds:code
        org     100h
start:  jmp     begin
;-----------------------------------------------------------
msg00   db      '讀取錯誤$'
msg01   db      'Hard Disk Sector Viewer v.0.30$'
;HDSV 0.10 版是把所有副程式都包含在一個 HDSV.ASM 原始碼中,且以暫存器傳遞參數
;HDSV 0.20 版是把一些副程式放在 MYASMLIB.LIB 堙A組譯成 HDSV1.COM。
msg02   db      'Esc=離開   D=硬碟機   M=MBR   ',18h,19h,'=顯示磁區前/後半個磁區$'
msg03   db      '實體硬碟:0  磁柱:000000  磁頭:00  磁區:00   LBA 磁區:00000000$'
msg04   db      ' 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F   '
        db      '0123456789ABCDEF$'
msg05   db      '沒有實體硬碟$'
msg06   db      'C=磁柱   H=磁頭   S=磁區   L=LBA 磁區   PageUp/PageDn=上/下一磁區'
msg07   db      '$'
msg08   db      '輸入磁頭(00∼00):$'
msg09   db      '輸入磁柱(00∼000000):$'
msg0a   db      '輸入磁區(01∼00):$'
msg0b   db      '輸入實體硬碟﹙0∼0):$'
msg0c   db      '輸入 LBA 編號﹙00∼00000000):$'      ;055
n_hd            db      ?               ;實體硬碟機數目
harddisk        db      80h             ;第零台實體硬碟機
hd_info         hdi     <1ah,?,?,?,?,?,?>       ;磁碟機資訊,含總磁柱、磁軌、磁區
hd_read_pkt     hdpkt   <10h,0,1,?,?,?,?>       ;讀取硬碟時的參數
chs_sn          chs     <0,0,1>                 ;正顯示於螢幕上的 CHS
;-----------------------------------------------------------
al_to_ascii     proc    near
        push    ax      ;處理 AL 時,先處理較高的四個位元,故也先
        shr     al,4    ;把較低的四個位元存入堆疊。然後較把高的 4
        call    ax2as1  ;個位元移到較低的四個位元。
        pop     ax      ;把這四個位元換成 ASCII 碼的數字或 'A'∼'F'
        and     al,0fh  ;由堆疊取回 AL 的較低四位元,依同樣步驟處理
ax2as1: add     al,'0'  ;這四個位元
        cmp     al,'9'
        jbe     ax2as2
        add     al,7
ax2as2: stosb
        ret
al_to_ascii     endp
;-----------------------------------------------------------
;把 AX 內的十六進位數變成 ASCII 字元,存於 DI 所指位址
ax_to_ascii     proc    near
        push    ax      ;把 AX 拆成 AH、AL,兩等分,先處理 AH,再
        mov     al,ah   ;處理 AL,故先把 AL 存入堆疊,令 AL=AH
        call    al_to_ascii
        pop     ax      ;由堆疊取回 AL
        call    al_to_ascii
        ret
ax_to_ascii     endp
;-----------------------------------------------------------
;印出一頁,共 256 個位元組的磁區資料,即半個磁區的資料
;輸入:SI=磁區資料開始位址
;      BX=印出那一半的磁區資料,0 表示前半部,100H 表示後半部
;輸出:BX=100H-表示前半部;200H-表示後半部
print_sector_data       proc    near
;在『實體硬碟:0  磁柱:000000  磁頭:00  磁區:00……』字串填上
;適當的實體硬碟、磁柱、磁頭、磁區、LBA 磁區編號
        mov     al,harddisk
        mov     di,offset msg03+10
        sub     al,50h          ;填入實體硬碟
        stosb
        add     di,8
        mov     eax,chs_sn.cylinder
        push    ax
        shr     eax,16
        call    al_to_ascii     ;填入磁柱較高四位數
        pop     ax
        call    ax_to_ascii     ;填入磁柱較低四位數
        add     di,8
        mov     eax,chs_sn.head
        call    al_to_ascii     ;填入磁頭
        add     di,8
        mov     eax,chs_sn.sector
        call    al_to_ascii     ;填入磁區

        add     di,13
        mov     eax,hd_read_pkt.sector_sn_l
        push    ax
        shr     eax,16
        call    ax_to_ascii     ;填入較高的四位 LBA 磁區編號
        pop     ax
        call    ax_to_ascii     ;填入較低的四位 LBA 磁區編號
        invoke  print_string,offset msg03,driver_color,500h
        invoke  print_string,offset msg04,title_color,609h

;印出每一列,總共要印出 16 列。每列可分為三部分:偏移位址、位元組資料、ASCII 字元
        mov     dh,7h   ;DH=第幾列
        add     si,bx   ;SI=要印出的位元組資料位址

nxt_r:  mov     dl,3h   ;DL=第幾行,每列都從第 3 行開始
        mov     ax,bx
        invoke  print_ax,title_color,dx ;印出偏移位址
        inc     dl
        inc     dl

nxt_by: lodsb
        invoke  print_al,normal_color,dx;印出此列的一個位元組資料
        mov     ax,si
        mov     cl,' '
        and     al,0fh  ;若位址的較低 8 位元均為零,表示此列已結束
        jz      pt_asc  ;下步應印出 ASCII 字元
        cmp     al,8    ;若此列已經印出 8 個位元組時
        jne     pt_chr  ;必須印出 '-',否則印出空白
        mov     cl,'-'
pt_chr: invoke  print_char,cl,normal_color,dx
        jmp     nxt_by

;印出 ASCII 字元
pt_asc: mov     cx,10h  ;每列 16 個 ASCII 字元 
        add     dl,3    ;間隔位元組資料三行
        sub     si,cx   ;使位址指向此列開始處
nxt_pt: lodsb
        invoke  print_char,al,normal_color,dx
        loop    nxt_pt
        inc     dh
        add     bx,10h
        cmp     dh,17h  ;若印到螢幕第 17H 行,表示整頁已顯示完畢
        jne     nxt_r

        ret
print_sector_data       endp
;-----------------------------------------------------------
;轉換硬碟的磁柱、磁頭、磁區變成實體硬碟的 LBA 磁區編號
;公式:LBA 磁區編號= ( 磁柱 * 磁頭總數 + 磁頭 )* 每軌磁區數 + 磁區 - 1
;輸入:chs_sn
;輸出:EDX:EAX-LBA 磁區編號
;      hd_read_pkt.sector_sn_h:hd_read_pkt.sector_sn_l
get_lba_sector  proc    near
        sub     edx,edx
        mov     ecx,edx
        mov     eax,chs_sn.cylinder
        mul     hd_info.head_p_cylinder
        add     eax,chs_sn.head
        adc     edx,ecx
        mul     hd_info.sector_p_track
        add     eax,chs_sn.sector
        adc     edx,ecx
        dec     eax
        sbb     edx,ecx
        mov     hd_read_pkt.sector_sn_l,eax
        mov     hd_read_pkt.sector_sn_h,edx
        ret
get_lba_sector  endp
;-----------------------------------------------------------
;當輸入完成後,清除螢幕上『輸入…』字串
clear_message   proc    near
        invoke  print_string,offset msg07,normal_color,1700h
        ret
clear_message   endp
;-----------------------------------------------------------
;變更實體硬碟機編號
;輸出:CY-使用者取消輸入
;      NC-使用者正確輸入,chs_sn 及 
;          hd_rd.sector_sn_h:hd_rd.sector_sn_l 內容會指向 MBR
change_hd       proc    near
        mov     al,n_hd
        mov     di,offset msg0b+11h
        add     al,30h  ;計算最大實體硬碟機編號
        dec     al      ;AL=最大實體硬碟機編號
        stosb
        invoke  print_string,offset msg0b,input_color,1700h
        invoke  input,1,10h,input_color,dx
        jc      ch0     ;若 CY,表示使用者按下 Esc 鍵
        cmp     al,n_hd ;使用者輸入之實體硬碟標號是否超過範圍
        jae     change_hd       ;若超過範圍,則跳到 change_hd:
        add     al,80h          ;若沒超過範圍,則改變實體硬碟機編號
        mov     harddisk,al
        sub     eax,eax
        mov     chs_sn.cylinder,eax
        mov     chs_sn.head,eax ;變更硬碟機後,可能因磁柱編號或容量較小
        inc     eax             ;造成讀取錯誤,故 chs_sn 自動換成 MBR
        mov     chs_sn.sector,eax
        clc
ch0:    ret
change_hd       endp
;-----------------------------------------------------------
;變更磁柱
;輸出:CY-使用者取消輸入
;      NC-使用者正確輸入,chs_sn 及 
;          hd_rd.sector_sn_h:hd_rd.sector_sn_l 內容會隨之變更
change_cylinder proc    near
        mov     eax,hd_info.total_cylinder
        dec     eax
        push    ax
        mov     di,offset msg09+14
        shr     eax,16
        call    al_to_ascii
        pop     ax
        call    ax_to_ascii
        invoke  print_string,offset msg09,input_color,1700h
        invoke  input,8,10h,input_color,dx
        jc      cc0
        cmp     eax,hd_info.total_cylinder
        jae     change_cylinder
        mov     chs_sn.cylinder,eax
        clc
cc0:    ret
change_cylinder endp
;-----------------------------------------------------------
;變更磁頭
;輸出:CY-使用者取消輸入
;      NC-使用者正確輸入,chs_sn 及 
;          hd_rd.sector_sn_h:hd_rd.sector_sn_l 內容會隨之變更
change_head     proc    near
        mov     eax,hd_info.head_p_cylinder
        mov     di,offset msg08+14
        dec     ax
        call    al_to_ascii
        invoke  print_string,offset msg08,input_color,1700h
        invoke  input,2,10h,input_color,dx
        jc      ch0
        cmp     eax,hd_info.head_p_cylinder
        jae     change_head
        mov     chs_sn.head,eax
        clc
ch0:    ret
change_head     endp
;-----------------------------------------------------------
;變更磁區
;輸出:CY-使用者取消輸入
;      NC-使用者正確輸入,chs_sn 及 
;          hd_rd.sector_sn_h:hd_rd.sector_sn_l 內容會隨之變更
change_sector   proc    near
        mov     di,offset msg0a+14
        mov     eax,hd_info.sector_p_track
        call    al_to_ascii
        invoke  print_string,offset msg0a,input_color,1700h
        invoke  input,2,10h,input_color,dx
        jc      cs0
        or      eax,eax
        jz      change_sector
        cmp     eax,hd_info.sector_p_track
        ja      change_sector
        mov     chs_sn.sector,eax
        clc
cs0:    ret
change_sector   endp
;-----------------------------------------------------------
;變更 LBA 磁區編號
;輸出:CY-使用者取消輸入
;      NC-使用者正確輸入,chs_sn 及 
;          hd_rd.sector_sn_h:hd_rd.sector_sn_l 內容會隨之變更
change_lba      proc    near
        mov     si,offset hd_info.total_sector
        lodsd
        dec     eax
        mov     ecx,eax
        shr     eax,16
        mov     di,offset msg0c+19
        call    ax_to_ascii
        mov     eax,ecx
        call    ax_to_ascii
        invoke  print_string,offset msg0c,input_color,1700h
        invoke  input,8,10h,input_color,dx
        jc      cl0
        cmp     eax,dword ptr hd_info.total_sector
        jae     change_lba
        mov     hd_read_pkt.sector_sn_l,eax

;轉換 LBA 磁區編號 ( 存於 EDX ) 變成 CHS 參數 ( 存於 chs_sn )
;公式:LBA 磁區編號= ( 磁柱 * 磁頭總數 + 磁頭 )* 每軌磁區數 + 磁區 - 1
        sub     edx,edx
        div     hd_info.sector_p_track
        inc	edx
        mov     chs_sn.sector,edx
        sub     edx,edx
        div     hd_info.head_p_cylinder
        mov     chs_sn.head,edx
        mov     chs_sn.cylinder,eax
        clc
cl0:    ret
change_lba      endp
;-----------------------------------------------------------
;於 40:75 處取得實體硬碟機個數
begin:  mov     bx,40h
        mov     es,bx
        mov     si,75h
        mov     bl,es:[si]
        mov     si,offset msg05 ;若無實體硬碟,則跳到 exit1 處
        cmp     bl,0
        jz      exit1

        mov     ax,cs
        mov     es,ax
        mov     n_hd,bl
        mov     ax,600h         ;清除螢幕
        mov     bh,normal_color
        sub     cx,cx
        mov     dx,184fh
        int     10h

;印出軟體名、按鍵說明
        invoke  print_string,offset msg01,software_color,24
        invoke  print_string,offset msg02,normal_color,200h
        invoke  print_string,offset msg06,normal_color,300h

;計算程式結束位址,修改作為讀取磁區資料後的存放位址
        sub     eax,eax
        mov     hd_read_pkt.buffer_offset,900h
        mov     hd_read_pkt.buffer_segment,ds
        mov     hd_read_pkt.sector_sn_l,eax
        mov     hd_read_pkt.sector_sn_h,eax
        cld

;讀取實體硬碟資料,並存於 hd_info 結構體
read0:  mov     dl,harddisk
        mov     ah,48h
        mov     si,offset hd_info
        int     13h
        jnc     read1

exit0:  mov     si,offset msg00
exit1:  invoke  print_string,si,normal_color,1700h
exit2:  mov     ax,4c00h
        int     21h

;依據 hd_read_pkt 內的 LBA 邏輯磁區編號,讀取實體硬碟上一個磁區的資料
read1:  mov     dl,harddisk
        mov     ah,42h
        mov     si,offset hd_read_pkt
        int     13h
        jc      exit0

;在螢幕上顯示 256 個位元組的資料
psd0:   sub     ebx,ebx
psd1:   mov     si,hd_read_pkt.buffer_offset
        call    print_sector_data

;等待使用者輸入按鍵
;注意!BX 內之值必須保存,因為程式以 DS:hd_read_pkt.buffer_offset[BX]
;指向磁區資料位址,除了會改變 LBA 邏輯磁區編號的功能,例如改變磁柱、磁頭等
;必須重新讀取整個磁區外,可不必保存 BX,其餘功能例如上/下鍵須保存 BX。
getkey: call    clear_message   ;清除『輸入…』字串
        sub     ecx,ecx
        mov     ah,ch
        int     16h
        cmp     ah,01
        je      exit2
        cmp     ah,51h
        je      pagedn
        cmp     ah,49h
        je      pageup
        cmp     ah,20h
        je      d_key
        cmp     ah,32h
        je      m_key
        cmp     ah,2eh
        je      c_key
        cmp     ah,23h
        je      h_key
        cmp     ah,1fh
        je      s_key
        cmp     ah,26h
        je      l_key
        cmp     ah,48h
        je      up_key
        cmp     ah,50h
        jne     getkey

;若 BX=100H,表示剛剛已顯示 0∼0FFH 的資料,此刻若使用者按下向上/下鍵,表示
;將顯示 100H∼1FFH 的資料,而 BX 已指向 100H,故直接顯示即可。若 BX=200H,
;則表示已顯示後 100H 的資料,此時使用者再按下向上/下鍵,BX 應從零開始顯示
up_key: cmp     bx,100h
        jz      psd1
        jmp     psd0

;按下 PageDown 鍵時,先檢查 chs_sn.sector 是否已到每磁頭的最後一磁區,若不是
;的話將其加一就完成了,若是的話要考慮磁區『進位』成下一磁頭等問題
pagedn: mov     edx,chs_sn.sector
        cmp     edx,hd_info.sector_p_track
        jb      pgdn3                           ;若不是的話,跳到 pgdn3
        mov     edx,chs_sn.head                 ;若已經是最後一磁區時,則再檢
        mov     eax,hd_info.head_p_cylinder     ;檢查 chs_sn.head 是否到每磁
        inc     edx                             ;柱的最後一磁頭
        dec     eax
        cmp     edx,eax
        ja      pgdn2           ;若已經是最後一磁頭的話,跳到 pgdn2 否則
        inc     chs_sn.head     ;chs_sn.head 增一,且令 chs_sn.sector=1
pgdn1:  mov     chs_sn.sector,1
        jmp     short pgdn4
pgdn2:  mov     edx,chs_sn.cylinder
        mov     eax,hd_info.total_cylinder
        inc     edx
        dec     eax             ;檢查 chs_sn.cylinder 是否
        cmp     edx,eax         ;已到最後一磁柱
        ja      getkey          ;若已經到最後一磁柱,則跳到 getkey
        mov     chs_sn.cylinder,edx     ;若不是最後磁柱,則 chs_sn.cylinder
        mov     chs_sn.head,ecx         ;增加一、chs_sn.head 變為零,且令
        jmp     pgdn1                   ;chs_sn.sector 等於 1
pgdn3:  inc     chs_sn.sector
pgdn4:  call    get_lba_sector  ;依據新輸入的 chs_sn,求出 LBA 邏輯
        jmp     read1           ;磁區編號,再讀取該邏輯磁區

;按下 PageUp 鍵時,先檢查 chs_sn.sector 是否為一,若不為一,則將其減一就完成了
;若為一,則必須到上一磁頭,要考慮『借位』的問題
pageup: cmp     chs_sn.sector,1
        ja      pgup1
        cmp     chs_sn.head,ecx ;若為一,則繼續檢查 chs_sn.head 是否為零,
        ja      pgup2           ;若不為零,則跳到 pgdn2
        cmp     chs_sn.cylinder,ecx     ;若為零,則檢查 chs_sn.cylinder 是否為
        je      getkey                  ;零,若為零,表示已到 MBR,故則不動作
        mov     edx,hd_info.head_p_cylinder
        dec     chs_sn.cylinder ;若不為零,則使 chs_sn.cylinder 減一、
        dec     edx             ;chs_sn.head 變為最大磁頭編號、chs_sn.sector
        mov     chs_sn.head,edx ;變為最大磁區編號
        jmp     short pgup3
pgup1:  dec     chs_sn.sector
        jmp     pgdn4
pgup2:  dec     chs_sn.head
pgup3:  mov     edx,hd_info.sector_p_track
        mov     chs_sn.sector,edx
        jmp     pgdn4

m_key:  mov     chs_sn.cylinder,ecx
        mov     chs_sn.head,ecx
        inc     ecx
        mov     chs_sn.sector,ecx
        jmp     pgdn4

c_key:  call    change_cylinder
c_key1: jc      getkey          ;若 CY,表示使用者取消輸入
        jmp     pgdn4

h_key:  call    change_head
        jmp     c_key1

s_key:  call    change_sector
        jmp     c_key1

l_key:  call    change_lba
        jmp     c_key1

d_key:  call    change_hd
        jc      getkey
        call    get_lba_sector
        jmp     read0
;-----------------------------------------------------------
code    ends
;***********************************************************
        end     start

這個程式雖然約四百六十行,但是邏輯上非常簡單,沒什麼複雜的演算法。小木偶僅就幾個重點說明。

程式架構

執行 HDSV2 後,CPU 由程式 start: 標記開始執行。首先會做必要的檢查,例如實體硬碟個數,然後印出按鍵的使用說明,接下來讀取第零顆實體硬碟的 MBR 磁區,顯示此磁區的前 256 個位元組於螢幕上,然後呼叫 INT 16H/AH=00H 等待使用者輸入指令。

HDSV2.COM 所能用的指令不多,大致分成三種:

  1. 只有一個指令,結束 HDSV2 程式。
  2. 按向上或向下鍵,切換顯示前或後 256 個位元組的資料。雖然 HDSV2 每次只讀取一個磁區的資料,但是一個磁區有 512 個位元組,一個螢幕容納不下,所以每次只能顯示 256 個位元組的資料。小木偶設計以向上/下鍵切換顯示前或後 256 個位元組。至於切換的關鍵是在呼叫完 print_sector_data 後,BX 傳回 100H 或 200H。見程式註解。
  3. 改變顯示那一個磁區,包含六種功能。當使用者按下 M、D、PageUp/PageDwon、C、H、S,分別表示直接顯示 MBR、變更實體硬碟、顯示前一或後一個磁區、變更磁柱、磁頭、磁區,以及改變 LBA 磁區編號。

呼叫 INT 16H/AH=00H 取得使用者按鍵輸入指令後,判斷使用者按了什麼鍵,然後依據按鍵執行使用者的意志跳到相對的功能去執行。

取得實體硬碟個數

在 IBM PC 及其相容機種,絕對位址 0040:0000 開始的一塊約 250 個位元組的記憶體中,存有 BIOS 會使用到的一些資料,一般稱為 BIOS 資料區 ( BIOS data area )。這塊區域的資料包含 RS232 埠位址、LTP 埠位址等等,而 0040:0075 處的一個位元組,表示實體硬碟個數。

磁區資料存放處

在原始碼 begin: 標記之後不遠處,有一紅色十六進位數值,900H,它是 HDSV2.COM 所讀入的磁區資料存放處。HDSV2.COM 每次僅讀取一個磁區,一個磁區有 512 個位元組,算是很大的資料,本來也可以放在位址 100H 之後與其他資料放在一起。但這樣的話,會使執行檔瞬間膨脹 512 個位元組,並非好的做法。最好的做法是向系統配置一塊記憶體,但是要有較多的背景知識。此處小木偶採用另一種做法,把它放在程式碼後面。

眾所周知,*.COM 檔有 64KB 大小,而 HDSV2.COM 僅佔 1902 位元組 ( 不到 2KB,嘿嘿,這便是組合語言的好處了 ),所以在這一區段中,其實就有許多空間是浪費了。因此小木偶把它放在程式碼之後,一般情形是不會造成問題。

但是在撰寫程式時並不知道程式有多大,要如何知道設為 900H?其實這個問題很簡單,只要在撰寫時先任意假設一值,待組譯成功之後就知道程式有多大了。但要注意,COM 執行檔載入記憶體時,是放在由位址 100H 開始,因此還得加上 100H 才行,因此為 1902D+100H=86EH,然後小木偶再取個整數變成 900H。當然您放在 64KB 內程式碼之後的任一處也行,但不要太後面,以免覆蓋了堆疊。

chs_sn 與 hd_read_pkt 結構體

chs_sn 結構體是定義現在在螢幕上所顯示的磁柱、磁頭、磁區編號,這是為了使用者按下 PageDown、PageUp、M 鍵時能夠很方便的跳到上一個、下一個、MBR 磁區,或者使用者按下 C、H、S 鍵時也能很快的修改磁柱、磁頭、磁區編號。例如使用者按下 PageDown 鍵時,先檢查是否已經是每磁頭的最後一磁區了,如果不是的話只要 chs_sn.sector 增加一就可以了;如果已經是每磁頭的最後一磁區的話,就會由磁區『進位』一到磁頭編號,同時磁區編號變成一,同樣的,也較檢查磁頭編號是不是已經是最大了,如果是,那麼也要『進位』到磁柱。

hd_read_pkt 是配合延伸 INT 13H/AH=42H 讀取磁區功能的結構體。如前所述,延伸讀取或寫入功能,已經不用磁柱、磁頭、磁區這三個參數定位磁區了,改用 LBA 邏輯磁區編號,這 LBA 邏輯磁區編號就放在 hd_read_pkt 結構體的 sector_sn_l 和 sector_sn_h 欄位堙CLBA 邏輯磁區編號共有 64 位元,但是 80386∼Pentium IV 只有 32 位元暫存器可供使用,其指令集也只能移動 32 位元的資料,所以小木偶把它分為較低的 32 位元,sector_sn_l,和較高的 32 位元,sector_sn_h,兩欄。

get_lba_sector 副程式

在邏輯上 CHS 參數必須和表示 LBA 邏輯磁區編號的 hd_read_pkt.sector_sn_l、hd_read_pkt.sector_sn_h 互相配合,我的意思是只要 chs_sn 內的任何一欄改變了,sector_sn_l 和 sector_sn_h 也要隨之變動,這個動作由呼叫 get_lba_sector 副程式完成。換句話說,get_lba_sector 副程式的工作就是把 chs_sn 轉換成 LBA 磁區編號。

使用者按下 PageUp、PageDown、M、C、H、S 等六鍵的前三鍵時,HDSV2.COM 會自行算出適當磁柱、磁頭或磁區編號;而使用者按下後三鍵時,HDSV2.COM 會出現訊息要求使用者輸入適當磁柱、磁頭或磁區編號,不論前三鍵或後三鍵,完成後 HDSV2.COM 都會改變 chs_sn,這時當然也一定要使 hd_read_pkt 內的 sector_sn_l 和 sector_sn_h 保持和 chs_sn 一致才行,所以這些功能最後都會呼叫 get_lba_sector 副程式,也就是在改變磁柱功能的 pgdn4: 標記處。

照理來說,這六個功能都應該呼叫 get_lba_sector 副程式,不過我們在原始碼中只需寫一次即可,其餘五次可由跳躍指令,跳到 pagn4: 處執行,這是為了縮小程式碼的緣故。不僅如此,HDSV2.COM 還要清除要求使用者輸入的訊息,連這個也可以節省一些。這便是用組合語言的好處。

al_to_ascii 副程式

最後提一提 al_to_ascii 副程式所用的技巧。al_to_ascii 副程式是用來把 AL 內的十六進位數變成 ASCII 字元,存於 DI 所指位址。因為十六進位每位數必須用 4 位元表示,AL 共有 8 位元長,故需要 2 個位元組存放 ( ASCII 碼每個字元佔用 8 位元 )。例如 AL=1B,DI=0200 的話,執行完這個副程式,記憶體 ES:0200 處應該會變成

ES:0200  42 31

您可查查 ASCII 表,十六進位的 31H 兩數值代表『1』這個阿拉伯數字,42H 代表英文字母『B』。因為 AL 暫存器中 1 是較高的四位元,B 是較低的八位元,故填到記憶體中時,由低位址到高位址依次填入『B』、『1』。

處理 AL 這 8 個位元,每四個位元都必須做同樣的事,就是先加上 30H,再檢查是否超過 39H,若超過表示是英文字母,還要再加上 7H。這種重複的事情,應該寫個副程式來做,但是您可以發現這個副程式就是從 ax2as1: 標記到 ax2as2: 標記處,竟然是直接『嵌』在 al_to_ascii 副程式堙A這樣做是被允許的,因為實際上,副程式的進入點也是一種標記。這樣做可減少原始碼長度。


分割區

既已有了觀察磁區的利器,HDSV2.COM,我們就來觀察硬碟中每個分割區之間的關係。首先,我先大致介紹在 FAT16/32 分割區埵釣漕ン垠n的部份,下一章再詳細說明。說完 FAT16/32 分割區的概略部份之後,再用 HDSV2.COM 觀察小木偶的一顆 80GB 硬碟中,分割區之間如何劃分。

分割區的結構

在硬碟上的 FAT16 與 FAT32 分割區和軟碟結構有類似之處,都有一個稱為啟動磁區的特殊磁區,但不同的地方是軟碟片上的啟動磁區必定在邏輯編號第零磁區,而硬碟上的啟動磁區卻不是在第零號 LBA 邏輯磁區。

前面提過,硬碟的第零號 LBA 邏輯磁區是 MBR,那麼硬碟上的啟動磁區在那堜O?這個問題應該這樣回答, 硬碟上可以有數個分割區,假如是 FAT16 或 FAT32 分割區的話,每個分割區依序可分為四部份:保留磁區、FAT、根目錄以及資料磁區四部份,而啟動磁區必定在保留磁區的第一個磁區。第一個主要分割之前,一般會保留若干磁區,這些磁區就是第零磁柱上的第零面所有磁區,這些磁區的第一個磁區就是 MBR,其他被保留的磁區一般是給啟動管理程式或是作為特殊用途 ( 病毒也常利用這塊區域為非作歹 )。這些磁區一般使用 DOS 中斷服務程式是無法見到的,故稱為隱藏磁區 ( 但是用 BIOS 的 INT 13H 可以存取)。

在隱藏磁區之後,就是第一個主要分割了,而第一個主要分割區的第一個磁區就是啟動磁區了,所以假如知道隱藏磁區數就找得著第一個主要分割的啟動磁區。那麼怎麼知道隱藏的磁區有多少呢?原來在分割表的『分割區相對位置』處 ( 參考前面的說明 ),會記載著該分割區的啟動磁區距離 MBR 有多少個磁區,這些磁區的數目就是隱藏磁區的數目。

同樣的,第二個主要分割與第三個主要分割的啟動磁區,也是在該分割的最前面磁區。所以只要找到分割表的『分割區相對位置』就能找到第二、三個主要 分割的啟動磁區。以另一個觀點來看,假如用 DOS 中斷服務程式讀取第二個主要分割磁區時 ( 例如用 INT 25H 或 AX=7305H/INT 21H ),必定無法指定邏輯磁區而能讀到第一個分割區的邏輯磁區,所以對第二個分割來說,第一個分割的所有磁區都是隱藏的磁區。同理,對第三個分割來說,第一個分割與第二個分割的所有磁區也都是隱藏的磁區。

一個 FAT16 或 FAT32 分割的第一個磁區是啟動磁區,如果是 FAT16 分割的話,緊接著啟動磁區之後是兩個 FAT,如果是 FAT32 分割的話,在兩個 FAT 之前還有若干啟動備份等磁區,緊接著的才是 FAT。接著 FAT 的是根目錄,根目錄之後就是資料磁區了。綜合以上的說明,小木偶附上底下的圖片,說明一個硬碟經過分割後的分布情形。假設這個硬碟被分成兩個主要分割與一個擴充分割,而擴充分割再繼續分成兩個邏輯硬碟,而這些邏輯硬碟都被格式化成 FAT16 或是 FAT32 檔案格式,那麼每個分割區與邏輯硬碟內的結構應該如下圖分布:

硬碟各分割的分布圖

依照上圖,擴充分割內的第一個磁區內含擴充分割表,擴充分割表內有指向下一個擴充分割表及邏輯硬碟的資料,而每個擴充分割表之後與邏輯硬碟之前,也有數個隱藏磁區。這種情形,跟第一個主要分割相同。或許我們可以這樣看,每個邏輯硬碟就像具體而微的實體硬碟,所以分割表 ( 更正確的說是擴充分割表 ) 和啟動磁區之間有隱藏磁區。

觀察

底下是小木偶用 HDSV2.COM 觀察一顆 Western Digital 80GB ( 型號 WD800BB ) 的硬碟,這顆硬碟有三個主要分割及一個擴充分割,這三個主要分割分別是安裝 DOS 6.20、Win 98 SE 及 Win XP,檔案系統分別是 FAT16、FAT32、NTFS。擴充分割再細分成兩個 16GB 的 FAT32、一個 20GB 的 NTFS、2 個安裝 Mandrake Linux 的 Ext2 及一個 Linux SWAP,總共 5 個邏輯硬碟。首先觀察這個硬碟的 MBR,在 MBR 的位址 1BEH∼1FDH,這 64 個位元組就是分割表 ( 紫線框起的區域 ):

HDSV 執行畫面
圖一

注意到位址 1C6H 開始的雙字組是 3FH ( 白框框起之處 ),此欄位是『分割區相對位置』,就表示第一個分割之前的 3FH 個磁區是隱藏的,此時您若改變 LBA 邏輯磁區到 3FH ( 按下『L』鍵,再輸入『3F』及 Enter 鍵 ),如下圖二,您就能夠看到第一個分割的啟動磁區了,注意到圖中的『LBA 磁區』已經變成 3FH 了。下圖二就是第一個分割的啟動磁區,也就是 DOS 6.20 的啟動磁區:

第一個分割的啟動磁區
圖二

如果想找第二主要分割區的啟動磁區,可先到分割表的第二欄的『分割區相對位置』,請參考圖一中分割表的第二個分割的『分割區相對位置』的雙字組為 1F6080H ( 即白框之下的雙字組 ),就表示這個硬碟的第二個主要分割在第 1F6080H 個編號的邏輯磁區,改變 HDSV 中的『LBA 磁區』為第 1F6080H 個邏輯磁區就可以找到 Win 98 SE 的啟動磁區,如下圖三:

第二個主要分割
圖三

至於擴充分割區內可以有數個邏輯分割,而這些邏輯分割各自代表一個邏輯硬碟,要觀察擴充分割得先找到擴充分割起始磁區,請看圖一的分割表最後一欄,您就會發現擴充分割開始的邏輯磁區是 1966DFAH ( 淺藍框框起之處 ),改變『LBA 磁區』到這個邏輯磁區,並按向下鍵觀察後 256 個位元組,如下圖四:

第一個擴充分割表
圖四

果然如前章所述,擴充分割表只佔兩個欄位,第一個是在此分割的邏輯硬碟所在位置,此位置是相對於擴充分割開始之處 ( 及紅框框起之處 ),所以第一個邏輯硬碟開始於此擴充分割表之後的 3FH 磁區處,所以這個邏輯硬碟的啟動磁區是在 LBA 第 1966DFAH+3FH 個邏輯磁區之處,亦即第 1966E39H,改變『LBA 磁區』到這個邏輯磁區就可以觀察到這個邏輯硬碟的啟動磁區:

第一個邏輯硬碟
圖五

如果要找第二個邏輯分割,就必須回到第一個擴充分割表的第二欄的『分割區相對位置』,亦即圖四中藍框框起之處。換句話說,第二個邏輯分割開始於第一個擴充分割表之後的 1F1DD2FH 之處,所以第二個邏輯分割開始於 1966DFAH+1F1DD2FH,也就是 3884B29H,觀察這個 LBA 邏輯磁區的後 256 個位元組 ( 按向下鍵 ):

第二個擴充分割表
圖六

就可以找到第二個邏輯硬碟的啟動磁區是在 3884B68H ( 即 3884B29H+3FH,3FH 為灰色框框框起之處 ),其餘邏輯分割可照此步驟重複,就不再描述了。


結論

有了 HDSV2.COM 後,您可以觀察您自個兒的硬碟到底藏了那些東西,老實告訴你,我還發現硬碟娷疆簿ヱ祖漲a方還真不少。


回到首頁到第三十一章到第三十三章