核心代號101 — 動手寫自己的核心
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]處。這個地址就是我之前說過的,核心所希望的起始地址。
彙編程式碼如下:
;;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)的縮寫。
用C實現的核心
在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) } }
指令碼指定了輸出格式為 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。
因此,我們的核心程式碼如下:
;;kernel.asm ;nasm directive - 32 bit bits 32 section .text ;multiboot spec align 4 dd 0x1BADB002 ;magic dd 0x00 ;flags dd - (0x1BADB002 + 0x00) ;checksum. m+f+c should be zero global start extern kmain ;kmain is defined in the c file start: cli ;block interrupts call kmain hlt ;halt the CPU
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,啟動核心
GRUB 需要以kernel-的形式來命名核心程式,所以,我將它重名為kernel-701.
接著將它放在/boot目錄下,這一步需要你需要擁有超級使用者許可權才能夠進行操作。
在你的GRUB配置檔案grub.cfg中加上一個引匯入口,如下:
title myKernel root (hd0,0) kernel /boot/kernel-701 ro
如果存在一個“hiddenmenu”的指令,記得要把它移除掉。
重啟電腦,你就能夠看到你的核心也在啟動選擇項列表中了。
選擇啟動它之後,結果如下:
成功顯示出來了。
”這是你的核心“
”不,是你的核心“。
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
原文連結: Arjun Sreedharan 翻譯: 極客範 - 何偉寰
相關文章
- 核心技術靠化緣是要不來的——自己動手寫ORM框架ORM框架
- 手寫ArrayList核心原始碼原始碼
- 手寫 ArrayList 核心原始碼原始碼
- 核心必須懂(四): 撰寫核心驅動
- 手寫 Java HashMap 核心原始碼JavaHashMap原始碼
- 自己動手寫PromisePromise
- Vite本地構建:手寫核心原理Vite
- Linux 核心101:cache原理Linux
- Linux 核心 101:NUMA架構Linux架構
- 自己動手寫一個 SimpleVueVue
- 自己動手寫 PHP 框架(一)PHP框架
- Linux 核心101:cache組織策略Linux
- Linux 核心101:NUMA下的競爭管理Linux
- 自己動手寫SQL執行引擎SQL
- 自己動手寫RecyclerView的下拉重新整理View
- 自己動手寫RecyclerView的上拉載入View
- Linux 核心101:程式資料結構Linux資料結構
- Linux 核心驅動中對檔案的讀寫Linux
- 自己動手寫一個簡單的MVC框架MVC框架
- 寫作的核心與價值
- 自己動手寫basic直譯器 一
- 自己動手寫事件匯流排(EventBus)事件
- 自己動手寫一個持久層框架框架
- 自己動手寫Vector【Cherno C++教程】C++
- 自己動手寫Web自動化測試框架Web框架
- 手機寫作業系統之 使用C語言編寫核心作業系統C語言
- Linux 核心101:虛擬檔案系統的使命Linux
- 驅動篇——核心空間與核心模組
- Linux系統核心模組和驅動的編寫(轉)Linux
- 30個類手寫Spring核心原理之動態資料來源切換Spring
- 自己動手寫Android資料庫框架Android資料庫框架
- WPF啟動流程-自己手寫Main函式AI函式
- 自己動手寫個 Android客戶端Android客戶端
- 自己動手寫DB資料庫框架(增)資料庫框架
- 【Linux核心版本號命名的規則 】Linux
- rhel 4 update4 的核心版本號
- 編寫最簡單的核心:HelloWorld
- RAC核心元素與訊號流