1. 程式人生 > >對FAT12檔案系統的理解

對FAT12檔案系統的理解

於淵老師實驗用了兩個檔案系統,一個是前幾章用的FAT12檔案格式,還有一個是後面他自己設計的一個簡單的orange‘s檔案系統。

    對於檔案系統的作用,我想可以分成兩個方面去理解。一個是像我們這次實驗用到的檔案系統,非常簡單直接,是直接屬於核心的一部分用來和磁碟驅動打交道;另一個方面就是再抽象一層出來的虛擬檔案系統,這個是用來給上層應用提供統一介面的,給具體檔案系統的實現規劃方圓。我們不需要考慮虛擬檔案系統的事情,只是實現一個簡單的可以湊合著在磁碟上增刪改查的系統就可以了。

    那說到底,檔案系統的作用就是解析給出的路徑,將檔案相對於其所管轄裝置(可以簡單理解為磁碟上的某個分割槽)的扇區偏移計算出來,將這個偏移傳遞給磁碟驅動,磁碟驅動將這個偏移轉換成相對於整個磁碟的偏移,然後根據讀寫指令向磁碟控制器傳送訊號。磁碟讀寫完資料後傳送中斷訊號,這個中斷訊號會讓cpu去執行IRQ14硬碟中斷處理程式。對於這個程式的具體實現是通過讀寫埠的方式,將資料讀取到記憶體或寫入到磁碟緩衝區,並將結果返回給上層的檔案系統。

    作者用了兩種載入方式,一個是軟盤,一個是硬碟。軟盤引導是FAT12格式,硬碟引導的就是作者自己仿照這MINIX精簡的格式。
    之所以先寫對檔案系統的理解,是因為我們一上來就要理解從軟盤或者硬碟開始的載入程式,就必然涉及到檔案的組織結構,要不然怎麼找核心檔案呢?而且我覺得作業系統的核心除了排程程序這些純粹記憶體操作,就數檔案系統最核心了。

  先說說FAT12檔案系統
    
之所以叫FAT12,我也是前段時間溫習書籍的時候,看到圖才有點恍然的,12位來表示簇號而檔案系統對於硬碟的管理如作者所說,一般分三個層次:
    
1、分割槽:通常指整個檔案系統(我自己讀書粗心,這句話我是在計算了幾次主分割槽的分割槽表大小才領悟到檔案系統就管理一個分割槽,不管這是個主分割槽還是一
個邏輯分割槽)

2、簇:一個或者多個扇區

3、扇區:磁碟上最小的資料單元(也就是硬體廠商提供給磁碟驅動的硬體介面,按照一定大小讀寫資料)

FAT12格式的引導扇區前62個位元組都有固定的作用,前36個位元組是一個叫做BPB(BIOS Params Block)結構的資料,隨後26個位元組是引導扇區的一部分。具體的格式讀者可以百度,或者可以查閱《一個作業系統的實現》104頁,不過記著這個結構體裡面的資料都是我們手動填充的,每個成員都有固定的意義,要麼真的很懂FAT12的佈局,要麼還是按照作者給的資料來比較好,比如說下面我們要用到的BPB_RootEntCnt成員,表示根目錄檔案數最大值,我們寫入的值是224,那麼根目錄區就可以確定佔用的大小了。在實際用的時候,前三個位元組是填充跳轉指令的,緊隨其後的是固定的格式,中間不要安插任何資料。隨後的448個位元組是我們載入程式的空間,最後兩個位元組為引導扇區結束標誌0xAA55。

整個軟盤的分割圖大概如下

0(起始扇區編號) 1 10 19(大小不固定)
引導扇區 FAT1 FAT2 根目錄區 資料區
     每個目錄條目佔用32個位元組,格式如下
名稱 偏移 長度 描述

DIR_NAME

0 B 檔名8位元組,副檔名3位元組

DIR_ATTR

0xB 1 檔案屬性

保留位

0xC A 保留位

DIR_WrtTime

0x16 2 最後一次寫入時間

DIR_WrtDate

0x18 2 最後一次寫入日期

DIR_FstClus

0x1A 2 次條目對應的開始簇號

DIR_FileSize

0x1C 4 檔案大小






作者為了簡便,只有根目錄一級目錄,不支援除根目錄以外的目錄形式存在(我也沒功力去實現)。可以看到根目錄區起始扇區號是19,大小我們可以根據BPB_RootEntCnt和BPB_BytsPerSec來計算。

根目錄區大小設為RootEntCnt,我們在BPB資料結構中已經填充了根目錄區條目個數BPB_RootEntCnt,每個條目32位元組,所以RootEntCnt=((BPB_RootEntCnt*32)+(BPB_BytsPerSec-1))/BPB_BytsPerSec,在這裡可以算出來佔用14個扇區。那麼也順便可以得到資料區第一個扇區的編號是19+14=33。另外,資料區第一個扇區的簇號是2,不是1和0,也就是說檔案在資料區的扇區編號M和FAT表中的簇號N的關係是M=33+N-2。

