寫操作系統之開發引導扇區


本篇目標

  1. 介紹引導扇區。
  2. 介紹軟盤結構(FAT12)。
  3. 用匯編代碼把加載器讀取到內存中。
  4. 用匯編代碼把內核加載器讀取到內存中。

簡略流程

計算機啟動的簡略流程如下:

BIOS對應的中文術語是“基本輸入輸出系統”。計算機啟動時,首先運行的便是BIOS

BIOS是計算機廠商預置在計算機硬件中的一種軟件,它會完成一些操作。我們只需知道,它會從內存地址0x7c00處讀取引導扇區,就足夠了。引導扇區的作用是從軟盤中讀取加載器。

我們把”引導扇區“叫做boot,把引導扇區的源碼文件命名為boot.asmboot恰好占用一個扇區,因此,boot所在的扇區被稱為“引導扇區”。我們就把“加載器”叫做loader吧,把加載器的源碼文件命名為loader.asm

我們將使用nasm編寫bootloader

軟盤

軟盤和硬盤一樣,是一種存儲介質,但目前已經很少使用。我們將使用bochs創建虛擬軟盤。

軟盤使用FAT12文件系統。我們寫好loader后,會把它存儲到軟盤中。

從軟盤的第一個字節開始存儲還是從0x7c00開始存儲?

先介紹一下軟盤的數據結構分布圖。

我們使用的軟盤是1.44M軟盤。這種軟盤有80個磁道,每個磁道有18個扇區。軟盤有兩個盤面,因此這種軟盤的容量是:

$軟盤容量 = 80 * 18 * 512 * 2 / 512 = 2879個扇區≈1.44M$。

int 13h

我們使用BIOS的中斷int 13h從軟盤中讀取數據到內存中。先看下圖了解一下中斷int 13h的用法。

什么是BIOS中斷?我們不必糾纏這個概念,先從我們熟悉的高級語言的角度理解int 13h

int 13h理解成一個函數,把這個函數命名為ReadSectorFromFloppy

這個函數的聲明是:void ReadSectorFromFloppy(int ah, int al, int ch, int cl, int dh, int dl, char *dest)。除最后一個參數char *dest外,這個函數的參數對應上圖中的同名寄存器。char *dest對應上圖中的es:bx

ReadSectorFromFloppy的作用能簡化為:把數據從src復制到dest指定的內存地址處。只不過,src不是通過一個參數傳遞給函數,而是通過一系列參數傳遞給函數。再說得明確一些,一系列參數聯合起來告訴了函數src是多少。

怎么調用ReadSectorFromFloppy?很簡單,按照函數聲明傳遞參數給它就行。對int 13h的使用也是如此,將每個寄存器需要的值填入對應的寄存器,然后,使用int 13h。使用int 13h的偽代碼如下:

ReadSector:
		mov ah, 02h
		mov al, 要讀的扇區數
		mov ch, 磁道號
		mov cl, 起始扇區號
		mov dh, 磁頭號
		mov dl, 驅動器號(0表示A盤)
		mov bx, 目標數據比如loader在內存中的位置
		
		int 13h

我們在后續的開發過程中,還會多次遇到對一些端口的讀寫操作。和這里的BIOS中斷的類似,都能用高級語言中的函數來理解。沒什么神秘的,傳遞一些參數,然后執行某種操作,從某個指定的地址獲取數據。僅此而已。

上面說得輕描淡寫,大家可能會以為我們只要三四分鍾就能從軟盤中讀取loader了。真這么順利嗎?讓我們來試試。

  1. ah,只需往這個寄存器中填充02h
  2. al,每次讀取一個扇區,往al中填充01h
  3. ch,磁道號是多少?未知。cldhdlbx中應該填充什么值?全是未知。
  4. 再看一次上面的說明,dl中應該填充0hbx的值也好確定。

除了四個參數未知,還需要知道調用ReadSector幾次才能讀完全部loader數據。讓我們帶着這些疑問去多了解一下軟盤。

數據分布

一張1.44M的軟盤中存儲的數據的結構如下圖所示。

補充說明一下這張圖:

  1. 第0個扇區是引導扇區。
  2. 第1個扇區到第18個扇區是FAT區域。FAT區域中存儲兩個完全相同的FAT表,分別是FAT1和FAT2。它們互為備份。
  3. 從第19個扇區開始,存儲根目錄。根目錄的占用的扇區數量是多少,取決於在軟盤中存儲多少個文件。
  4. 根目錄區域之后的所有扇區都存儲數據區。數據區的初始扇區號、一共占用多少個扇區,都是未知數。

猜猜看,loader存儲在軟盤數據結構的哪個區域?

顯而易見,loader被存儲在數據區。

