Hi, 大家好。
在這篇文章中,我們將從零開始,動手編寫一個可以用GRUB來引導的簡單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)的縮寫。
在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 Bootloader來啟動我們的內核。
在按照Mutileboot 規范來編譯我們的內核後,它就可以被GRUB引導了。
按照Mutileboot 的規范說明,內核必須在起始的8KB中包含這一個多引導項頭(Multiboot header)。
而且,這個多引導項頭裡面必須有3個4字節對齊的塊。
一個魔術塊:包含了魔數[0x1BADB002],是多引導項頭結構的定義值。
一個標志塊:我們不關心這個塊的內容,我們簡單設定為0。
一個校檢塊:校檢塊,魔術塊和標志塊的數值的總和必須是0。
因此,我們的內核代碼如下:
dd 指令定義了個4字節的雙字。