(1)操作系統學習筆記——FAT12文件系統與Loader的加載
之前我們利用BIOS實現了一個打印字符串的操作,現在我們要在這基礎上加入文件加載的功能,以完成boot的操作。Boot程序主要的是負責開機啟動和加載Loader程序;Loader引導加載程序則用於完成配置硬件工作環境、引導加載內核等任務。
加載Loader程序最理想的方法自然是從文件系統中把Loader程序加載到內存中。那么此處的文件系統就選擇比較簡單的FAT12。在將軟盤格式化成FAT12文件系統的過程中,FAT類文件系統會對軟盤里的扇區進行結構化處理,進而把軟盤扇區划分成: 引導扇區、FAT表、根目錄區和數據區 這4部分。
-
引導扇區
FAT12文件系統的引導扇區不僅包含有引導程序,還有FAT12文件系統的整個組成結構信息。下表描述了FAT12文件系統的引導扇區結構:名稱 偏移 長度 內容 本系統引導程序數據 BS_jmpBoot 0 3 跳轉指令 jmp short Label_Start nop BS_OEMName 3 8 生產廠商名 'MINEboot' BPB_BytesPreSec 11 2 每扇區字節數 512 BPB_SecPreClus 13 1 每簇扇區數 1 BPB_RsvSecCnt 14 2 保留扇區數 1 BPB_NumFATs 16 1 FAT表的份數 2 BPB_RootEntCnt 17 2 根目錄可容納的目錄項數 224 BPB_TotSec16 19 2 總扇區數 2880 BPB_Media 21 1 介質描述符 0xF0 BPB_FATSz16 22 2 每FAT扇區數 9 BPB_SecPreTrk 24 2 每磁道扇區數 18 BPB_NumHeads 26 2 磁頭數 2 BPB_HiddSec 28 4 隱藏扇區數 0 BPB_TotSec32 32 4 如果BPB_Tot16值為0,則由這個值記錄扇區數 0 BS_DrvNum 36 1 int 13h的驅動器號 0 BS_Reserved1 37 1 未使用 0 BS_BootSig 38 1 擴展引導標記(0x29) 0x29 BS_VolID 39 4 卷序列號 0 BS_VolLab 43 11 卷標 'boot loader' BS_FileSysType 54 8 文件那系統類型 'FAT12' 引導代碼 62 448 引導代碼、數據及其他信息 結束標志 510 2 結束標志0xAA55 0xAA55 -
FAT表
FAT文件系統以簇來為單位來分配數據區的存儲空間(扇區), 每個簇的長度為:BPB_BytesPreSec * BPB_SecPreClus字節,數據區的簇號與FAT表的表項是一一對應關系。因此,文件在FAT類文件系統的存儲單位是簇,而非字節或扇區。這種設計方式可以將磁盤存儲空間按固定儲片(頁)有效管理起來,進而按照文件偏移,分片段訪問文件內的數據,就不必一次將文件里的數據全部讀取出來。
FAT表中的表項位寬與FAT類型有關,例如,FAT12文件系統的表項位寬就是12bit、FAT16文件系統的表項位寬就是16bit、FAT32文件系統的表項位寬就是32bit。當文件的體積增大時,其所需的磁盤存儲空間也會增加,隨時間的推移,文件系統將無法保證文件中的數據存儲在連續的磁盤扇區內,文件往往被分成若干個片段。借助FAT表項,可將這些不連續的文件片段按簇號鏈接起來,這個鏈接原理與鏈表極為相似。下面是對FAT表項取值的說明:FAT項 實例值 描述 0 0xFF0 磁盤標志字,低字節與BPB_Media數值保持一致 1 0xFFF 第一個簇已經被占用 0x000:可用簇 0x002~0xFEF:已用簇標識下一個簇的簇號 0xFF0 ~ 0xFF6:保留簇 0xFF7:壞簇 0xFF8~0xFFF:文件的最后一個簇在編寫程序的時候我們直接跳過FAT[0]和FAT[1]即可,並不使用它們,不必要理會其中的值。
-
根目錄和數據區
從本質上講,根目錄和數據區都保存着與文件相關的數據,只不過根目錄只能保存目錄項信息,而數據區不但可以保存目錄項信息,還可以保存文件內的數據。
此處所提及的目錄項是由一個32B組成的結構體,它既可以 表示成一個目錄,又可以表示成一個文件。名稱 偏移 長度 描述 DIR_Name 0x00 11 文件名8B,擴展名3B DIR_Attr 0x0B 1 文件屬性 保留 0x0C 10 保留位 DIR_WrtTime 0x16 2 最后一次寫入時間 DIR_WrtDate 0x18 2 最后一次寫入日期 DIR_FstClus 0x1A 2 起始簇號 DIR_FileSize 0x1C 4 文件大小 注: 對於
DIR_FstClus字段必須特別注意,它描述了文件在磁盤中存放的具體位置。由於 FAT[0] 和 FAT[1] 是保留項,不能用於數據區的簇索引,因此數據區的第一個有效簇號是2,而不是0或1。
程序編寫
FAT12文件系統數據
首先是為虛擬軟盤創建FAT12文件系統引導扇區數據:
org 0x7c00
; 設置棧指針的位置
BaseOfStack equ 0x7c00
; Loader的位置(20位)
; BaseOfLoader << 4 + OffsetOfLoader = 0x10000
BaseOfLoader equ 0x1000
OffsetOfLoader equ 0x00
; 根目錄占用的扇區數:(BPB_RootEntCnt * 32 + BPB_BytesPerSec - 1) / BPB_BytesPerSec
; (224 * 32 + 512 - 1) / 512 = 14
RootDirSectors equ 14
; 根目錄的起始扇區號 =
; 保留扇區數(BPB_RsvdSecCnt) + FAT表扇區數(BPB_FATSz16) + FAT表份數(BPB_NumFATs) = 1 + 9 * 2 = 19
SectorNumOfRootDirStart equ 19
; FAT表的起始扇區號,引導扇區的扇區號是0
SectorNumOfFAT1Start equ 1
; 用於平衡文件(或目錄)的起始簇號與數據區起始簇號的差值。
SectorBalance equ 17
;****************************************************
; 程序一開始就跳轉到Label_Start的位置處執行
jmp short Label_Start
nop
;****************************************************
; 生產廠商名(8 Bits)
BS_OEMName db 'Testboot'
; 每扇區字節數
BPB_BytesPerSec dw 512
; 每簇扇區數
BPB_SecPerClus db 1
; 保留扇區數
BPB_RsvdSecCnt dw 1
; FAT表的份數
BPB_NumFATs db 2
; 根目錄可容納的的目錄項數
BPB_RootEntCnt dw 224
; 總扇區數(因為使用的是1.44M的軟盤)
BPB_TotSec16 dw 2880
; 介質描述符
BPB_Media db 0xf0
; 每FAT扇區數
BPB_FATSz16 dw 9
; 每磁道扇區數
BPB_SecPerTrk dw 18
; 磁頭數
BPB_NumHeads dw 2
; 隱藏扇區數
BPB_HiddSec dd 0
; 如果BPB_FATSz16=0,那么這個值記錄扇區數
BPB_TotSec32 dd 0
; int 0x13的驅動器號
BS_DrvNum db 0
; 未使用(不用管)
BS_Reserved1 db 0
; 擴展引導標記
BS_BootSig db 0x29
; 卷序列號
BS_VolID dd 0
; 卷標(11 Bits)
BS_VolLab db 'boot loader'
; 文件系統類型(8 Bits)
BS_FileSysType db 'FAT12 '
這段代碼是在我們的程序一開始就定義的,但是程序是從上往下順序執行的,代碼中有想當一部分是數據,並不是代碼,所以我們一開始就需要使用無條件跳轉指令jmp short Label_Start,跳轉到Label_Start標簽處開始執行代碼。
讀取磁盤扇區模塊(函數)
有了FAT12文件系統引導扇區的數據,那么還要一個軟盤的讀取功能模塊(也可以理解為函數),該模塊的作用就是讀取軟盤,每次讀取一個扇區:
;==============================
; 模塊(可以理解為C語言中的函數)
; 從軟驅中讀取扇區
; 參數說明:
; AX = 待讀取的磁盤起始扇區號
; CL= 讀入的扇區數量
; EX : BX => 數據緩沖區起始地址
Func_ReadOneSector:
push bp
mov bp, sp
sub esp, 2
mov byte [bp - 2], cl
push bx
mov bl, [BPB_SecPerTrk]
div bl
inc ah
mov cl, ah
mov dh, al
shr al, 1
mov ch, al
and dh, 1
pop bx
mov dl, [BS_DrvNum]
Label_Go_On_Reading:
mov ah, 2
mov al, byte [bp - 2]
int 13h
jc Label_Go_On_Reading
add esp, 2
pop bp
ret
;==============================
同樣地,讀取扇區,也借助了BIOS的 0x13號中斷,功能號為 AH=0x02 ,該中斷服務各寄存器參數說明如下:
- INT 0x13 AH = 0x02: 讀取磁盤扇區
- AL = 讀入的扇區數(非0)
- CH = 磁道號(柱面號)的低8位
- CL = 扇區號1~63(
bit 0~5),磁道號(柱面號)的高兩位(bit 6~7,只對硬盤有效) - DH = 磁頭號
- DL=驅動器號(如果是硬盤驅動器,
bit 7必須被置位) - ES : BX => 數據緩沖區
- 出口參數:CF=0——操作成功,AH=00H,AL=傳輸的扇區數,否則,AH=狀態代碼(具體可以自行百度或Google)
模塊Func_ReadOneSector僅僅是對BIOS的0x13號中斷0x02號功能的封裝,以簡化磁盤讀取扇區的的操作過程,該模塊是需要參數的,參數說明如下:
- AX = 待讀取的磁盤起始扇區號
- CL= 讀入的扇區數量
- EX : BX => 數據緩沖區起始地址
需要說明的是,模塊Func_ReadOneSector的參數中傳入的磁盤扇區號是 LBA(Logical Block Address,邏輯塊尋址) 格式的,而BIOS的0x13號中斷0x02號功能只能接受 CHS(Cylinder/Head/Sector,柱面/磁頭/扇區) 格式的磁盤扇區號,那么就需要將LBA格式的轉換為CHS格式的,通過以下公式,便可以轉換:
- 模塊
Func_ReadOneSector一開始,先保存棧幀寄存器(bp)和棧寄存器(sp)中的數值(bp存儲在棧中,sp保存在bp寄存器中) - 然后從棧中開辟兩個字節的存儲空間,即是
sp向下移動兩個字節(sub esp, 2)。然后將CL中的內容(1個字節,參數,讀入扇區數)存入棧中,因為接下來要使用到CL寄存器。注:CL中的內容是一個字節,而棧項要對齊,所以要要用兩個字節保存一個字節的內容,即使浪費了一個字節。 - 然后就是把
BX的內容也保存到棧中,因為接下來要用到BX。 - 使用
AX寄存器(待讀取的磁盤起始扇區號)除以BL寄存器(每磁道扇區數),計算出目標磁道號(商:AL寄存器)和目標磁道內的起始扇區號(余數:AH寄存器),又因為磁道內的起始扇區號是從1開始計數的,故將此余數值加1(inc ah; mov cl, ah)緊接着按照以上公式計算出磁道號(即是柱面號)、磁頭號。 - 恢復
BX - 獲取驅動器號
BS_DrvNum - 設置
0x13中斷功能號AH=0x02,設置讀入扇區數mov al, byte [bp - 2](我們之前把參數CL讀入扇區數保存在了棧中,所以現在用bp去索引它)。 - 然后調用
0x13號中斷。 - 判斷是否讀取成功(即是若CF位為0,則成功),否則(CF=1)回到步驟
7。
LOADER.BIN文件搜索
當我們實現了讀取扇區的功能后,我們就可以實現文件搜索功能了:
; ================查找文件loader.bin================
;保存根目錄的起始扇區號([SectorNo]是一個臨時變量地址)
; 賦值為:SectorNumOfRootDirStart(根目錄的起始扇區號)
; 初始值為:19(前面已算出)
mov word [SectorNo], SectorNumOfRootDirStart
Lable_Search_In_Root_Dir_Begin:
; RootDirSizeForLoop也是一個臨時變量,值是:RootDirSectors(14)
; 判斷是否為0,如果是提示找不到loader.bin
cmp word [RootDirSizeForLoop], 0
jz Label_No_LoaderBin
; 查找的扇區減1
dec word [RootDirSizeForLoop]
;設置讀扇區模塊的參數 EX:BX、AX、CL
; 對應緩沖區起始、待讀取的磁盤起始扇區號、讀入扇區數量
mov ax, 00h
mov es, ax
mov bx, 8000h
mov ax, [SectorNo]
mov cl, 1
; 調用讀扇區模塊
call Func_ReadOneSector
; 開始查找loader.bin
mov si, LoaderFileName ;文件名源地址
mov di, 8000h ;文件名目標地址
cld ;DF復位=0,往高地址遞進
; dx記錄每個扇區可容納的目錄項目個數
mov dx, 10h
Label_Search_For_LoaderBin:
;如果dx為0,說明比較完了,開始讀取下一個根目錄扇區
cmp dx, 0
jz Label_Goto_Next_Sector_In_Root_Dir
;否則dx減1(目錄項減1)
dec dx
; cx保存目錄項文件名長度 11 bits
mov cx, 11
Label_Cmp_FileName:
; 判斷文件名是否比較完
; 即是cx是否為0,能比較完的話,說明已經找到了
cmp cx, 0
jz Label_FileName_Found
;否則沒有比較完,cx -= 1,開始比較下一個字符
dec cx
; 從DS:SI讀取一個字節到寄存器AL,然后SI += 1(取決於DF)
lodsb
; 比較目標字串中對應的字符與al中的字符,看是否相等
cmp al, byte [es:di]
; 如果相等,繼續比較下一個字符(跳轉到Label_Go_On)
jz Label_Go_On
; 如果不相等,就下一個文件項
jmp Label_Different
Label_Go_On:
; 目標字串指針加一,然后繼續比較
inc di
jmp Label_Cmp_FileName
Label_Different:
; di的低5位清零,
and di, 0ffe0h
; di += 32,表示指向下一個目錄項
add di, 20h
; si指向LoaderFileName的起始地址
mov si, LoaderFileName
; 繼續比較
jmp Label_Search_For_LoaderBin
Label_Goto_Next_Sector_In_Root_Dir:
; 在當前扇區找不到loader.bin
; 那么就在下一個扇區找,扇區號+1:[SectorNo]+1
add word [SectorNo], 1
jmp Lable_Search_In_Root_Dir_Begin
注意: FAT12文件系統的文件名是不分大小寫的,即是將小寫字母命名的文件賦值到FAT12文件系統內,文件系統也會為其創建大寫字母的文件名和目錄項。而小寫字母文件名只作為其顯示名,而真正的數據內容皆保存在對應的大寫字母目錄項。所以這里應該搜索大寫字母的文件名字符串。
找不到LOADER.BIN文件
當上面那一段程序的指令cmp word [RootDirSizeForLoop], 0判斷發現[RootDirSizeForLoop]中的數據為0的時候,說明沒有找到LOADER.BIN文件,那么就提示找不到LOADER.BIN。
; ========= 打印屏幕信息 : ERROR:No LOADER Found =========
Label_No_LoaderBin:
mov ax, 1301h
mov bx, 008ch
mov dx, 0100h
mov cx, 21
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, NoLoaderMessage
int 10h
jmp $
; =======================================================
當boot程序找不到LOADER.BIN文件會有如下所示的結果:

FAT表項解析
當找到loader.bin文件后,便可以根據FAT表項提供的簇號順序依次加載扇區數據到內存中,這個過程會涉及FAT表項的解析工作:
;==============================
; 模塊(可以理解為C語言中的函數)
; 根據當前FAT表項索引出下一個FAT表項
; 參數說明:
; AX = FAT表項號(輸入/輸出參數)
Func_GetFATEntry:
push es
push bx
push ax
mov ax, 00
mov es, ax
pop ax
; Odd為奇偶標志變量(奇數1,偶數0)
mov byte [Odd], 0
mov bx, 3
mul bx
mov bx, 2
div bx
; 判斷余數的奇偶性
cmp dx, 0
; 如果為偶數,則直接跳轉
jz Label_Even
mov byte [Odd], 1
Label_Even:
; 讀取FAT表扇區,總共兩個扇區
xor dx, dx
mov bx, [BPB_BytesPerSec]
div bx
push dx
mov bx, 8000h
add ax, SectorNumOfFAT1Start
mov cl, 2
call Func_ReadOneSector
pop dx
add bx, dx
mov ax, [es:bx]
cmp byte [Odd], 1
jnz Label_Even_2
shr ax, 4
Label_Even_2:
and ax, 0fffh
pop bx
pop es
ret
;==============================
- 該模塊需要的參數: AX = FAT表項號(輸入/輸出參數)
在這段程序中,首先把FAT表項號(AX)保存,並將奇偶標志變量置0,因為每個FAT表項占1.5B,並不是偶數對齊,所以就將FAT表項乘以3再除以2(也就是擴大1.5倍),然后判斷余數的奇偶性並保存在變量[Odd]中(奇數為1,偶數為0),再將計算結果除以每扇區字節數,商即為FAT表項的偏移扇區號,余數為FAT表項在扇區中的位置。然后通過Func_ReadOneSector模塊,連續讀入兩個扇區,這樣做的目的是,為了解決FAT表項橫跨兩個扇區的問題,最后根據奇偶標志變量[Odd]處理奇偶項錯位問題,即是奇數項向右移動4位。當然如果覺得麻煩的話,可以將FAT12文件系統換成FAT16文件系統。
從FAT12文件系統中加載loader.bin到內存
在完成Func_ReadOneSector和Func_GetFATEntry模塊后,就可以借助這兩個模塊把loader.bin文件的數據從軟盤扇區讀取到指定地址中:
; =======================================================
; 在根目錄中找到名為loader.bin的文件了
Label_FileName_Found:
;如果找到了的話,當前es:di指向的就是目標表項
; AX保存根目錄占用扇區數
mov ax, RootDirSectors
; 定位至表項起始簇號的位置(偏移0x1a)
and di, 0ffe0h
add di, 01ah
; 起始簇號保存到cx
mov cx, word [es:di]
push cx
; 計算數據區起始扇區號
; 數據區起始扇區號=根目錄起始扇區號+根目錄所占扇區數-2
add cx, ax
; SectorBalance = 根目錄起始扇區號 - 2
add cx, SectorBalance
; 配置loader的加載位置 EX:BX
mov ax, BaseOfLoader
mov es, ax
mov bx, OffsetOfLoader
; ax = loader起始扇區號
mov ax, cx
Label_Go_On_Loading_File:
; 顯示字符 '.'
push ax
push bx
mov ah, 0eh
mov al, '.'
mov bl, 0fh
int 10h
pop bx
pop ax
; 讀入loader
; 每讀入一個扇區就通過Func_GetFATEntry模塊獲取下一個FAT表項
; 然后繼續讀入數據
mov cl, 1
call Func_ReadOneSector
; 恢復簇號
pop ax
; 獲取下一個簇號
call Func_GetFATEntry
; 查看是否已經讀完了
cmp ax, 0fffh
jz Label_File_Loaded
; 否則保存當前獲取到簇號
push ax
; 計算loader文件的下一個簇的位置
mov dx, RootDirSectors
add ax, dx
add ax, SectorBalance
add bx, [BPB_BytesPerSec]
; 繼續讀
jmp Label_Go_On_Loading_File
Label_File_Loaded:
jmp $
; =======================================================
在Label_FileName_Found模塊中,程序首先會取得目錄DIR_FstClus字段的數值,並且通過配置ES:BX來指定loader程序在內存中的加載位置,再通過loader.bin程序的起始簇號計算出與其對應的扇區號。然后每讀一個扇區前就答應一個字符'.',每讀入一個扇區的數據就通過Func_GetFATEntry模塊獲取下一個FAT表項,直到Func_GetFATEntry返回0xfff為止,當全部讀完之后就跳轉到Label_File_Loaded處,准備執行loader.bin程序。
這段代碼中使用了BIOS中斷服務程序INT 0x10的AH=0x0e號功能,它能在屏幕上顯示一個字符詳細的寄存器參數如下:
- INT 0x10; AH=0x0e功能: 在屏幕上顯示一個字符。
- AL=待顯示字符
- BL=前景色
因為並未進入Loader程序的開發,所以就讓程序死循環在Label_File_Loaded這里。
剩下的部分
代碼剩下的部分就是臨時變量和日志信息字符串了:
; ==================臨時變量==================
RootDirSizeForLoop dw RootDirSectors
SectorNo dw 0
Odd db 0
; ==================打印信息==================
StartBootMessage: db "Start Boot"
NoLoaderMessage: db "ERROR:No LOADER Found"
LoaderFileName: db "LOADER BIN",0 ; loader文件名
注: NASM編譯器中的單引號和雙引號作用相同,並非C語言規定的雙引號會在結尾添加字符'\0',在NASM中必須自行添加。
再有就是,文件需要填滿512個字節(一個扇區大小),和末尾的0xaa55
; 扇區剩下的字節全部填滿0(除了最后兩個字節)
times 510 - ($ - $$) db 0
dw 0xaa55
總體代碼
所以總的代碼看起來應該是這樣的:
; File name: boot.asm
; org 用於指定程序的起始地址
; 如果不使用org指定程序的起始地址
; 那么編譯器會把0x0000作為程序的起始地址。
org 0x7c00
; 設置棧指針的位置
BaseOfStack equ 0x7c00
; Loader的位置(20位)
; BaseOfLoader << 4 + OffsetOfLoader = 0x10000
BaseOfLoader equ 0x1000
OffsetOfLoader equ 0x00
; 根目錄占用的扇區數:(BPB_RootEntCnt * 32 + BPB_BytesPerSec - 1) / BPB_BytesPerSec
; (224 * 32 + 512 - 1) / 512 = 14
RootDirSectors equ 14
; 根目錄的起始扇區號 =
; 保留扇區數(BPB_RsvdSecCnt) + FAT表扇區數(BPB_FATSz16) + FAT表份數(BPB_NumFATs) = 1 + 9 * 2 = 19
SectorNumOfRootDirStart equ 19
; FAT表的起始扇區號,引導扇區的扇區號是0
SectorNumOfFAT1Start equ 1
; 用於平衡文件(或目錄)的起始簇號與數據區起始簇號的差值。
SectorBalance equ 17
;****************************************************
; 真正的代碼在這開始,前面的都相當於宏定義
; 程序一開始就跳轉到Label_Start的位置處執行
jmp short Label_Start
nop
;****************************************************
; 生產廠商名(8 Bits)
BS_OEMName db 'Testboot'
; 每扇區字節數
BPB_BytesPerSec dw 512
; 每簇扇區數
BPB_SecPerClus db 1
; 保留扇區數
BPB_RsvdSecCnt dw 1
; FAT表的份數
BPB_NumFATs db 2
; 根目錄可容納的的目錄項數
BPB_RootEntCnt dw 224
; 總扇區數(因為使用的是1.44M的軟盤)
BPB_TotSec16 dw 2880
; 介質描述符
BPB_Media db 0xf0
; 每FAT扇區數
BPB_FATSz16 dw 9
; 每磁道扇區數
BPB_SecPerTrk dw 18
; 磁頭數
BPB_NumHeads dw 2
; 隱藏扇區數
BPB_HiddSec dd 0
; 如果BPB_FATSz16=0,那么這個值記錄扇區數
BPB_TotSec32 dd 0
; int 0x13的驅動器號
BS_DrvNum db 0
; 未使用(不用管)
BS_Reserved1 db 0
; 擴展引導標記
BS_BootSig db 0x29
; 卷序列號
BS_VolID dd 0
; 卷標(11 Bits)
BS_VolLab db 'boot loader'
; 文件系統類型(8 Bits)
BS_FileSysType db 'FAT12 '
Label_Start:
; 初始化一些段寄存器和棧指針
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, BaseOfStack
; 清屏
mov ax, 0600h
mov bx, 0700h
mov cx, 0
mov dx, 184fh
int 10h ; 調用0x10號中斷
; 設置光標位置
mov ax, 0200h
mov bx, 0000h
mov dx, 0000h
int 10h
; 打印字符:Start Bootint......
mov ax, 1301h
mov bx, 000fh
mov dx, 0000h
mov cx, 10
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, StartBootMessage
int 10h
; 初始化軟盤驅動器
xor ah, ah
xor dl, dl
int 13h
; ================查找文件loader.bin================
;保存根目錄的起始扇區號([SectorNo]是一個臨時變量地址)
; 賦值為:SectorNumOfRootDirStart(根目錄的起始扇區號)
; 初始值為:19(前面已算出)
mov word [SectorNo], SectorNumOfRootDirStart
Lable_Search_In_Root_Dir_Begin:
; RootDirSizeForLoop也是一個臨時變量,值是:RootDirSectors(14)
; 判斷是否為0,如果是提示找不到loader.bin
cmp word [RootDirSizeForLoop], 0
jz Label_No_LoaderBin
; 查找的扇區減1
dec word [RootDirSizeForLoop]
;設置讀扇區模塊的參數 EX:BX、AX、CL
; 對應緩沖區起始、待讀取的磁盤起始扇區號、讀入扇區數量
mov ax, 00h
mov es, ax
mov bx, 8000h
mov ax, [SectorNo]
mov cl, 1
; 調用讀扇區模塊
call Func_ReadOneSector
; 開始查找loader.bin
mov si, LoaderFileName ;文件名源地址
mov di, 8000h ;文件名目標地址
cld ;DF復位=0,往高地址遞進
; dx記錄每個扇區可容納的目錄項目個數
mov dx, 10h
Label_Search_For_LoaderBin:
;如果dx為0,說明比較完了,開始讀取下一個根目錄扇區
cmp dx, 0
jz Label_Goto_Next_Sector_In_Root_Dir
;否則dx減1(目錄項減1)
dec dx
; cx保存目錄項文件名長度 11 bits
mov cx, 11
Label_Cmp_FileName:
; 判斷文件名是否比較完
; 即是cx是否為0,能比較完的話,說明已經找到了
cmp cx, 0
jz Label_FileName_Found
;否則沒有比較完,cx -= 1,開始比較下一個字符
dec cx
; 從DS:SI讀取一個字節到寄存器AL,然后SI += 1(取決於DF)
lodsb
; 比較目標字串中對應的字符與al中的字符,看是否相等
cmp al, byte [es:di]
; 如果相等,繼續比較下一個字符(跳轉到Label_Go_On)
jz Label_Go_On
; 如果不相等,就下一個文件項
jmp Label_Different
Label_Go_On:
; 目標字串指針加一,然后繼續比較
inc di
jmp Label_Cmp_FileName
Label_Different:
; di的低5位清零,
and di, 0ffe0h
; di += 32,表示指向下一個目錄項
add di, 20h
; si指向LoaderFileName的起始地址
mov si, LoaderFileName
; 繼續比較
jmp Label_Search_For_LoaderBin
Label_Goto_Next_Sector_In_Root_Dir:
; 在當前扇區找不到loader.bin
; 那么就在下一個扇區找,扇區號+1:[SectorNo]+1
add word [SectorNo], 1
jmp Lable_Search_In_Root_Dir_Begin
; ========= 打印屏幕信息 : ERROR:No LOADER Found =========
Label_No_LoaderBin:
mov ax, 1301h
mov bx, 008ch
mov dx, 0100h
mov cx, 21
push ax
mov ax, ds
mov es, ax
pop ax
mov bp, NoLoaderMessage
int 10h
jmp $
; =======================================================
; =======================================================
; 在根目錄中找到名為loader.bin的文件了
Label_FileName_Found:
;如果找到了的話,當前es:di指向的就是目標表項
; AX保存根目錄占用扇區數
mov ax, RootDirSectors
; 定位至表項起始簇號的位置(偏移0x1a)
and di, 0ffe0h
add di, 01ah
; 起始簇號保存到cx
mov cx, word [es:di]
push cx
; 計算數據區起始扇區號
; 數據區起始扇區號=根目錄起始扇區號+根目錄所占扇區數-2
add cx, ax
; SectorBalance = 根目錄起始扇區號 - 2
add cx, SectorBalance
; 配置loader的加載位置 EX:BX
mov ax, BaseOfLoader
mov es, ax
mov bx, OffsetOfLoader
; ax = loader起始扇區號
mov ax, cx
Label_Go_On_Loading_File:
; 顯示字符 '.'
push ax
push bx
mov ah, 0eh
mov al, '.'
mov bl, 0fh
int 10h
pop bx
pop ax
; 讀入loader
; 每讀入一個扇區就通過Func_GetFATEntry模塊獲取下一個FAT表項
; 然后繼續讀入數據
mov cl, 1
call Func_ReadOneSector
; 恢復簇號
pop ax
; 獲取下一個簇號
call Func_GetFATEntry
; 查看是否已經讀完了
cmp ax, 0fffh
jz Label_File_Loaded
; 否則保存當前獲取到簇號
push ax
; 計算loader文件的下一個簇的位置
mov dx, RootDirSectors
add ax, dx
add ax, SectorBalance
add bx, [BPB_BytesPerSec]
; 繼續讀
jmp Label_Go_On_Loading_File
Label_File_Loaded:
jmp $
; =======================================================
;==============================
; 模塊(可以理解為C語言中的函數)
; 從軟驅中讀取扇區
; 參數說明:
; AX = 待讀取的磁盤起始扇區號
; CL= 讀入的扇區數量
; EX : BX => 數據緩沖區起始地址
Func_ReadOneSector:
push bp
mov bp, sp
sub esp, 2
mov byte [bp - 2], cl
push bx
mov bl, [BPB_SecPerTrk]
div bl
inc ah
mov cl, ah
mov dh, al
shr al, 1
mov ch, al
and dh, 1
pop bx
mov dl, [BS_DrvNum]
Label_Go_On_Reading:
mov ah, 2
mov al, byte [bp - 2]
int 13h
jc Label_Go_On_Reading
add esp, 2
pop bp
ret
;==============================
;==============================
; 模塊(可以理解為C語言中的函數)
; 根據當前FAT表項索引出下一個FAT表項
; 參數說明:
; AX = FAT表項號(輸入/輸出參數)
Func_GetFATEntry:
push es
push bx
push ax
mov ax, 00
mov es, ax
pop ax
; Odd為奇偶標志變量(奇數1,偶數0)
mov byte [Odd], 0
mov bx, 3
mul bx
mov bx, 2
div bx
; 判斷余數的奇偶性
cmp dx, 0
; 如果為偶數,則直接跳轉
jz Label_Even
mov byte [Odd], 1
Label_Even:
; 讀取FAT表扇區,總共兩個扇區
xor dx, dx
mov bx, [BPB_BytesPerSec]
div bx
push dx
mov bx, 8000h
add ax, SectorNumOfFAT1Start
mov cl, 2
call Func_ReadOneSector
pop dx
add bx, dx
mov ax, [es:bx]
cmp byte [Odd], 1
jnz Label_Even_2
shr ax, 4
Label_Even_2:
and ax, 0fffh
pop bx
pop es
ret
;==============================
; ==================臨時變量==================
RootDirSizeForLoop dw RootDirSectors
SectorNo dw 0
Odd db 0
; ==================打印信息==================
StartBootMessage: db "Start Boot"
NoLoaderMessage: db "ERROR:No LOADER Found"
LoaderFileName: db "LOADER BIN",0 ; loader文件名
; 扇區剩下的字節全部填滿0(除了最后兩個字節)
times 510 - ($ - $$) db 0
dw 0xaa55
編譯運行:
(需要配置環境,之前那篇文章已經配置過了,這里不再重復)
大概的執行步驟是這樣的:

-
把上面的代碼保存為
boot.asm。 -
使用NAME編譯它:
> nasm ./boot.asm -o boot.bin
- 使用
dd把內容寫進boot.img中去:
> dd if=boot.bin of=boot.img bs=512 count=1
使用VSCode的Hexdump擴展查看是否已經寫入進去:

- 啟動虛擬機,因為之前已經配置過環境,所以Bochs虛擬機在彈出窗口后,直接點
Start按鈕或者回車就行了。
> bochs -f .\bochsrc.bxrc
- 查看結果:

因為還沒有編寫loader.bin所以,結果會提示如上圖所示(紅色部分應該是閃爍的,但是由於是圖片,無法展示)。
那么對於讀取Loader文件的功能模塊要等到編寫好Loader文件才能再進行測試了。
- 參考文獻:
[1] 田宇.一個64位操作系統的設計與實現[M].北京:人民郵電出版社,2018.5.