補充說明一點。每個扇區只會存儲一個文件的數據,絕對不會存儲兩個文件的數據。例如,文件A的大小是510字節,從扇區號為N的扇區的最開始那個字節存儲文件A。N號扇區存儲完A文件后,還剩下2個字節。這個時候,如果往軟盤中存儲文件B,文件B的大小是2個字節,文件B也不會使用N號扇區的剩下的2個字節來存儲,而是會重新使用一個完全沒有使用過的扇區來存儲文件B。軟盤、硬盤都是如此。

繼續回到我們的主題。

根目錄

查詢根目錄

根目錄的大小由軟盤能存儲的最大文件數量決定。但是,根目錄的大小一定是整數個扇區。

根目錄由若干個根目錄項組成。根目錄項是一段32個bit的存儲空間。在根目錄項中,最有用的是"文件名"和“文件名對應的文件在數據區中的第一個扇區的扇區號”。

先看一段偽代碼。它非常清楚地說明了根目錄的作用:根據文件名找到文件對應的根目錄項,從目標根目錄項中找到文件在數據區的第一個扇區的扇區號。

int get_1st_sector_by_filename(filename){
  		start_address;	// 根目錄的初始地址
  		count;	// 根目錄項的數量
  		for(i = 0; i < count; i++){
        	if(start_address.文件名 == filename){
            	return start_address.文件在數據區的第一個扇區的扇區號
          }
        	start_address += 32;
      }
  		return -1;		// 不存在文件名是filename的文件
}

在后面,我們會使用nasm實現和偽代碼思路相同的匯編函數。

根目錄項結構

如果用C語言為根目錄項建立一個struct,將會是下面這樣的。

struct root_directory_entry{
  	char[11] DIR_Name;
  	char	DIR_Attr;
  	char[10] DIR_Reserved;
  	short	DIR_WrtTime;
  	short	DIR_WrtDate;
  	short DIR_FstClus;
  	int DIR_FileSize;
};

FAT表

單鏈表

先說結論:一個文件對應的所有FAT表項構成一個單鏈表。

什么是FAT表項?它們怎么構成一個單鏈表?請繼續往下看。

從根目錄中找到目標文件在數據區的第一個扇區的扇區號,就知道從哪個扇區讀取數據了。

可是,根目錄只提供了目標文件的第一個扇區的扇區號。如果目標文件需要兩個以上扇區存儲呢?如何知道第二個扇區、第三個扇區、第N個扇區的扇區號呢?FAT表會提供這些信息。

回憶一下軟盤的數據分布圖,圖中有FAT1FAT2,它們都是FAT表,我們只需從一個FAT表中獲取數據,就選擇FAT1吧。

FAT1的大小是9個扇區,512*9個字節。每12個bit構成一個FAT表項,FAT表項的值有兩重含義:

  1. 下一個FAT表項的編號。
  2. 文件的下一個扇區的扇區號。

舉例說明。軟盤中存儲着文件名為CG的文件。CG的大小是514個字節。從根目錄中查詢到CG在數據區的第一個扇區的扇區號是4。

4除了是扇區號,還是FAT表項的編號。注意這句話,非常重要。

FAT1中找到編號為4的FAT表項,這個FAT表項的值是5,那么,5既是下一個FAT表項的編號,又是下一個扇區號。也就是說,CG對應的FAT表項是第4個FAT表項、第5個FAT表項;在數據區占用的扇區是第4個扇區、第5個扇區。

根據當前FAT表項找到下一個FAT表項,這其實就是一個單鏈表。和單鏈表一樣,FAT表項構成的單鏈表也有一個尾結點。識別偽結點的方法是判斷FAT表項的值是否大於等於FAT_Entry_ValueFAT_Entry_Value是一個具體的值,等我們寫代碼時再看看它是多少。

FAT表項

本小節的目的是弄清楚:根據FAT表項的編號計算FAT表項的值。

先看一下FAT表項圖。

每個FAT表項占用12個bit,計算機讀取數據的最小單位是1個字節8個bit。為了每次都讀取到完整的FAT表項,需要一次讀取2個字節16個bit。16個bit只能存儲一個FAT表項。怎么存儲?只有圖2-2的兩種情況。

每次讀取FAT表項,都會讀取兩個字節,而這兩個字節的低12位和高12位都可能是FAT表項。要想獲取FAT表項,首先要讀取兩個字節,然后要判斷FAT表項存儲在低12位還是高12位。這對我來說,是一個有點費勁的問題,我會寫得詳細一些。

用具體例子來尋找判斷FAT表項存儲位置的方法。FAT表存儲在初始地址為$512$字節的存儲空間中。

編號 讀取地址(字節) 占用(bit) 實際讀取(bit) 注釋 實際讀取(字節)
0 1 8~~19 8~~23 20~~23是其他表項數據,低12位是本表項數據 1~~2
1 2 20~~31 16~~31 16~~19是其他表項數據,高12位是本表項數據 2~~3
2 3 32~~43 32~~47 44~~47是其他表項數據,低12位是本表項數據 3~~4