對於不超過512個位元組的檔案來說FAT表並沒有什麼作用,但是對於位元組數超過512的檔案來說,根目錄區存放的檔案資訊的條目中的DIR_FstClus存放的是檔案在資料區的第一個扇區的編號M0對應的簇號N0(也就是M0-33+2),而在FAT表中,簇號N位置存放的是下一個扇區的編號M1對應的簇號N1,如果檔案大小超過兩個扇區,那麼N1位置存放的是下一個扇區的編號M2對應的簇號N2...也就是說簇號的作用有兩個,一個是用來尋找下一個簇,一個是用這個值加上一定的偏移作為該簇代表的扇區編號。

FAT表中每個簇號佔用12個位,也就是1.5個位元組,所以我們要把簇號擴大1.5倍,但是還要考慮到時奇數的時候。如果是偶數,比如說2,乘以1.5就是3,我們直接讀取第三個位元組然後算術右移4位就好了。如果是奇數,3,那麼擴大1.5倍就是4.5,但是我們不可能讀取半個位元組,所以用ax*3/2的方式讀到能包含想要資料的整數個位元組,該例是從第4個位元組開始讀取了,然後右移4位就可以了,我們不要的那0.5個位元組就移位移出去了。唉,我數學不好,把這個想明白可算是費了老大的勁。

比如簇號3,所在地址是0x204(這裡也驗證了我們擴大1.5倍的說法,3*3/2=4),我們看到的是8f 00,但是讀到暫存器中的順序是從高位到低位存放的。也就是00 8f,這樣的形式支援我們右移4位能夠得到我們想要的結果。

到這裡,我想手工去找一個檔案應該是可以了。下面說說引導扇區的大致流程。

載入檔案到記憶體,肯定是要讀盤的,此時什麼程式碼都沒有,那就只能依靠BIOS在開機後所做的工作,也就是BIOS初始化的中斷表裡有我們需要的讀盤函式,int 0x13。

我就抄過來吧。

中斷號 暫存器 作用
13h ah=00h dl=驅動器號(0表示A盤)

復位軟碟機

ah=02h    al=要讀的扇區數

ch=柱面(磁軌)號    cl=起始扇區號

dh=磁頭號  dl=驅動器號(0表示A盤)

es:bx->資料緩衝區

從磁碟將資料讀入es:bx指向的資料緩衝區

對於1.44MB的軟盤來講,總共有兩面,也就是兩個磁頭(0號和1號磁頭),每面80個磁軌(0到79),每個磁軌18個扇區(1-18號),我們需要將我們平常所說的扇區號(從0號扇區開始編碼)轉換為這三個分量。

在趙炯老師寫的核心原始碼完全分析的書中對轉換有很好的總結。

扇區號與柱面號,當前磁軌扇區號和當前磁頭號的對應關係:

假定硬碟的每磁軌扇區數是track_secs,硬碟磁頭總數是dev_heads,指定的硬碟順序扇區號sector(從0開始),對應當前磁軌數為tracks,對應柱面號cyl,當前磁軌上的扇區號sec,磁頭號head

sector/track_secs=商是tracks,餘數是sec

tracks/dev_heads=商是cyl,餘數是head

當前磁軌扇區從1開始計數,所以sec+1;

反過來計算

sector=(cyl*dev_heads+head)*track_secs+sec-1;

對於我們而言,track_secs=18,dev_heads=2

對於我們想讀的扇區,根據扇區號做上述轉換,並將結果填到對應的暫存器,int 0x13就可以了。如果該中斷失敗,CF位會被置位,所以我們根據這個來判斷資料是否已經載入進來了。

下面我們來整理出如何在軟盤上尋找loader的思路。

作者為了簡便起見,沒有分多級目錄,所有檔案的目錄資料都是放在“/”目錄區。檔案的大小都是固定的,0x800個扇區

步驟:

(我組織的不好,最好結合著程式碼看)

1、讀取根目錄所在扇區,將資料存放在LOADER_SEG:LOADER_OFF指定的地址處。

2、等資料讀取完畢後,讓si指向 "LOADER  BIN"字串的起始地址,讓di指向LOADER_OFF,dx賦值為0x10,也就是每個扇區存放的目錄條目數,cx賦值為11,也就是字串的長度。

3、開始比較,cx==0,表示找到檔案。

4、如果找到檔案,讓di回退到當前條目起始地址(di&0xFFE0),然後加上0x1A,指向當前條目的DIR_FstClus域,該值表示檔案對應的開始簇號a,加上根目錄區大小b(14),加上相對簇號的根目錄起始扇區號c(c=17,本來是19的,但是由於簇號的起始號是2,所以我們在此處提前減掉2,方便以後的計算),我們得到檔案的扇區號d=a+b+c,我們將檔案讀到LOADER_SEG:LOADER_OFF指定的位置,此時LOADER_SEG:LOADER_OFF儲存的內容已經沒用了,可以隨意覆蓋。如果檔案大小超過512個位元組,我們還需要繼續讀取,直到讀取到的簇號等於0FFFh。至於讀取硬碟資料,讀取簇號的方式上面都講過了。

5、不相等讓di回退到當前條目起始地址(di&0xFFE0),然後指向下一個條目(di+0x20),si指回"LOADER  BIN"起始地址,dx-1(表示當前扇區剩下的條目),cx=11,然後重新比較。

6、如果dx==0,還沒有找到檔案,則讀取下一個扇區,跳到第2步。

等到都讀取完了,我們就jmp  LOADER_SEG:LOADER_OFF,開始執行我們自己的loader了。