從零開始寫 OS 內核 - 加載並進入 kernel

系列目錄

kernel 磁盤鏡像

接上一篇 虛擬內存初探,本篇將正式加載並啓動 kernel,也就是圖中綠色的部分:git

固然 kernel 鏡像要從磁盤上讀取加載,因此這裏回顧一張老圖,是 diskmemory(物理內存)的數據對應關係:shell

順便提一下,上圖中斜線陰影打問號的部分,就是上一章講的 kernel page tables,即第一張圖的橙色部分,共 256 張佔地 1MB。segmentfault

編寫 kernel

回到 kernel ,即圖中綠色部分,它如今實際上還不存在,因此首先咱們須要實現、編譯一個簡單的 demo 性質的 kernel。若是對 kernel 是什麼尚未概念的同窗,可能會問:到底 kernel 長什麼樣?bash

答案很是簡單:kernel 和你平時用 C 語言寫的可執行程序幾乎沒有任何區別,也是從一個 main 函數開始。多線程

下面咱們就實現咱們的第一個 kernel:函數

void main() {
  while (1) {}
}

就是這樣簡單,除了一個 while 循環,沒有任何其它東西,但它足以用做咱們這裏的 demo。gitlab

編譯 kernel

這裏有不少編譯參數,例如以 32 位編碼,禁用 C 標準庫等(這是咱們本身定製的 OS,和 C 標準庫不可能兼容)。ui

gcc -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector -no-pie -fno-pic -c main.c -o main.o

連接 kernel:

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 段會被加載到此處,日後依次是 databss 等段。loader 結束後將會跳轉到該地址。

另外上面還定義了整個可執行文件的入口函數爲 main

編譯連接後的 kernel 是一個 ELF 格式的二進制,咱們不妨將它反彙編 dump 看一下:

objdump -dsx kernel

能夠看到 main 函數的地址爲 0xC080000,這是進入 kernel 後的第一條指令。

製做 kernel 鏡像

dd if=kernel of=scroll.img bs=512 count=2048 seek=9 conv=notrunc

seek=9 是由於前面 mbrloader 已經在磁盤上佔據了前 9 個 sectors。這裏 kernel 大小爲 2048 個 sectors 共 1MB,對於咱們這個項目而言已經足夠大了,徹底夠用。

如今磁盤鏡像終於變成了這樣:

讀取並加載 kernel

鏡像準備完畢,接下來就能夠將 kernel 讀取而且加載了。首先仍是給出代碼連接 init_kernel,供你參考。

和以前 mbrloader加載不一樣,這裏將讀取加載兩個詞分開,是由於它們是兩個步驟:

  • 讀取:是將 kernel 磁盤鏡像的 原始二進制 複製到內存中某空閒處,這裏的二進制是 ELF 格式的;
  • 加載:是將前一步獲得的 ELF 可執行二進制進行解析,將每個 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

一切準備就緒,接下來就能夠真正進入 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 Headere_entry 字段給出的。ELF 可執行二進制的入口地址是在連接階段肯定的,它其實是由以前的 link.ld 裏的 ENTRY(main) 指定的。

順利的話,運行的結果以下:

程序已經成功地進入 kernel 而且運行到了 0xC0800003 處,就是那個 while 循環的位置,這將是 kernel 征途的真正開篇:)

相關文章
相關標籤/搜索