對上面表格的補充說明:

  1. 單位為bit的列中的數值應該加上基數:$512 * 8$。
  2. 單位為字節的列中的數值應該加上基數:511。
  3. 因版本需要,沒有把基數寫到表格的列中。

觀察表格中的“編號”列和“注釋"列,能得到下面的結論:

  1. 當FAT項的編號是奇數時,FAT表項存儲在2個字節的高12位。
  2. 當FAT項的編號是偶數時,FAT表項存儲在2個字節的低12位。

知道了FAT表項編號N,怎么計算存儲FAT表項的存儲空間的字節偏移量?計算公式是:$N * 12 / 8$。例如,編號為2的FAT表項的存儲空間的初始地址是:$2 * 12/8 = 3 $。

最后,再看一張圖。

定位FAT項在軟盤中的位置,需要確定兩個值:

  1. FAT項在軟盤中的扇區偏移量。
  2. FAT項在軟盤中的某個扇區中的字節偏移量。

結合上面的示意圖來解釋。扇區偏移量是N,字節偏移量是M。讀取N+1號扇區后,從N+1號扇區的第M字節開始讀取兩個字節,目標FAT項就存儲在這兩個字節中。

獲取FAT項的值

方法一

現在,可以給出查詢FAT表項的值的偽代碼了。

int get_fat_entry_value(fat_entry_no){
  	// fat_entry_no是FAT項的編號。
  	remainder = fat_entry_no % 2;
  	// sector_number是要讀取的扇區數量
  	sector_number = 2;
  	// sector_offset 是FAT項存儲示意圖中的N。
  	sector_offset = fat_entry_no * 3 / 2 / 512;
  	// bit_offset 是FAT項存儲示意圖中的M。
  	bit_offset = fat_entry_no * 12 / 8;
  	// 讀取偏移量是sector_offset兩個扇區
  	// sectors是FAT項存儲示意圖中的N+1號扇區和沒有畫出來的N+2號扇區。
  	sectors = read_sector(sector_offset, sector_number);
    two_byte_value = sector + bit_offset;
    fat_entry_value = remainder == 0 ? (two_byte_value && 0x0FFF) : (two_byte_value && 0xFFF0);
  	return fat_entry_value;
}
方法二

在上面的偽代碼中,檢查FAT項的字節偏移量是不是整數個字節的方法是根據FAT項的編號識別。除了這種方法,還有第二種方法。這個方法如下所述:

  1. 一個FAT項占用12個bit,也就是1.5個字節,所以FAT項的字節偏移量等於FAT項的編號乘以1.5
  2. 為了避免不同類型的數據之間進行運算,可以采用乘以1.5的等價運算:先乘以3,再除以2。
    1. 商是字節偏移量。
    2. 余數是bit偏移量。根據余數是否為0判斷FAT項的字節偏移量是不是整數個字節。

在后面會講到的GetFATEntry函數中,使用的是第二種方法。理解不了這個函數的代碼的時候,記得回頭看看這里的講解。

草稿
  1. 編號為0的FAT表項,讀取地址是1,占用(1*8)~~19個bit,實際讀取的空間是第823個bit(第1~~2個字節)。~~

  2. 編號為1的FAT表項,讀取地址是2,占用20~~31個bit,實際讀取的空間是第1631個bit(第2~~3個字節)。~~

  3. 編號為2的FAT表項,讀取地址是4,占用32~~43個bit,實際讀取的空間是第3247個bit(第4~~5個字節)。~~

  1. 編號為0的FAT表項,讀取地址是1,占用(1*8)~~19個bit,實際讀取的空間是第1~~2個字節(第8~~23個bit),字節偏移量是$(8/1=1)$個字節。
  2. 編號為1的FAT表項,讀取地址是2,占用20~~31個bit,實際讀取的空間是第2~~3個字節(第16~~31個bit),字節偏移量是$(20/8=2)$個字節。
  3. 編號為2的FAT表項,讀取地址是4,占用32~~43個bit,實際讀取的空間是第4~~5個字節(第32~~47個bit),字節偏移量是$(32/8=4)$個字節。

boot

boot要實現的功能是:

  1. 從根目錄中找到目標文件在FAT表中的FAT項。
  2. FAT項包含目標文件的數據存儲在哪個扇區。
  3. 使用BIOS中斷int 13h讀取目標扇區的數據。

直接看代碼吧。代碼比較長,但是不要被嚇到,也不要煩躁,我們一起來看看。

代碼解讀

泛讀

