核心是作業系統最核心的內容,主要提供硬體抽象層、磁碟及檔案系統控制、多工等功能,由於其涉及非常廣泛的計算機知識,很少被人們所熟悉,因而披上了一層神祕的面紗。
本文將從零開始實現一個最簡單的核心,其可以通過x86系統的GRUB引導啟動,並向螢幕輸出“Hello World!“字串。該核心程式碼非常簡短,並且在本人的Debian 7系統中可以正常執行。
x86機器啟動過程
在具體實現這個核心之前,我們先看看機器具體是怎麼啟動並且把控制權交給核心的。
x86的CPU固定地在實體地址為[0xFFFFFFF0]的地方開始執行,這是32位地址空間的最後16個位元組。這裡只包含了一個跳轉指令,跳轉到BIOS把它自己拷貝到的記憶體區域的地址。
然後,BIOS開始執行。它首先根據配置的裝置啟動順序依次尋找可啟動的裝置(根據一個特定的魔數可以決定一個裝置是否啟動)。一旦找到一個可啟動的裝置,它就把該裝置第一個扇區的內容複製到RAM中實體地址從[0x7C00]開始的地方,然後跳轉到該地址並且開始執行那裡載入的程式碼。這段程式碼稱為啟動引導裝載程式(bootloader)。Bootloader然後在實體地址為[0x100000]的地方載入核心,地址[0x100000]就是x86機器上核心的起始地址。
需要的工具
- 一臺x86電腦
- Linux
- NASM彙編器
- gcc
- ld(GNU連結器)
- grub
彙編入口點
我們希望用C來寫所有的程式碼,但免不了要寫一點彙編程式碼。我們會寫一個x86組合語言的小檔案來作為核心的起始點,這段彙編所做的事情就是呼叫一個我們用C寫的外部函式,然後停止程式執行。
怎麼確定這段彙編程式碼會作為核心的起始點呢?
我們會使用一個連結指令碼來連結所有的目標檔案來產生一個最終的核心可執行映像。在這個連結指令碼中,我們會顯式指明二進位制檔案要載入在地址為[0x100000]的地方,這就是核心所在的地方。於是,bootloader會負責觸發這個核心的入口點。
以下是彙編程式碼:
1 2 3 4 5 6 7 8 9 10 11 |
;;kernel.asm,核心彙編程式碼 bits 32 ;nasm偽指令 section .text ;程式碼段 global start ;全域性變數 extern kmain ;kmain定義在C檔案中 start: cli ;禁止中斷 call kmain ;呼叫kmain函式 hlt ;終止CPU執行 |
第一條指令中的bit 32不是x86彙編指令,而是NASM彙編器的偽指令,表明將會產生一段執行在32位處理器上程式碼。這句程式碼不是必須的,但顯示加上會是一個好的程式設計實踐。
第二行開始就是程式碼段,即放置程式碼的地方。
global也是NASM的偽指令,表示把原始碼中的一個符號設定成全域性符號。於是連結器知道start符號在哪裡,其實這就是我們的入口點。
kmain是將會在kernel.c中實現的一個函式,extern表明這個函式會在其他地方定義。
於是,我們有了start函式,它會呼叫kmain函式,然後通過hlt指令停止CPU。由於中斷會從hlt指令中喚醒CPU,所以我們事先使用cli(意為clear interrupts)指令禁止中斷。
C語言核心
我們在kernel.asm中呼叫kmain()函式,所以C程式碼會從kmain()開始執行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
//kernel.c檔案 void kmain(void) { char *str = "Hello World!"; char *vidptr = (char*)0xb8000; //視訊記憶體開始地址 unsigned int i = 0; unsigned int j = 0; //清空螢幕,共25行,每行80個字元,每個字元2位元組 while(j < 80 * 25 * 2) { //空白字元 vidptr[j] = ' '; //屬性位元組:黑色背景,灰色前景 vidptr[j+1] = 0x07; j = j + 2; } j = 0; while(str[j] != '') { vidptr[i] = str[j]; vidptr[i+1] = 0x07; ++j; i = i + 2; } return; } |
這裡核心所做的事情就是:清空螢幕,列印字串“Hello World!”。
首先是指標vidptr指向地址[0xb8000],這是保護模式下視訊記憶體的開始地址。螢幕的文字記憶體只是地址空間的一連串記憶體區域,它從[0xb8000]開始對映螢幕的輸入輸出,支援25行,每行80個ASCII字元,每個字元用16位(2位元組)表示,而不是我們熟悉的8位(1位元組)。2位元組中第1個位元組是該字元的ASCII表示,第2個位元組是屬性位元組,描述字元的包括顏色在內的屬性。如果要讓背景為黑色而字型為綠色,可以在第1個位元組儲存字元的ASCII值,在第2個位元組儲存值[0x02]:0代表黑色背景,2代表綠色前景。
其它顏色屬性定義如下:
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
Black | Blue | Green | Cyan | Red | Magenta | Brown | Light Grey |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
Dark Grey | Light Blue | Light Green | Light Cyan | Light Red | Light Magenta | Light Brown | White |
我們的核心使用了黑色背景以及灰色字型,所以屬性位元組為[0x07]。
在第一個while迴圈中,程式在所有的25行80列中寫入空字元和[0x07]屬性,從而清空了螢幕。
在第二個while迴圈中,字串”Hello World!“被寫到了視訊記憶體的開始區域,每個字元仍是擁有[0x07]屬性。這就在螢幕上列印了該字串。
連結部分
使用NASM把kernel.asm編譯成目標檔案,再使用GCC把kernel.c編譯成另一個目標檔案,然後需要把這兩個目標檔案連結成一個可以啟動的核心映像。
我們使用連結指令碼來達到這個目的,連結指令碼可以作為引數傳遞進連結器ld中以控制連結的過程。
1 2 3 4 5 6 7 8 9 10 |
//link.ld檔案 OUTPUT_FORMAT(elf32-i386) ENTRY(start) SECTIONS { . = 0x100000; .text : { *(.text) } .data : { *(.data) } .bss : { *(.bss) } } |
OUTPUT_FORMAT設定輸出的可執行檔案為32位的ELF檔案,ELF是x86架構上類Unix系統的標準二進位制檔案格式。
ENTRY接受一個引數,指定其為最終可執行檔案的入口點。
SECTION是這裡最關鍵的部分,它指定不同的段怎麼合併以及放在什麼地方,從而定義最終可執行檔案的佈局。
大括號內就是SECTION的語句,句點(.)為位置計數器,一般被初始化為SECTIONS塊開始的地方[0x0],但可以任意修改。因為核心程式碼需要在地址[0x100000]處開始,所以設定位置計數器為[0x100000]。
第二行中的星號是萬用字元,可以匹配任何檔名,*(.text)即表示匹配所有輸入檔案的程式碼段。於是,連結器合併所有目標檔案的程式碼段到可執行檔案的程式碼段中,具體地址由位置計數器決定,這裡即為[0x100000]。連結器產生程式碼段後,位置計數器會變成:0×100000 + 輸出程式碼段的大小。
同樣地,資料段和bss段會被合併,並放置在位置計數器指定的地方。
Grub和多重引導
現在已經準備好了構建核心的所有檔案了,但要用GRUB進行引導還需要最後一個步驟。
多重引導規範(Multiboot specification)是一個使用bootloader載入不同X86核心的標準,GRUB只會載入滿足這個規範的核心。根據這個規範,核心必須在它的前8KB位元組中包含頭資訊(Multiboot header)。這個頭資訊包含4位元組對齊的3個域,分別為:
- 魔數域:包含魔數[0x1BADB002]。
- 標誌域:這裡不關心這個域,置為0。
- 校驗和域:檢驗和域和前面兩個域相加之後的結果必須為0。
於是kernel.asm應該修改為,程式碼中的dd表示定義一個4位元組的雙字:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
;;kernel.asm,核心彙編程式碼 bits 32 ;nasm偽指令 section .text ;程式碼段 ;多重引導規範 align 4 dd 0x1BADB002 ;魔數 dd 0x00 ;標誌 dd - (0x1BADB002 + 0x00) ;校驗和 global start ;全域性變數 extern kmain ;kmain定義在C檔案中 start: cli ;禁止中斷 call kmain ;呼叫kmain函式 hlt ;終止CPU執行 |
構建核心
現在可以從kernel.asm和kernel.c生成目標檔案,然後使用連線指令碼進行連結。
使用匯編器nasm產生ELF-32格式的目標檔案kasm.o:
1 |
nasm -f elf32 kernel.asm -o kasm.o |
使用編譯器gcc產生目標檔案kc.o,”-c”引數保證只編譯,不進行連結:
1 |
gcc -m32 -c kernel.c -o kc.o |
使用連結器ld根據連結控制指令碼產生可執行映像檔案kernel:
1 |
ld -m elf_i386 -T link.ld -o kernel kasm.o kc.o |
配置GRUB並執行核心
GRUB需要核心以kernel-<version>形式命名,於是把核心kernel重新命名為kernel-701,並利用超級管理員許可權放到/boot目錄下。
對於bootloader為GRUB的發行版,修改配置檔案/boot/grub/grub.cfg,新增以下條目:
1 2 3 |
title myKernel root (hd0,0) kernel /boot/kernel-701 ro |
對於bootloader為GRUB2的發行版,新增的配置應該為:
1 2 3 4 |
menuentry 'kernel 701' { set root='hd0,msdos1' multiboot /boot/kernel-701 ro } |
重啟電腦,選擇GRUB列表中新增加的kernel-701核心選項,這時可以看到螢幕上顯示”Hello World!“。
這就是你的核心!