内核代号101 — 动手写自己的内核
本文由極客范-何偉寰翻譯自?Arjun Sreedharan?。歡迎加入?極客翻譯小組?,同我們一道翻譯與分享。轉載請參見文章末尾處的要求。?
Hi, 大家好。
在這篇文章中,我們將從零開始,動手編寫一個可以用GRUB來引導的簡單x86內核,該內核會在屏幕上打印一條信息后掛起。
?一個人寫一個內核是一件簡單的事情
X86機器是怎樣啟動的?
在我們思考怎樣寫一個內核之前,讓我們先看一下x86機器從啟動到把控制權交給內核的過程是怎樣的:
x86?CPU在機器啟動之后就會從地址??[0xFFFFFFF0]?處開始執行,這個地址就是在32位尋址空間中的最后16個字節處,這里存放了一條跳轉指令,會跳轉到內存中BIOS代碼起始處。
接著,cpu就開始開始執行BIOS代碼塊了,BIOS首先會在我們配置好的啟動設備序列中,通過檢查一個特定的魔數,找到第一個可以引導的設備。
一旦BIOS找到一個可以引導的設備后,它就會把該設備第一個扇區的代碼復制到物理內存的[?0x7c00?]的位置,然后跳轉到這個地址開始執行這一段代碼,我們習慣把這一段代碼叫作bootloader。
Bootloader會將內核代碼加載到物理內存[?0x100000?]的位置,[?0x100000?]這個地址是所有x86機器宏內核代碼的起始地址。
我們需要哪一些工具?
* 一個x86構架的計算機
*?Linux
*?NASM??匯編器
*??GCC
*?LD?(GNU?連接器)
*??GRUB
源碼
源代碼可以在我的Github系在??Github?repository?-?mkernel
用匯編代碼來編寫內核入口
我們喜歡用c來做所有的事情,但是我們無可避免地需要用到一點兒匯編,我們將會寫一小段x86的匯編代碼來作為內核入口,這一段匯編代碼會在調用我們的c代碼后停止整個程序流程。
我們怎樣確認匯編代碼會作為內核的起始點呢?
?我們將用一個連接器腳本將這些目標文件鏈接成我們最終的內核程序(稍后解釋更多),在連接器腳本里,我們指定了這段二進制代碼會被加載到內存??[0x100000?]處。這個地址就是我之前說過的,內核所希望的起始地址。
匯編代碼如下:
第一條指令??bit32??不是x86匯編指令,它是一條NASM?指令,指定nasm匯編器產生32位的程序,這條語句并不是必不可少的,但加上它是一個好的編程習慣。
第二行是text段(代碼段)的開始,在這里存放著我們的代碼塊。
global?是另外一個NASM指令,用將一個符號設置為全局符號。這樣做連接器才會知道符號?start?在哪兒開始,?start?是我們程序的入口地址。
kmain?是我們定義在?kernel.c?文件中的函數,?extern?關鍵字聲明了該函數定義在別的文件中。
到這里,我們的函數?start?調用?kmian?函數之后就會使用?hlt?指令將CPU掛起,中斷會cpu從?hlt??指令中喚醒,我們要在掛起之前用?cli?指令來關閉系統的中斷響應,?cli?指令是清除中斷(clear-interrupts)的縮寫。
用C實現的內核
在?kernle.asm?中,我們調用了?kmain()?函數,所以我們的c代碼將會在?kmain()?中開始運行:
我們的內核首先會清空整個屏幕,然后打印出字符串。
首先,我們用一個?vidptr?指針,指向地址?[0xb8000] ,??這個地址是保護模式下顯存的起始地址。屏幕的文本內容對應著的內存空間中一個內存段,即屏幕的輸出輸出映射到了內存中地址?[0xb8000]?的地方,整個屏幕共支持25行,每行80個ASCII字符。
在文本內存中每一個字符由16bits(2個字節)表示,這不像我們以前使用8bits來定義。其中第一個字節是該字符的ASCII碼,第二個字節是?屬性字節?,?它描述了字符的表現形式,包括了字符顏色等屬性。
為了在黑色的背景下打印綠色字符’?s?‘,我們將字符’?s?‘放在顯存中的第一個字節,接著將[?0x02?]放在第二個字節中, 其中 0表示黑色背景,2表示綠色前景。
下面是不同顏色的定義:
在我們的內核中,我們將字符顏色設置為灰色,將背景顏色設定為黑色,因此我們的屬性字節的值是[?0x07?].
在第一個while循環中,程序將屬性值為[?0x07?]的空格字符(‘ ?’)寫到整個屏幕中(共25行,每行80個字符),這樣就會將整個屏幕清空了。
在第二個while循環中,我們將null結尾的字符串?“my first kernel”?,從顯存的起始處開始寫入。
這樣字符串就打印在屏幕上了
鏈接部分
我們用NASM,GCC分別將?kernale.asm,?kernel.c?編譯成目標文件,接著將這些目標文件鏈接成一個可引導的內核程序。
我們指定?ld?連接器按照我們腳本規定來進行鏈接。
腳本指定了輸出格式為 32位的ELF文件格式.?ELF(?Executable and Linkable Format?)是x86構架的類Unix系統標準的二進制格式。
ENTRY?接收一個參數。它指定了可執行文件的入口符號。
SECTIONS ??對我們來講是最重要的。在這里,我們定義即將生成的可執行文件的布局。我們可以定義各個段鏈接融合的方式以及放置的位置。
在?SECTIONS?后的花括號中,符號?(.)?表示的是一個位置計數器。它通常會被初始化為[?0x0],?作為SECTIONS?塊的起始地址?,它的值是可以被修改的。?之前我說過,內核代碼需要在地址[?0x100000?]處,所以我們將它修改為[?0x100000?]。
接著看下一行的??.text?:?{?*(.text)?}
星號( * )是一個通配符,表示所有的文件名。*(.text)表示將所有輸入文件的??.text??段
因此,按照這個設定,連接器將所有目標文件的text段融合到最終可執行文件的text?段中,即在位置計數器所標識的地址處?([?0x100000?])。
在連接器將處理好輸出的text段后,地址計數器的值會變為[?0x100000?]+text段的長度。
類似的,data段和bss段也會相應得融合后放置到地址計數器所標識的位置。
Grub和多重引導
現在我們已經準備好所有制作內核所需的文件了,但我們還有一步工作,我們還需要用grub?Bootloader來啟動我們的內核。
在按照Mutileboot?規范來編譯我們的內核后,它就可以被GRUB引導了。
按照Mutileboot?的規范說明,內核必須在起始的8KB中包含這一個多引導項頭(Multiboot?header)。
而且,這個多引導項頭里面必須有3個4字節對齊的塊。
一個魔術塊:包含了魔數[0x1BADB002],是多引導項頭結構的定義值。
一個標志塊:我們不關心這個塊的內容,我們簡單設定為0。
一個校檢塊:校檢塊,魔術塊和標志塊的數值的總和必須是0。
因此,我們的內核代碼如下:
dd 指令定義了個4字節的雙字。
生成內核
我們現在開始將kernel.asm和kernel.c編譯成目標文件,接著將它們根據我們的連接器腳本的設定鏈接到一起:
①?
啟動NASM匯編器將kernel.asm編譯成ELF-32位格式的目標文件。
②?
-c選項告知GCC編譯器在將源文件編譯成目標文件后,不要對它們進行鏈接。
③?
啟動鏈接器,根據我們的鏈接腳本生成一個名為kernel的可執行的文件。
配置grub, 啟動內核
GRUB?需要以?kernel-<version>?的形式來命名內核程序,所以,我將它重名為kernel-701.
接著將它放在/boot目錄下,這一步需要你需要擁有超級用戶權限才能夠進行操作。
在你的GRUB配置文件?grub.cfg?中加上一個引導入口,如下:
如果存在一個隱藏菜單的指令,記得要把它移除掉。
重啟電腦,你就能夠看到你的內核也在啟動選擇項列表中了。
選擇啟動它之后,結果如下:
成功顯示出來了。
”這是你的內核“
”不,是你的內核“。
PS:
* ?建議你在虛擬機中進行你所有內核hacking。
* ?在一些新的發行版中,使用了grub2作為默認的bootloader,你需要向下面這樣來配置你的配置文件。
?(感謝?Rubén?Laguna提供了grub2的配置)
* ?如果你想用qemu模擬器代替GRUB來啟動你的內核程序的話,你可以怎么做:
總結
以上是生活随笔為你收集整理的内核代号101 — 动手写自己的内核的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 代码分析:NASM源码阅读笔记
- 下一篇: 实现一个汇编器