; 計算機啟動后,會檢查有沒有存儲設備例如軟盤、硬盤等。如果有,會選擇一種設備例如軟盤,
; 從軟盤的引導扇區中讀取數據並且復制到內存地址為0x7c00的那段內存空間。
; 也就是說,這個指令的作用是,讓BIOS把boot存儲到內存地址是0x7c00的內存空間中。
; 然后,BIOS執行結束后,會從0x7c00處開始執行。
; 為什么是0x7c00?這涉及到古老的計算機歷史。我以為這種知識不重要,不知道也不影響我們繼續開發操作系統。
; 因此,不深究這個問題。
org 0x7c00

	; 跳轉到LABEL_START為開頭的那塊代碼。
	jmp	LABEL_START
	; 空指令。
	nop

	; 下面是 FAT12 磁盤的頭,叫做"BPB"。
	; 必須有這段指令,BIOS才會把存儲設備中的這個扇區識別為引導扇區。
	; 也不必深究,我們寫操作系統時,照搬這段即可。
  BS_OEMName      DB 'YOUR--OS'   ; OEM String, 必須 8 個字節
  BPB_BytsPerSec  DW 512          ; 每扇區字節數
  BPB_SecPerClus  DB 1            ; 每簇多少扇區
  BPB_RsvdSecCnt  DW 1            ; Boot 記錄占用多少扇區
  BPB_NumFATs     DB 2            ; 共有多少 FAT 表
  BPB_RootEntCnt  DW 224          ; 根目錄文件數最大值
  BPB_TotSec16    DW 2880         ; 邏輯扇區總數
  BPB_Media       DB 0xF0         ; 媒體描述符
  BPB_FATSz16     DW 9            ; 每FAT扇區數
  BPB_SecPerTrk   DW 18           ; 每磁道扇區數
  BPB_NumHeads    DW 2            ; 磁頭數(面數)
  BPB_HiddSec     DD 0            ; 隱藏扇區數
  BPB_TotSec32    DD 0            ; wTotalSectorCount為0時這個值記錄扇區數
  BS_DrvNum       DB 0            ; 中斷 13 的驅動器號
  BS_Reserved1    DB 0            ; 未使用
  BS_BootSig      DB 29h          ; 擴展引導標記 (29h)
  BS_VolID        DD 0            ; 卷序列號
  BS_VolLab       DB 'YOUR--OS.02'; 卷標, 必須 11 個字節
  BS_FileSysType  DB 'FAT12   '   ; 文件系統類型, 必須 8個字節

LABEL_START:
	; many code
	

; 引導器最多只有510個字節,如果存儲完實現功能的指令后還不夠510個字節,就用0填充剩余的存儲空間。
times	510 - ($ - $$)	db	0
; 0xAA55是一個魔數。BIOS讀取存儲設備的第一個扇區后,會檢查扇區的最后兩個字節是不是`0xAA55`。
; 如果不是,BIOS認為這個扇區不是引導扇區;如果是,BIOS認為這個扇區是引導扇區。
dw	0xAA55

這段代碼中的FAT12的磁盤頭和扇區的最后兩個字節0xAA55一起構成了引導扇區的標志。沒有這兩個標志,BIOS就認為這個扇區不是引導扇區。

BPB:BIOS參數塊(BIOS Parameter Block)。

ReadSector

泛讀

先回顧一下前面給出的偽代碼。

ReadSector:
		mov ah, 02h
		mov al, 要讀的扇區數
		mov ch, 磁道號
		mov cl, 起始扇區號
		mov dh, 磁頭號
		mov dl, 驅動器號(0表示A盤)
		mov bx, 目標數據比如loader在內存中的位置
		
		int 13h

下面的代碼中:

  1. ah的值通過mov ah, 02h ; 讀軟盤設置成02h

  2. al的值通過兩條語句設置。

    1. mov byte [bp-2], cl
      mov al, [bp-2]
      
    2. 也就是說,al中的值是cl中的值。cl的值應該在調用ReadSector前設置了值。

  3. ch的值通過下面的語句設置。

    1. mov ch, al
      shr ch, 1	; ch 是柱面號
      
    2. 不能一眼看出這兩條語句的含義。先擱置。

  4. cl的值通過下面的語句設置。

    1. inc ah
      mov cl, ah
      
    2. 也不能一眼看出這兩條語句的含義。先擱置。

  5. dh的值通過下面的語句設置。

    1. mov dh, al
      and dh, 1	; dh 是磁頭號
      
    2. 也不能一眼看出這兩條語句的含義。先擱置。

  6. dl的值通過mov dl, 0 ; 驅動器號,0表示A盤

  7. bx有關系的語句是:

    1. push bx
      pop bx
      
    2. bx的值應該是在調用ReadSector前設置的。

難點

經過上面的仔細分析,發現了三個疑問,分別是clchdh的值。它們分別是:起始扇區號、柱面號和磁頭號。先給出這三個值的計算公式:

理解這個計算公式前,復習一次1.44M式軟盤的知識。

  1. 軟盤有80個磁道。每個磁道有18個扇區。
  2. 軟盤有兩個盤面,有兩個磁頭,每個盤面有一個磁頭。盤面號和磁頭號分別是:0號、1號。
  3. 兩個盤面上的對應的一對磁道組成一個柱面。每個柱面包含兩個磁道。柱面號的初始值是0。
  4. 每個磁道的扇區的扇區號的初始值是1,不是0。

