運行 shellhtml
接上一篇準備工做,從這篇開始咱們將進入 boot loader 的編寫。網上有一些相似的教程可能跳過了這個階段,直接爲你準備好了 boot loader,從而你能夠直接開始 kernel 的編寫,例如以前推薦的 JamesM's kernel development tutorials 就是這樣的。不過我仍是強烈建議將 boot loader 也本身實現了,尤爲是對初學者,緣由以下:java
這是一個經典的問題,就是計算機主板開機上電後到啓動,發生了什麼?git
首先咱們須要知道開機後 CPU 和內存所處的狀態,開機後 CPU 初始模式是實模式,地址寬度爲 20 位,即最大地址空間 1MB。這 1MB 空間的劃分是固定的,每一塊都有規定的用途的,被映射到不一樣的設備上:shell
咱們來看一下開機後發生的事情:編程
0xFFFF0
,這一地址被映射到 BIOS 固件上的代碼,這就是計算機開機後的第一條指令的地址;mbr
分區,所謂 mbr 分區就是磁盤上的第一個 512B 內容,又叫引導分區
;BIOS 會對這 512B 作一個檢查:它的最後2個字節必須是兩個 magic number:0x55
和 0xaa
,不然它就不是一個合法的啓動盤;0x7C00
處,到 0x7E00 爲止,而後指令跳轉到 0x7C00 開始執行;至此 BIOS 退出舞臺;將上面那張表格畫成圖,去掉干擾項,只留下咱們關心的部分:segmentfault
圖中標出了 BIOS 的主要工做流程,從地址 0xFFFF0 開始,通過一系列代碼執行,最終校驗並讀取磁盤第一個 512B 扇區,加載到黃色部分即爲 mbr,地址爲 0x7C00,而後指令跳轉過去,進入 mbr 的執行;bash
那麼 mbr 須要作什麼事情?由於 mbr 大小被限制在了 512B,你不可能在裏面放不少代碼和數據,因此它最重要的工做只有一個:多線程
因此咱們須要規劃一下整個 boot load 階段的內存佈局,這裏咱們直接給出磁盤以及內存的全貌:框架
咱們目前重點關注內存 1MB 如下部分的內容,不一樣部分用了不一樣的顏色標識出來:函數
通過 BIOS 的工做,如今指令已經來到了 mbr 部分,它須要將藍色部分的 loader 從磁盤上加載到內存,地址就定爲 0x8000,注意這個地址能夠自由指定的,只要在圖中白色區域內,而且空間足夠用便可。咱們的 loader 部分也不會很大,按照比較富餘地估計,4KB 足以。
先給出我項目裏的代碼,路徑爲 src/boot/mbr.S,供你參考。
首先關注開始部分:
SECTION mbr vstart=MBR_BASE_ADDR
MBR_BASE_ADDR
定義在了 boot.inc
中,爲 0x7C00,這表示了整個 mbr 裏的內容都是從 0x7C00 開始編址,包括代碼和數據。這一點很是重要,由於咱們已經提早知道了 BIOS 會將 mbr 加載到這個位置,因此整個 mbr 裏的內容的編址必須從這裏開始,這樣 BIOS 在跳轉到 mbr 的第一條指令後,後續對 mbr 中代碼和數據的訪問才能正確尋址。
mbr 的入口我標記爲了 mbr_entry
,後面我定義了幾個函數,後面的講解咱們不妨用 C 語言給它作註釋:
void init_segments();
這裏初始化了幾個 segment 寄存器,初始值均爲 0,這表示 flat mode 的段內存分佈方式,如今你也沒必要深究。另外我還將 stack 移動到了 0x7B00 的位置,這只是自由發揮,徹底不是必須的。
接下來加載 loader:
void load_loader_img();
// 這個函數的彙編代碼直接使用寄存器傳參。 void read_disk(short load_mem_addr, short load_start_sector, short sectors_num);
這裏就是 mbr 最主要的工做,把 loader 從磁盤上加載進來到內存 0x8000 的位置,read_disk
三個參數傳參分別爲:
// loader 加載地址爲 0x8000; short load_mem_addr = LOADER_BASE_ADDR; // loader 鏡像在磁盤上起始位置爲第 2 個sector,緊接着 mbr 以後; short load_start_sector = 1; // loader 大小爲 8 個 sectors,共 4KB; short sectors_num = 8;
read_disk 函數涉及到了讀取磁盤,須要用到一堆 CPU 控制磁盤的端口和中斷功能,你須要查閱文檔使用,冗長繁雜,我是照搬了《操做系統真相還原》一書第三章的內容。你其實也沒必要深究,拿來用就能夠,只須要知道它作了什麼工做便可。
加載完 loader 以後,就能夠跳轉到 loader 地址 0x8000
執行:
jmp LOADER_BASE_ADDR
整個從 BIOS -> mbr -> loader 的指令運行跳轉流程以下圖所示。loader 部分用淺藍色陰影標出,由於它實際上目前沒有有效數據,等待咱們後續將它實現並加載入內存:
最後還有個關鍵的小東西:
這一通代碼下來,所用的空間還遠未到 512B,咱們將剩餘的空間所有用 0 填充(其實隨便填什麼都行,反正執行不到),最後在 512B 的末尾處寫上 0x55
和 0xaa
兩個 magic number:
times 510-($-$$) db 0 db 0x55, 0xaa
至此 mbr 便編碼完成了,很是短小簡單。接下來咱們須要將它編譯而且製做成啓動鏡像,加載到 Bochs 裏運行。
首先你須要製做一個磁盤鏡像文件,這裏又用到了 Bochs 自帶的 bximage
這個命令行工具:
>> bximage -hd -mode="flat" -size=3 -q scroll.img 1>/dev/null
它其實就是產生了一個 3MB 的寫滿了 0 的文件,3MB 的大小的磁盤對於咱們的項目已經足夠容納 mbr,boot,kernel 以及其它用戶程序等全部數據。bximage
的打印日誌還會告訴你,應該給配置文件 bochsrc.txt 裏的磁盤設置什麼參數,很方便。
接下來使用 nasm 編譯 mbr.S:
nasm -o mbr mbr.S
而後你就獲得一個 512B 大小的 mbr 文件。接下來將它刻寫進磁盤鏡像文件,這裏用到了 dd
這個命令:
dd if=mbr of=scroll.img bs=512 count=1 seek=0 conv=notrunc
注意到這裏把 mbr 寫到了磁盤鏡像文件的第一個扇區(512B)。
如今咱們獲得一個這樣的磁盤鏡像文件:
而後你就能夠把磁盤鏡像文件加載到 Bochs 裏運行了,和以前同樣:
bochs -f bochsrc.txt
不過在此以前,mbr 最好先作一個小小的改動。由於此時咱們鏡像裏尚未任何 loader 內容,加載完的 loader 其實全是 0,這不是能夠執行的代碼,所以 mbr 的最後一條指令 jmp LOADER_BASE_ADDR
以後 CPU 就會掛掉,因此你能夠在這條指令以前加一句 jmp $
,這至關因而死循環 while (true) {}
,讓程序懸停在這裏,你就能夠暫停 Bochs 而後看它是否是停在這條指令了,若是是的話,說明 mbr 的運行已經成功了。
mbr 短小精悍,自己沒有太多難點在裏面,不過完事開頭難,做爲整個內核鏡像的開篇,咱們須要開始提早對整個內存的佈局進行謀劃。若是是對彙編,指令,內存等在裸機上運行的原理還不太熟悉的同窗,mbr 也是一個很是好的練手機會,建議你多對照着反編譯後 mbr 代碼,以及 Bochs 調試,能快速地幫助你創建相關的認知。