hi, 大家好。
在這篇文章中,我們将從零開始,動手編寫一個可以用grub來引導的簡單x86核心,該核心會在螢幕上列印一條資訊,然後——挂起!

one-does-not-kernel
一個人寫一個核心是一件簡單的事情
<a target="_blank"></a>
在我們思考怎樣寫一個核心之前,讓我們先看一下x86機器從啟動到把控制權交給核心的過程是怎樣的:
x86 cpu在機器啟動之後就會從位址 [0xfffffff0]處開始執行,這個位址就是在32位尋址空間中的最後16個位元組處,這裡存放了一條跳轉指令,會跳轉到記憶體中bios代碼起始處。
接着,cpu就開始開始執行bios代碼塊了,bios首先會在我們配置好的啟動裝置序列中,通過檢查一個特定的魔數,找到第一個可以引導的裝置。
一旦bios找到一個可以引導的裝置後,它就會把該裝置第一個扇區的代碼複制到實體記憶體的[0x7c00]的位置,然後跳轉到這個位址開始執行這一段代碼,我們習慣把這一段代碼叫作bootloader。
bootloader會将核心代碼加載到實體記憶體[0x100000]的位置,[0x100000]這個位址是所有x86機器宏核心代碼的起始位址。
* 一個x86構架的計算機
* linux
我們喜歡用c來做所有的事情,但是我們無可避免地需要用到一點兒彙編,我們将會寫一小段x86的彙編代碼來作為核心入口,這一段彙編代碼會在調用我們的c代碼後停止整個程式流程。
我們怎樣确認彙編代碼會作為核心的起始點呢?
我們将用一個連接配接器腳本将這些目标檔案連結成我們最終的核心程式(稍後解釋更多),在連接配接器腳本裡,我們指定了這段二進制代碼會被加載到記憶體 [0x100000]處。這個位址就是我之前說過的,核心所希望的起始位址。
彙編代碼如下:
;;kernel.asm
bits 32 ;nasm directive - 32 bit
section .text
global start
extern kmain ;kmain is defined in the c file
start:
cli ;block interrupts
call kmain
hlt ;halt the cpu
第一行指令 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()中開始運作:
/*
* kernel.c
*/
void kmain(void)
{
char *str = "my first kernel";
char *vidptr = (char*)0xb8000; //video mem begins here.
unsigned int i = 0;
unsigned int j = 0;
//clear all
while(j < 80 * 25 * 2) {
//blank character
vidptr[j] = ' ';
//attribute-byte: light grey on black screen
vidptr[j+1] = 0x07;
j = j + 2;
}
j = 0;
while(str[j] != '\0') {
vidptr[i] = str[j];
vidptr[i+1] = 0x07;
++j;
i = i + 2;
return;
我們的核心首先會清空整個螢幕,然後列印出字元串。
首先,我們用一個vidptr指針,指向位址[0xb8000] , 這個位址是保護模式下顯存的起始位址。螢幕的文本内容對應着的記憶體空間中一個記憶體段,即螢幕的輸出輸出映射到了記憶體中位址[0xb8000]的地方,整個螢幕共支援25行,每行80個ascii字元。
在文本記憶體中每一個字元由16bits(2個位元組)表示,這不像我們以前使用8bits來定義。其中第一個位元組是該字元的ascii碼,第二個位元組是屬性位元組, 它描述了字元的表現形式,包括了字元顔色等屬性。
為了在黑色的背景下列印綠色字元’s‘,我們将字元’s‘放在顯存中的第一個位元組,接着将[0x02]放在第二個位元組中, 其中 0表示黑色背景,2表示綠色前景。
下面是不同顔色的定義:
0 - black, 1 - blue, 2 - green, 3 - cyan, 4 - red, 5 - magenta, 6 - brown, 7 - light grey,
8 - dark grey, 9 - light blue, 10/a - light green, 11/b - light cyan, 12/c - light red,
13/d - light magenta, 14/e - light brown, 15/f – white.
在我們的核心中,我們将字元顔色設定為灰色,将背景顔色設定為黑色,是以我們的屬性位元組的值是[0x07].
在第一個while循環中,程式将屬性值為[0x07]的空格字元(‘ ’)寫到整個螢幕中(共25行,每行80個字元),這樣就會将整個螢幕清空了。
在第二個while循環中,我們将null結尾的字元串 “my first kernel” ,從顯存的起始處開始寫入。
這樣字元串就列印在螢幕上了
我們用nasm,gcc分别将kernale.asm,kernel.c編譯成目标檔案,接着将這些目标檔案連結成一個可引導的核心程式。
我們指定ld連接配接器按照我們腳本規定來進行連結。
* link.ld
output_format(elf32-i386)
entry(start)
sections
. = 0x100000;
.text : { *(.text) }
.data : { *(.data) }
.bss : { *(.bss) }
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。
是以,我們的核心代碼如下:
;nasm directive - 32 bit
bits 32
;multiboot spec
align 4
dd 0x1badb002 ;magic
dd 0x00 ;flags
dd - (0x1badb002 + 0x00) ;checksum. m+f+c should be zero
dd 指令定義了個4位元組的雙字。
我們現在開始将kernel.asm和kernel.c編譯成目标檔案,接着将它們根據我們的連接配接器腳本的設定連結到一起:
nasm -f elf32 kernel.asm -o kasm.o
啟動nasm彙編器将kernel.asm編譯成elf-32位格式的目标檔案。
gcc -m32 -c kernel.c -o kc.o
-c選項告知gcc編譯器在将源檔案編譯成目标檔案後,不要對它們進行連結。
ld -m elf_i386 -t link.ld -o kernel kasm.o kc.o
啟動連結器,根據我們的連結腳本生成一個名為kernel的可執行的檔案。
grub 需要以kernel-的形式來命名核心程式,是以,我将它重名為kernel-701.
接着将它放在/boot目錄下,這一步需要你需要擁有超級使用者權限才能夠進行操作。
在你的grub配置檔案grub.cfg中加上一個引導入口,如下:
title mykernel
root (hd0,0)
kernel /boot/kernel-701 ro
如果存在一個“hiddenmenu”的指令,記得要把它移除掉。
重新開機電腦,你就能夠看到你的核心也在啟動選擇項清單中了。
選擇啟動它之後,結果如下:
mkernel
成功顯示出來了。
”這是你的核心“
”不,是你的核心“。
ps:
* 建議你在虛拟機中進行你所有核心hacking。
* 在一些新的發行版中,使用了grub2作為預設的bootloader,你需要向下面這樣來配置你的配置檔案。
(感謝 rubén laguna提供了grub2的配置)
menuentry 'kernel 7001' {
set root='hd0,msdos1'
multiboot /boot/kernel-7001 ro
* 如果你想用qemu模拟器代替grub來啟動你的核心程式的話,你可以怎麼做:
qemu-system-i386 -kernel kernel
原文釋出時間為:2014-04-25
本文來自雲栖社群合作夥伴“linux中國”