再理解公式。

  1. $磁道號 = 扇區號/每磁道扇區數$
  2. $初始扇區號 = 扇區號sector_no \mod\ 每磁道扇區數 + 1$
    1. 扇區號除以每磁道扇區數的余數是填充若干個磁道后剩余的扇區數量M。
    2. 換句話說,這些扇區是位於第N磁道的前M個扇區,扇區號為sector_no的扇區是第N磁道的第M個扇區。
    3. 第M個扇區在第N磁道的扇區號是多少?
    4. 要回答這個問題,先補充兩個知識點:sector_no是扇區在軟盤中的扇區號,初始值是0;M是扇區在磁道中的扇區號,初始值是1。
    5. 因此,在磁道中,偏移量是0個扇區的扇區的扇區號是(1 + 0);偏移量是1個扇區的扇區的扇區號是(1 + 1);偏移量是2個扇區的扇區的扇區號是(1 + 2);由此歸納出,偏移量是M個扇區的扇區的扇區號是(1+M)。
    6. 這就是公式中起始扇區號 = R + 1的由來。
  3. 柱面號 = 磁道號 / 2。很容易理解。每個柱面有兩個磁道,0號磁道在0號柱面,1號磁道在0號柱面;2號磁道在1號柱面,3號磁道在1號柱面。
  4. 磁頭號 = 磁道號 & 1
    1. 磁道號是奇數,這個磁道的由0號磁頭處理;磁道號是偶數,這個磁道由1號磁頭處理。
    2. 奇數的最低位bit的值總是1,偶數的最低位bit的值總是0。因此,只需判斷磁道號的最低位bit是0還是1就能判斷出這個磁道號是奇數還是偶數。
再讀代碼

我初次看這塊知識時花了不少時間,所以,我要再重復寫幾句。

使用ReadSector讀取數據時,直接提供的參數只有從FAT12的根目錄和FAT中查詢出來的扇區號sector_no。這個扇區號的初始值是0。

ReadSector函數中把sector_no代入上面的公式計算出來的在磁道中的扇區號的初始值是1。

徹底掃清所有障礙之后,讓我們再次直面開始的那個難題吧:起始扇區號、柱面號和磁頭號是多少?

  1. sector_no存儲在ax中。
  2. SectorNumberOfTrack的值是18。在文末的全部代碼中將會看到為這個變量賦值的語句。
  3. div bl ; 商在al中,余數在ah中
    1. nasmdiv指令,進行除法計算,被除數存儲在ax中,除數存儲在bl中,商存儲在al中,余數存儲在ah中。
    2. 根據公式,$柱面號(ch) = 扇區號(ax)/每個磁道包含的扇區的數量(SectorNumberOfTrack)/2$。
    3. $在磁道中的起始扇區號(cl) = 扇區號(ax) % 每個磁道包含的扇區的數量(SectorNumberOfTrack) + 1$。
    4. $磁頭號(dh) = 扇區號(ax) / 每個磁道包含的扇區的數量(SectorNumberOfTrack) & 1$。

結合上面冗長的分析看下面加了注釋的代碼,應該不會有太多疑問。

; 讀取扇區
ReadSector:
	push ax
	push bp
	push bx
	mov bp, sp
	sub esp, 2
	mov byte [bp-2], cl
	
	; ax 存儲在軟盤中的扇區號
	mov bl, SectorNumberOfTrack	; 一個磁道包含的扇區數
	div bl	; 商在al中,余數在ah中
	mov ch, al
	shr ch, 1	; ch 是柱面號
	mov dh, al
	and dh, 1	; dh 是磁頭號
	mov dl, 0	; 驅動器號,0表示A盤
	inc ah
	mov cl, ah
	mov al, [bp-2]
	add esp, 2
	mov ah, 02h	; 讀軟盤
	pop bx
	
	int 13h

	pop bp
	pop ax
	ret

GetFATEntry

匯編指令div和mul

[]()

代碼

