接上一篇 虛擬內存初探,本篇將正式加載並啓動 kernel,也就是圖中綠色的部分:git
固然 kernel 鏡像要從磁盤上讀取加載,因此這裏回顧一張老圖,是 disk
和 memory
(物理內存)的數據對應關係:shell
順便提一下,上圖中斜線陰影打問號的部分,就是上一章講的 kernel page tables
,即第一張圖的橙色部分,共 256 張佔地 1MB。segmentfault
回到 kernel
,即圖中綠色部分,它如今實際上還不存在,因此首先咱們須要實現、編譯一個簡單的 demo 性質的 kernel。若是對 kernel 是什麼尚未概念的同窗,可能會問:到底 kernel 長什麼樣?bash
答案很是簡單:kernel 和你平時用 C 語言寫的可執行程序幾乎沒有任何區別,也是從一個 main 函數開始。多線程
下面咱們就實現咱們的第一個 kernel:函數
void main() { while (1) {} }
就是這樣簡單,除了一個 while
循環,沒有任何其它東西,但它足以用做咱們這裏的 demo。gitlab
這裏有不少編譯參數,例如以 32 位編碼,禁用 C 標準庫等(這是咱們本身定製的 OS,和 C 標準庫不可能兼容)。ui
gcc -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector -no-pie -fno-pic -c main.c -o main.o
ld -m elf_i386 -Tlink.ld -o kernel main.o
這裏會用到一個 link 配置文件 link.ld
:編碼
ENTRY(main) SECTIONS { .text 0xC0800000: { code = .; _code = .; __code = .; *(.text) } .data ALIGN(4096): { data = .; _data = .; __data = .; *(.data) *(.rodata) } .bss ALIGN(4096): { bss = .; _bss = .; __bss = .; *(.bss) . = ALIGN(4096); } end = .; _end = .; __end = .; }
這裏最重要的就是定義了 text
段的起始地址 0xC0800000
,也是整個 kernel 編址的起始。若是你還記得上一篇的內容,咱們規劃了 kernel 空間的虛擬內存分佈:spa
0xC0800000
將是 kernel 的入口地址,由於 text
段會被加載到此處,日後依次是 data
,bss
等段。loader
結束後將會跳轉到該地址。
另外上面還定義了整個可執行文件的入口函數爲 main
。
編譯連接後的 kernel 是一個 ELF 格式的二進制,咱們不妨將它反彙編 dump 看一下:
objdump -dsx kernel
能夠看到 main
函數的地址爲 0xC080000
,這是進入 kernel 後的第一條指令。
dd if=kernel of=scroll.img bs=512 count=2048 seek=9 conv=notrunc
seek=9
是由於前面 mbr
和 loader
已經在磁盤上佔據了前 9 個 sectors。這裏 kernel 大小爲 2048 個 sectors 共 1MB,對於咱們這個項目而言已經足夠大了,徹底夠用。
如今磁盤鏡像終於變成了這樣:
鏡像準備完畢,接下來就能夠將 kernel 讀取而且加載了。首先仍是給出代碼連接 init_kernel,供你參考。
和以前 mbr
和 loader
的加載
不一樣,這裏將讀取
和加載
兩個詞分開,是由於它們是兩個步驟:
section
複製到它們被 編址 的地方;首先來看第一步「讀取」。咱們選擇的是虛擬內存頂部的 1MB,即 (0xFFFFFFFF - 1MB) ~0xFFFFFFFF
的 1MB 空間做爲二進制鏡像的存放地址,固然這徹底是我的選擇,我選這裏是由於這裏目前不會有人打擾;固然也要爲它分配相應的物理頁 frames
,在 page table
中創建映射,因此我也從剩下的物理內存空間裏找了 1MB 空閒位置出來給它映射上去;而後就能夠像以前讀取 mbr 和 loader 同樣,將 kernel 鏡像讀取進來。
接下來是第二步「加載」。這裏涉及到了根據 ELF 文件格式的規範進行解析,須要你花點時間瞭解相關文檔,主要就是從 program header table
中獲取每一個 section
的位置和大小,以及加載的內存地址(固然是 virtual 地址),而後將數據 copy 過去。這一次加載的內存地址,纔是 0xC0800000
開始的位置。固然在 copy 以前,固然要爲它們預先分配好 frames 而且在 page table
中創建好內存映射。這一切工做都在 allocate_pages_for_kernel 這個函數中提早完成了。
一切準備就緒,接下來就能夠真正進入 kernel 了:
init_kernel: call allocate_pages_for_kernel call load_hd_kernel_image call do_load_kernel ; init floating point unit before entering the kernel finit ; move stack to 0xF0000000 mov esp, KERNEL_STACK_TOP - 16 mov ebp, esp ; let's jump to kernel entry :) jmp eax ret
首先初始化了 CPU 的浮點數單元,防止它後面異常。
而後我將 stack
移到了比較高的地址 0xF0000000
位置,這固然不是必須的,徹底是我我的選擇。當前的 stack 位置其實也很不錯(大約在 0xC0007B00
如下附近的位置,其中 0x7B00
這是在 mbr 中轉移過去的,而打開 paging 後咱們用 0xC0000000 + 0x7B00
訪問,若是你還記得的話)。只是我但願後面進入 kernel 之後的 stack 位置能被移到 一個全新的地方,因此才這麼多作了一步。stack 的位置是比較靈活的,只要是一個閒置的,不會受到干擾的地方就能夠。
而後很是簡單,jmp eax
一條指令跳到了 kernel
入口處。
爲何是 eax
?這是上面函數 do_load_kernel
的返回值,這個函數就是咱們解析加載 kernel 的 ELF 二進制的函數,它會返回值 kernel 的入口地址,即 main
函數地址,這個地址是由 ELF 文件中 ELF Header
的 e_entry
字段給出的。ELF 可執行二進制的入口地址是在連接階段肯定的,它其實是由以前的 link.ld
裏的 ENTRY(main)
指定的。
順利的話,運行的結果以下:
程序已經成功地進入 kernel 而且運行到了 0xC0800003
處,就是那個 while 循環的位置,這將是 kernel 征途的真正開篇:)