我們在上節博客中學習瞭如何進行主引導程序的 512 字節的擴展,那麼我們本節就繼續來學習下如何進行控制權的交接。就是將控制權由主引導程序交由下一個將要執行的程序,類似於嵌入式中的 uboot 在啓動內核的時候將控制權由 uboot 交由 kernel。下來我們先來看看 BootLoader 的內存佈局,如下所示
我們看到在 0x7c00 前還有一段預留的空間,那麼這段空間就是用來存放棧信息的。在主引導程序的 512 字節之後,緊接着就是 Fat 表,它大小爲 4kb。從 0x9000 地址之後便全部爲 Loader 了,也就是我們交由控制權的地方了。我們來看看通過 FAT 表是如何來加載文件內容的,如下圖所示
我們看到,先指定 FAT 表的地址,然後指定 DIR_FstClus 成員的入口地址,再間接賦給 dx 寄存器。這個 0xFF7 是哪來的呢?在我們之前用 Qt 編寫的代碼中就指定了這個大小,這是規定的,後面的流程也是我們之前實現過的流程。我們就來做個實驗:1、在虛擬軟盤中創建體積較大的文本文件(Loader);2、將 Loader 的內容加載到 BaseOfLoader 地址處;3、打印 Loader 中的文本(用來判斷加載是否完全)。具體源碼如下
start: mov ax, cs mov ss, ax mov ds, ax mov es, ax mov sp, BaseOfStack mov ax, RootEntryOffset mov cx, RootEntryLength mov bx, Buf call ReadSector mov si, Target mov cx, TarLen mov dx, 0 call FindEntry cmp dx, 0 jz output mov si, bx ; 將起始地址放到 si 中 mov di, EntryItem mov cx, EntryItemLength call MemCpy ; 計算 Fat 表所佔用的內存 mov ax, FatEntryLength mov cx, [BPB_BytsPerSec] mul cx ; 將所佔用的內存大小結果保存到 ax 中 mov bx, BaseOfLoader sub bx, ax ; bx 就是 Fat 表在內存中的起始位置了 mov ax, FatEntryOffset mov cx, FatEntryLength call ReadSector mov dx, [EntryItem + 0x1A] ; 獲取目標起始處的位置 mov si, BaseOfLoader loading: mov ax, dx add ax, 31 mov cx, 1 push dx push bx mov bx, si call ReadSector pop bx pop cx call FatVec cmp dx, 0xFF7 jnb output add si, 512 jmp loading output: mov bp, BaseOfLoader mov cx, [EntryItem + 0x1c] call Print last: hlt jmp last
我們來編譯看看
我們看到 make 直接報錯,原因是整個主引導程序的大小超出 512 字節的範圍了。那麼此時我們該怎麼辦呢?我們就有必要將之前的 push 和 pop 入棧出棧的操作進行刪除了,那麼我們之前爲何要這樣做呢?是爲了遵守彙編代碼的約定,有操作相關寄存器的值就要進行入棧出棧操作。那麼我們這塊內存已經不夠,因此沒必要進行這個操作了。我們將下面的入棧出棧操作進行刪除,但是要在 FindEntry 這個函數保留 cx 寄存器的入棧出棧的操作,原因是下面不停在改變 cx 寄存器的值。我們在 find 操作中,call MemCmp 操作前後有必要再加上 si 寄存器的入棧出棧操作。我們修改完再來進行 make 看看,是否還會出問題
我們看到已經編譯成功,我們來 bochs 調試下,看看 loader 文本的內容是什麼
我們看到打印了好多的 D.T.Software。我們來掛載下 data.img 看看 loader 文本的內容是否如此
那麼我們將 loader 文本的內容改爲我們剛纔編寫的 boot.asm 的內容,順便看看它的文件所佔內存是多大的,如果大於 512 字節還能正常進行讀取並顯示,那麼就說明我們所編寫的功能是沒有問題的。
我們看到這個文本的內存是 8 kb,那麼它早就超過 512 字節了。看看最後打印的是不是 db 0x55, 0xaa,結果如下
我們看到打印的確實是我們剛纔改的內容,也就證明了我們編寫的代碼是正確的。下來我們來講講由 boot 主引導程交由後的第一個程序 Loader:它的起始地址是 0x9000(org 0x9000),通過 int 0x10 號中斷在屏幕上打印字符串。在編寫 loader.asm 源碼之前,我們先來介紹下相關的彙編知識。我們在之前使用了 jz 指令,表示跳轉,其實 jxx 代表了一個指令族,功能是根據標誌位進行調整,具體如下
loader.asm 源碼如下
org 0x9000 begin: mov si, msg print: mov al, [si] add si, 1 cmp al, 0x00 je end mov ah, 0x0E mov bx, 0x0F int 0x10 jmp print end: hlt jmp end msg: db 0x0a, 0x0a db "Hello, D.T.OS!" db 0x0a, 0x0a db 0x00
我們在上面的代碼中使用了 je,我們通過反彙編來看看它在內部是怎樣實現的,如下
我們看到在編譯器的內部是將 je 指令當做 jz 指令使用了。我們將 loader.asm 編譯成 loader,然後將它拷貝至 data.img 中。看看運行的效果
我們看到已經打印出 Hell, D.T.OS! ;我們再將此時的 data.img 放在 window 中,將它作爲軟盤在我們所創建的虛擬機上,看看效果
我們看到已經成功輸出我們所打印的字符串了,雖然效果和我們之前所實現的是一樣的。但是此時已經發生了質的變化,此時的 loader.asm 文本大小不再有 512 字節的限制。換句話說,我們可以編寫更多的內核機制了。那麼我們在完成之後也要將之前的 makefile 重寫下,要不然每次都要手動編譯 loader.asm 程序。改動後的具體源碼如下
.PHONY : all clean rebuild BOOT_SRC := boot.asm BOOT_OUT := boot.bin LOADER_SRC := loader.asm LOADER_OUT := loader IMG := data.img IMG_PATH := /root/DT/wei RM := rm -rf all : $(BOOT_OUT) $(LOADER_OUT) @echo "Build Success ==> D.T.OS!" $(IMG) : bximage $@ -q -fd -size=1.44 $(BOOT_OUT) : $(BOOT_SRC) nasm $^ -o $@ dd if=$(BOOT_OUT) of=$(IMG) bs=512 count=1 conv=notrunc $(LOADER_OUT) : $(LOADER_SRC) nasm $^ -o $@ mount -o loop $(IMG) $(IMG_PATH) cp $@ $(IMG_PATH)/$@ umount $(IMG_PATH) clean : $(RM) $(IMG) $(BOOT_OUT) $(LOADER_OUT) rebuild : @$(MAKE) clean @$(MAKE) all
我們再來重新 make 下,看看效果
我們將輸出字符串在 loader.asm 中改爲 Hello, world!看看效果
我們看到已經成功改寫,那麼我們再來測試下對原來的功能有沒有造成什麼影響。將原來的 LOADER 在後面加個 a ,看看是否會因查找不到而輸出 No LOADER ... 提示性字符串
我們看到已經輸出了提示性的字符串,證明我們添加的功能以及改寫的 makefile 對原來功能並沒有造成任何影響。通過今天對控制權交接的學習,總結如下:1、boot 需要在進行重構保證在 512 字節內完成功能;2、在彙編程序中儘量確保函數調用前後通用寄存器的狀態不變;3、boot 成功加載 loader 後將控制權轉移;4、loader 程序沒有代碼體積的限制。