FATEntryIsInt	equ 0		; FAT項的字節偏移量是不是整數個字節:0,不是;1,是。
BytesOfSector	equ	512	; 每個扇區包含的字節數量
; 根據FAT項的編號獲取這個FAT項的值
GetFATEntry:
	; 用FAT項的編號計算出這個FAT項的字節偏移量 start
	; 復位軟驅時會修改ax的值,先把它存儲到棧中。
	push ax
	; 復位軟驅
	mov ah, 00h
	mov dl, 0
	int 13h
	
	; 還原被復位軟驅而修改的ax中的值。
	pop ax	
	; 下面的操作,實現 ax * 3 / 2。
	mov dx, 0
	mov bx, 3
	mul bx
	mov bx, 2
	div bx
	; 用FAT項的編號計算出這個FAT項的字節偏移量 end
	; div bx操作會把余數存儲在dx中,商存儲在ax中。
	; dx是bit偏移量,ax是字節偏移量。
	mov [FATEntryIsInt], dx
	; 用字節偏移量計算出扇區偏移量 start
	mov dx, 0
	; and ax, 0000000011111111b  ; 不知道這句的意圖是啥,忘記得太快了!
	; mov dword ax, al ; 錯誤用法
	; mov cx, [BytesOfSector]
	mov cx, 512
	; div cx操作計算FAT項的扇區偏移量,存儲在ax中,dx中存儲的是字節偏移量。
	div cx
	; push dx
	add ax, SectorNumberOfFAT1	; ax 是在FAT1區域的偏移。要把它轉化為在軟盤中的扇區號,需加上FAT1對軟盤的偏移量。
	; 用字節偏移量計算出扇區偏移量 end
	
	; 讀兩個扇區。
	mov cl, 2 
	mov bx, 0
	push es
	; dx的值可能會在call ReadSector改變,所以先存儲到棧中。
	push dx
	push ax
	mov ax, BaseOfFATEntry
	; ReadSector把兩個扇區的數據讀取到BaseOfFATEntry:bx處。
	; bx是什么?bx是0。
	mov es, ax
	pop ax
	; 用扇區偏移量計算出在某柱面某磁道的扇區偏移量,可以直接調用ReadSector
	call ReadSector
	; 恢復dx的值,此時,dx中的值是FAT項讀取到的兩個扇區中的字節偏移量。
	pop dx
	add bx, dx
	;[es:bx]是FAT項的初始位置,從這個位置開始,復制2個字節到ax中。
	mov ax, [es:bx]
	pop es
	; 根據FAT項偏移量是否占用整數個字節來計算FAT項的值。
	; 若偏移量是整數個字節,ax的低12位是FAT項;反之,ax的高12位是FAT項。
	cmp byte [FATEntryIsInt], 0
	jz FATEntry_Is_Int
	; 獲取ax的高12位。
	shr ax, 4	
FATEntry_Is_Int:
	; 獲取ax的低12位。
	and ax, 0x0FFF
	ret

結尾

本篇介紹了:

  1. 計算機啟動的極簡流程。
  2. 1.44M軟盤的結構。
  3. boot.的代碼解釋。

需要結合上一篇文章《寫操作系統之搭建開發環境》才知道怎么運行boot中的代碼。由於本文篇幅有點長,將在下篇《寫操作系統之開發引導器》中講解運行boot中的代碼的方法。

祝一切順利!

boot代碼全文

org 0x7c00

	jmp	LABEL_START
	nop

	; 下面是 FAT12 磁盤的頭
  BS_OEMName      DB 'YOUR--OS'   ; OEM String, 必須 8 個字節
  BPB_BytsPerSec  DW 512          ; 每扇區字節數
  BPB_SecPerClus  DB 1            ; 每簇多少扇區
  BPB_RsvdSecCnt  DW 1            ; Boot 記錄占用多少扇區
  BPB_NumFATs     DB 2            ; 共有多少 FAT 表
  BPB_RootEntCnt  DW 224          ; 根目錄文件數最大值
  BPB_TotSec16    DW 2880         ; 邏輯扇區總數
  BPB_Media       DB 0xF0         ; 媒體描述符
  BPB_FATSz16     DW 9            ; 每FAT扇區數
  BPB_SecPerTrk   DW 18           ; 每磁道扇區數
  BPB_NumHeads    DW 2            ; 磁頭數(面數)
  BPB_HiddSec     DD 0            ; 隱藏扇區數
  BPB_TotSec32    DD 0            ; wTotalSectorCount為0時這個值記錄扇區數
  BS_DrvNum       DB 0            ; 中斷 13 的驅動器號
  BS_Reserved1    DB 0            ; 未使用
  BS_BootSig      DB 29h          ; 擴展引導標記 (29h)
  BS_VolID        DD 0            ; 卷序列號
  BS_VolLab       DB 'YOUR--OS.02'; 卷標, 必須 11 個字節
  BS_FileSysType  DB 'FAT12   '   ; 文件系統類型, 必須 8個字節

LABEL_START:
	; 0B800h是顯存地址,gs存儲顯存地址。
	mov ax,	0B800h
	mov gs,	ax
	; 把es設置成BaseOfLoader。
	mov ax, BaseOfLoader
	mov es, ax

	; 復位軟驅
	mov  ah, 00h
	mov  dl, 0
	int 13h
	; FirstSectorOfRootDirectory的值是19,是根目錄在軟盤中的扇區號,也是扇區偏移量。
	mov ax,	FirstSectorOfRootDirectory
	mov cl, 1
	
	; OffSetOfLoader是存儲loader的內存空間的初始地址。
	mov bx, OffSetOfLoader
	; 讀取第19號扇區,存儲到內存空間的初始地址是OffSetOfLoader的這片內存中。
	call ReadSector
	; 在根目錄中檢查3個目錄項,這是人為規定,假設根目錄中只有3個目錄項。
	mov cx, 3
	
	; 執行這條指令后,[es:di]存儲的就是根目錄的第一個根目錄項。文件名位於根目錄項的最開始的11個字節。
	mov di, OffSetOfLoader
