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

東小夫發表於2021-10-15

本篇目標

  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

相關文章