從零開始寫 OS 內核 - BIOS 啓動到實模式

系列目錄

接上一篇準備工做,從這篇開始咱們將進入 boot loader 的編寫。網上有一些相似的教程可能跳過了這個階段,直接爲你準備好了 boot loader,從而你能夠直接開始 kernel 的編寫,例如以前推薦的 JamesM's kernel development tutorials 就是這樣的。不過我仍是強烈建議將 boot loader 也本身實現了,尤爲是對初學者,緣由以下:java

  • 它並不困難,相比於後面的 kernel;
  • 有助於你快速提升彙編能力,這在後面的 C 語言 kernel 編寫、調試中仍然很重要;
  • boot 裸機運行階段的的編程有助於你創建起對磁盤、內存、指令和 data 之間的加載、映射關係的正確認識,爲後面的內核、可執行程序的加載,以及虛擬內存的創建作好準備,尤爲是若是你感受對這一塊比較模糊的話;
  • boot 階段其實會初步搭建起 segment 以及虛擬內存的框架,爲後續 kernel 編寫打下基礎;

開機進入 BIOS

這是一個經典的問題,就是計算機主板開機上電後到啓動,發生了什麼?git

首先咱們須要知道開機後 CPU 和內存所處的狀態,開機後 CPU 初始模式是實模式,地址寬度爲 20 位,即最大地址空間 1MB。這 1MB 空間的劃分是固定的,每一塊都有規定的用途的,被映射到不一樣的設備上:shell

BIOS 的工做

咱們來看一下開機後發生的事情:編程

  1. 開機後 CPU 的指令寄存器 ip 被強置爲地址 0xFFFF0,這一地址被映射到 BIOS 固件上的代碼,這就是計算機開機後的第一條指令的地址;
  2. CPU 開始執行 BIOS 上的代碼,這一部分主要是硬件輸入輸出設備相關的檢查,以及創建一個最初的中斷向量表,目前沒必要深究;
  3. BIOS 代碼最後階段的工做,就是檢查啓動盤上的 mbr 分區,所謂 mbr 分區就是磁盤上的第一個 512B 內容,又叫引導分區;BIOS 會對這 512B 作一個檢查:它的最後2個字節必須是兩個 magic number:0x550xaa,不然它就不是一個合法的啓動盤;
  4. 檢查經過後,BIOS 將這 512B 加載到內存 0x7C00 處,到 0x7E00 爲止,而後指令跳轉到 0x7C00 開始執行;至此 BIOS 退出舞臺;

將上面那張表格畫成圖,去掉干擾項,只留下咱們關心的部分:segmentfault

  • 黃色部分是加載到內存的 mbr,起始地址 0x7C00;
  • 白色部分是咱們後面能夠自由使用的內存空間;
  • 斜線陰影部分爲 BIOS 代碼;

圖中標出了 BIOS 的主要工做流程,從地址 0xFFFF0 開始,通過一系列代碼執行,最終校驗並讀取磁盤第一個 512B 扇區,加載到黃色部分即爲 mbr,地址爲 0x7C00,而後指令跳轉過去,進入 mbr 的執行;bash

mbr 的工做

那麼 mbr 須要作什麼事情?由於 mbr 大小被限制在了 512B,你不可能在裏面放不少代碼和數據,因此它最重要的工做只有一個:多線程

  • 將後面的 loader 部分從磁盤加載到內存,並跳轉到 loader 繼續執行;

內存佈局規劃

因此咱們須要規劃一下整個 boot load 階段的內存佈局,這裏咱們直接給出磁盤以及內存的全貌:框架

咱們目前重點關注內存 1MB 如下部分的內容,不一樣部分用了不一樣的顏色標識出來:函數

  • 黃色:mbr
  • 藍色:loader
  • 白色:可自由使用

通過 BIOS 的工做,如今指令已經來到了 mbr 部分,它須要將藍色部分的 loader 從磁盤上加載到內存,地址就定爲 0x8000,注意這個地址能夠自由指定的,只要在圖中白色區域內,而且空間足夠用便可。咱們的 loader 部分也不會很大,按照比較富餘地估計,4KB 足以。

mbr 代碼

先給出我項目裏的代碼,路徑爲 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 的末尾處寫上 0x550xaa 兩個 magic number:

times 510-($-$$) db 0
db 0x55, 0xaa

至此 mbr 便編碼完成了,很是短小簡單。接下來咱們須要將它編譯而且製做成啓動鏡像,加載到 Bochs 裏運行。

運行 mbr

首先你須要製做一個磁盤鏡像文件,這裏又用到了 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 調試,能快速地幫助你創建相關的認知。

相關文章
相關標籤/搜索