; 遍歷根目錄
SEARCH_FILE_IN_ROOT_DIRECTORY:
	cmp cx, 0
	; 沒有找到目標文件,跳轉到FILE_NOT_FOUND開頭的那段代碼。
	jz FILE_NOT_FOUND
	push cx
	; LoaderBinFileName是目標文件即loader的文件名的初始地址。
	mov si, LoaderBinFileName
	; LoaderBinFileNameLength是目標文件的文件名的長度。
	mov cx, LoaderBinFileNameLength
	mov dx, 0
	mov bx, (80 * 18 + 40) * 2
; 開始檢查當前目錄項中存儲的文件名是否和目標文件的文件名相同。方法是:檢測每個字符是否相同。
COMPARE_FILENAME:
	; 從[es:si]中讀一個字符復制到al中。
	lodsb
	;從根目錄項的文件名中取一個字符和從LoaderBinFileName中獲取的對應位置的字符進行比較。
	;當二者不相等時,跳轉到FILENAME_DIFFIERENT代碼塊執行。
	cmp al, byte [es:di]
	jnz FILENAME_DIFFIERENT
	; cx是文件名的長度。
	; 比較文件名函數結束的條件有兩個:一個是對比完了所有字符;一個是發現了不相同的字符。
	dec cx
	; 將di加1,逐個對比LoaderBinFileName和根目錄項中的文件名。
	; 將dx加1,統計已經比較過的字符的個數。
	inc di
	inc dx

	; 當已經統計完了所有字符,並且所有字符都相同時,說明當前根目錄項就是要目標文件的根目錄項,跳轉到FILE_FOUND塊執行。
	cmp dx, LoaderBinFileNameLength
	jz FILE_FOUND
	; 繼續對比下一個字符。
	jmp COMPARE_FILENAME		
FILENAME_DIFFIERENT:
	mov al, 'D'
  mov ah, 0Ah
  mov [gs:(80 * 24 + 40) *2], ax


	pop cx		; 在循環中,cx會自動減少嗎?
	cmp cx, 0
	dec cx
	jz FILE_NOT_FOUND
	; 低5位設置為0,其余位數保持原狀。回到正在遍歷的根目錄項的初始位置。
	and di, 0xFFE0	
	add di, 32	; 增加一個根目錄項的大小
	jmp SEARCH_FILE_IN_ROOT_DIRECTORY
FILE_FOUND:
	mov al, 'S'
	mov ah, 0Ah
	mov [gs:(80 * 24 + 35) *2], ax
	; 修改段地址和偏移量后,獲取的第一個簇號錯了 
	; 獲取文件的第一個簇的簇號
	and di, 0xFFE0  ; 低5位設置為0,其余位數保持原狀。回到正在遍歷的根目錄項的初始位置; 獲取文件的第一個簇的簇號
	; 文件的第一個簇號(可以理解為扇區號)在根目錄項中的字節偏移量是0x1A。
	add di, 0x1A
	mov si, di
	mov ax, BaseOfLoader
	push ds
	mov ds, ax
	; 把[ds:si]處的數據復制到ax中。也就是說,ax中存儲着目標文件的第一個扇區的扇區號,同時也是這個文件的第一個FAT項的編號。
	lodsw
	pop ds	
	push ax
	; 將會把從軟盤中讀取到的數據復制到[es:bx]開始的內存空間。
	mov bx, OffSetOfLoader
	; 獲取到文件的第一個簇號后,開始讀取文件
READ_FILE:
	push bx
	
	; 簇號就是FAT項的編號,同時也是文件塊在數據區的扇區號。
	; 用簇號計算出目標扇區在軟盤中的的扇區號。
	add ax, 19
	add ax, 14
	; 為什么要減去2?因為0號FAT項、1號FAT項不表示記錄任何扇區信息,從2號FAT項開始記錄數據區的扇區。
	; 第2號FAT項記錄數據區的第0號扇區。
	sub ax, 2
		
	; 讀取一個扇區的數據 start
	; add ax, SectorNumberOfFAT1
	mov cl, 1
	pop bx	
	call ReadSector
	;;xchg bx, bx
	; 讀取一個扇區到[es:bx]后,把下一個扇區讀取到[es:bx+512]開始的內存。
  add bx, 512
	; 讀取一個扇區的數據 end
	
	;jmp READ_FILE_OVER
		
	; 執行pop后,ax中存儲的是目標文件的第一個FAT項的編號。
	; GetFATEntry能根據這個FAT項的編號獲取這個FAT項的值,也就是下一個FAT項的編號。
	pop ax
	push bx
	call GetFATEntry
	pop bx
	push ax
	;ax >= 0xFF8時,當前扇區是文件的最后一個扇區。
	cmp ax, 0xFF8
	; 注意了,ax >= 0xFF8 時跳轉,使用jc 而不是jz。昨天,一定是在這里弄錯了,導致浪費幾個小時調試。
	;jz READ_FILE_OVER	
	;jc READ_FILE_OVER	
	jnb READ_FILE_OVER	
	
	jmp READ_FILE
	
FILE_NOT_FOUND:
        mov al, 'N'
        mov ah, 0Ah
        mov [gs:(80 * 24 + 36) *2], ax
	jmp OVER

READ_FILE_OVER:
	
	; 簇號就是FAT項的編號,同時也是文件塊在數據區的扇區號。
	; 用簇號計算出目標扇區在軟盤中的的扇區號。
	add ax, 19
	add ax, 14
	sub ax, 2

	; 讀取一個扇區的數據 start
	mov cl, 1

	mov al, 'O'
	mov ah, 0Ah
	mov [gs:(80 * 24 + 33) * 2], ax
	
	; 跳轉到loader執行loader中的指令。
	jmp BaseOfLoader:OffSetOfLoader	
	jmp OVER

OVER:

	jmp $

BootMessage:	db	"Hello,World OS!"
;BootMessageLength:	db	$ - BootMessage
; 長度,需要使用 equ 
BootMessageLength	equ	$ - BootMessage

FirstSectorOfRootDirectory	equ	19
SectorNumberOfTrack	equ	18
SectorNumberOfFAT1	equ	1

LoaderBinFileName:	db	"LOADER  BIN"
LoaderBinFileNameLength	equ	$ - LoaderBinFileName	; 中間兩個空格

FATEntryIsInt	equ 0		; FAT項的字節偏移量是不是整數個字節:0,不是;1,是。
BytesOfSector	equ	512	; 每個扇區包含的字節數量
; 根據FAT項的編號獲取這個FAT項的值
GetFATEntry:
	; 用FAT項的編號計算出這個FAT項的字節偏移量 start
	; mov cx, 3
	; mul cx
	; mov cx, 2
	;div cx		; 商在al中,余數在ah中	; 
	push ax
	MOV ah, 00h
	mov dl, 0
	int 13h
	
	pop ax	
	mov dx, 0
	mov bx, 3
	mul bx
	mov bx, 2
	div bx
	; 用FAT項的編號計算出這個FAT項的字節偏移量 end
	mov [FATEntryIsInt], dx
	; 用字節偏移量計算出扇區偏移量 start
	mov dx, 0
	; and ax, 0000000011111111b  ; 不知道這句的意圖是啥,忘記得太快了!
	; mov dword ax, al ; 錯誤用法
	; mov cx, [BytesOfSector]
	mov cx, 512
	div cx
	; push dx
	add ax, SectorNumberOfFAT1	; ax 是在FAT1區域的偏移。要把它轉化為在軟盤中的扇區號,需加上FAT1對軟盤的偏移量。
	; mov ah, 00h

	; 用字節偏移量計算出扇區偏移量 end
	mov cl, 2 
	mov bx, 0
	push es
	push dx
	push ax
	mov ax, BaseOfFATEntry
	mov es, ax
	pop ax
	; 用扇區偏移量計算出在某柱面某磁道的扇區偏移量,可以直接調用ReadSector
	call ReadSector
	pop dx
	add bx, dx
	mov ax, [es:bx]
	pop es
	; 根據FAT項偏移量是否占用整數個字節來計算FAT項的值
	cmp byte [FATEntryIsInt], 0
	jz FATEntry_Is_Int
	shr ax, 4	
FATEntry_Is_Int:
	and ax, 0x0FFF
	ret

; 讀取扇區
ReadSector:
	push ax
	push bp
	push bx
	mov bp, sp
	sub esp, 2
	mov byte [bp-2], cl
	
	; ax 存儲在軟盤中的扇區號
	mov bl, SectorNumberOfTrack	; 一個磁道包含的扇區數
	div bl	; 商在al中,余數在ah中
	mov ch, al
	shr ch, 1	; ch 是柱面號
	mov dh, al
	and dh, 1	; dh 是磁頭號
	mov dl, 0	; 驅動器號,0表示A盤
	inc ah
	mov cl, ah
	;add cl, 1	; cl 是起始扇區號
	; pop al		; al 是要讀的扇區數量
	mov al, [bp-2]
	add esp, 2
	mov ah, 02h	; 讀軟盤
	pop bx
	
	int 13h

	pop bp
	pop ax
	ret	

BaseOfLoader	equ	0x9000
OffSetOfLoader	equ	0x100
BaseOfFATEntry	equ	0x1000


times	510 - ($ - $$)	db	0
dw	0xAA55


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM