Operating Systems : simple Operating System from scratch (4) — boot from uefi, part 3
Hello!
在上一篇我們寫了一個簡單的 UEFI Boot&Loader , 這時候就要開始製作出那個被載入的 "kernel.bin" 了!之後就是跟系統核心相關,越來越有趣的部份了!
編譯與執行一個簡易的系統核心
這邊分為幾個部份介紹:
- head.S
在 head.S 裡,我們需要為作業系統做好段 ( section ) 與頁表 ( paging ) ,設置中斷的默認處理函式...等等,作業系統的前置作業。 - main.c
main.c 就是作業系統的主程式了!可以開始構築"字串,螢幕顯示"、 “例外處理” 、"記憶體管理"、"中斷"、"行程 ( process ) 管理" 、"user space & kernel space"、"系統呼叫"、 "檔案系統" 等功能。 - Makefile
讓我們可以很方便的對 source code 進行編譯,而且有編譯過但沒改動的檔案,會幫我們自動略過,以加速編譯速度。 - Link Script
通常我們編譯一般的程式的時候,都不會自己寫 link script , 但是作業系統核心是比較特殊的程式,所以也需要自己設計下 link script 了。 - 掛載 USB,並將成品 ( kernel.bin ) 小心翼翼地放進去
經過編譯 ( compile ) 與連結 ( link ) 後,終於產出了 kernel.bin
head.S
完整的程式碼可以看下這裡。
初始化區段暫存器 ( segment register ) 與 cr3 ( PDBR, page directory base register ):
.section .textENTRY(_start)
mov %ss, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %ss
mov $0x7E00, %esp
movq $0x101000, %rax
movq %rax, %cr3/*
初始化 section register。 因為對 uefi 的研究沒有那麼透徹,但大致上可以確定 uefi 所使用的 ss ( stack segment ) 是可寫的位址,所以在這裡把大部分的段暫存器 ( segment register ) 都初始化成 uefi 所使用的 ss 。
esp 則是給個大家"大概"不太會用到的位址,其實也就是原版的 Boot&Loader 的起始位址。 在這裡就率先設定 cr3 大致上是因為我們不理解 uefi 的 paging 是怎麼映射記憶體的,所以先在這裡使用我們自己的 paging。*/
載入 GDT ( Global Descriptor Table ):
/======= load GDTRlgdt GDT_POINTER(%rip)/*
從這裡開始,我們就可以使用我們自己所設定的區段暫存器 ( segment register ) 了 !
*/
重新初始化區段暫存器 ( segment regsiter ) 並載入 IDT ( Interrupt Descriptor Table ):
//======= load IDTRlidt IDT_POINTER(%rip)mov $0x10, %ax
mov %ax, %ds
mov %ax, %es
mov %ax, %fs
mov %ax, %gs
mov %ax, %ss
/*
載入 IDT。
且因為我們已經載入好 GDT 了,所以可以用我們自己選定的區段 ( segment ) --> 0x10。
*/
正式啟用我們自己的區段暫存器:
/======= switch_segmovq switch_seg(%rip), %rax
pushq $0x08
pushq %rax
lretqswitch_seg:
.quad entry64
/*
因為這裡暫時不支援 jmp 以及 call , 所以我們製造一個假的 call stack,並用 lretq 。
Q1: 書上是這樣寫的,真的是這樣嗎?
Q2: 為什麼修改 segment register 後,會需要 long jmp 到另外一個地方,在這裡進行 long jump 真的是必要的嗎?
*/
在這裡,我們的 code segment register 初始化成 0x8,從下圖可以看到,前 3 bit 有其他用途,所以 0x8 >> 3 後可得 1 ,我們是使用 GDT 的第 1 個 entry。
準備跳進 main.c :
entry64:
movq $0x10, %rax
movq %rax, %ds
movq %rax, %es
movq %rax, %gs
movq %rax, %ss
movq $0xffff800000007e00, %rsp
/*
這裡再次初始化區段暫存器。
Q3: 為什麼這邊需要再初始化一次區段暫存器?
並且也順便初始化 rsp。因為我們暫時性的 GDT 以及 cr3 都已經載入了,所以這邊填寫的是 virtual memory。
*/movq go_to_kernel(%rip), %rax /* movq address */
pushq $0x08
pushq %rax
lretqgo_to_kernel:
.quad Start_Kernel
/*
使用 long jump 跳到 kernel 內執行。
*/
在這裡,我們的 data segment register 初始化成 0x10,從下圖可以看到,前 3 bit 有其他用途,所以 0x10 >> 3 後可得 2 ,我們是使用 GDT 的第 2 個 entry。
一些設定 ( GDT, IDT, TSS ) :
align 8.org 0x1000__PML4E: .quad 0x102007
.fill 255,8,0
.quad 0x102007
.fill 255,8,0.org 0x2000__PDPTE: .quad 0x103007 /* 0x103003 */
.fill 511,8,0.org 0x3000__PDE:
.quad 0x000087
.quad 0x200087
.quad 0x400087
.quad 0x600087
.quad 0x800087
.quad 0xa00087 /* 0xa00000 */
.quad 0xc00087
.quad 0xe00087
.quad 0x1000087 /* 0x1000000 */
.quad 0x1200087
.quad 0x1400087
.quad 0x1600087
.quad 0x1800087
.fill 499,8,0//======= GDT_Table.section .data.globl GDT_TableGDT_Table:
.quad 0x0000000000000000 /*0 NULL descriptor 00*/
.quad 0x0020980000000000 /*1 KERNEL Code 64-bit Segment 08*/
.quad 0x0000920000000000 /*2 KERNEL Data 64-bit Segment 10*/
.quad 0x0000000000000000 /*3 USER Code 32-bit Segment 18*/
.quad 0x0000000000000000 /*4 USER Data 32-bit Segment 20*/
.quad 0x0020f80000000000 /*5 USER Code 64-bit Segment 28*/
.quad 0x0000f20000000000 /*6 USER Data 64-bit Segment 30*/
.quad 0x00cf9a000000ffff /*7 KERNEL Code 32-bit Segment 38*/
.quad 0x00cf92000000ffff /*8 KERNEL Data 32-bit Segment 40*/
.fill 10,8,0 /*10 ~ 11 TSS (jmp one segment <9>) in long-mode 128-bit 50*/
GDT_END:
GDT_POINTER:
GDT_LIMIT: .word GDT_END - GDT_Table - 1
GDT_BASE: .quad GDT_Table//======= IDT_Table.globl IDT_TableIDT_Table:
.fill 512,8,0
IDT_END:IDT_POINTER:
IDT_LIMIT: .word IDT_END - IDT_Table - 1
IDT_BASE: .quad IDT_Table//======= TSS64_Table.globl TSS64_TableTSS64_Table:
.fill 13,8,0
TSS64_END:TSS64_POINTER:
TSS64_LIMIT: .word TSS64_END - TSS64_Table - 1
TSS64_BASE: .quad TSS64_TableTSS64_POINTER:
TSS64_LIMIT: .word TSS64_END - TSS64_Table - 1
TSS64_BASE: .quad TSS64_Table
到目前為止,我們還不會用到 IDT, TSS ,所以沒有特別填什麼值。
而 GDT ( 用於 segmentation ) 以及 __PML4E, __PDPTE, __PDE ( 用於 paging ) ,由於這邊使用 2MB 的 page , 所以就沒有 PTE 了。
main.c
void Start_Kernel(void)
{ int *addr = (int *)0xffff800003000000;
/* 可以看前幾篇的介紹,在 uefi Boot&Loader 就將 framebuffer 映射到了這個 virtual address */ int i; for(i = 0 ;i<1024*20;i++)
{
*((char *)addr+0)=(char)0x00;
*((char *)addr+1)=(char)0x00;
*((char *)addr+2)=(char)0xff;
*((char *)addr+3)=(char)0x00;
addr +=1;
}
for(i = 0 ;i<1024*20;i++)
{
*((char *)addr+0)=(char)0x00;
*((char *)addr+1)=(char)0xff;
*((char *)addr+2)=(char)0x00;
*((char *)addr+3)=(char)0x00;
addr +=1;
}
for(i = 0 ;i<1024*20;i++)
{
*((char *)addr+0)=(char)0xff;
*((char *)addr+1)=(char)0x00;
*((char *)addr+2)=(char)0x00;
*((char *)addr+3)=(char)0x00;
addr +=1;
}
for(i = 0 ;i<1024*20;i++)
{
*((char *)addr+0)=(char)0xff;
*((char *)addr+1)=(char)0xff;
*((char *)addr+2)=(char)0xff;
*((char *)addr+3)=(char)0x00;
addr +=1;
} while(1)
;
}
從上一篇的介紹可以知道,我們選擇的畫質為 1024 * 768。順待一提,每個 pixel 佔了 4 bytes ( 31 bits ) 。
0~7 bit 代表藍色
8~15 bit 代表綠色
16 ~ 23 bit 代表紅色
24 ~ 31 bit 是保留位
--> 0x000000ff : 藍色
--> 0x0000ff00 : 綠色
--> 0x00ff0000 : 紅色
--> 0x00ffffffff : 白色
所以上面的程式會顯示出四調色帶
Makefile
all: system
objcopy -I elf64-x86-64 -S -R ".eh_frame" -R ".comment" -O binary system kernel.bin
# -I elf64-x86-65 : 輸入的檔案的格式
# -S : 移除所有的 symbol 以及 relocation definition
# -R "xxx" : 移除某一個 elf 的 section
# -O : binary : 輸出的格式為純 binary 檔案
system: main.o head.o
ld -b elf64-x86-64 -z muldefs -o system head.o main.o -T Kernel.lds
# -b elf64-x86-64 : 想要 link 成哪一種 format 的 binary
# -z muldefs : 允許重複的 define。當遇到重複的 define 的時候,只使用其中一個。
# -T Kernel.lds : 使用我們自己的 link script ,而不使用預設的 link scripthead.o: head.S
gcc -E head.S > head.s
as --64 -o head.o head.s
# gcc -E : 把 macro 展開後的樣子存到 head.s
# as : 組譯器main.o: main.c
gcc -mcmodel=large -fno-builtin -m64 -c main.c -fno-stack-protector
# -mcmodel=large : mcmodel 用來限制程式可訪問的地址空間,選擇 large 表明程式可以訪問任何的 virtual address。這使我們的 code 即便超出 4 GB 的範圍也能編譯
# -fno-builtin : 除非使用 __builtin_ 來明確地引用,否則不使用 builtin function ( 系統內建函式 )
# -fno-stack-protector : 關掉 stack guardclean:
rm -rf *.o *.s~ *.s *.S~ *.c~ *.h~ system Makefile~ Kernel.lds~ kernel.bin
Link Script
OUTPUT_FORMAT("elf64-x86-64","elf64-x86-64","elf64-x86-64")
//當輸出設定成 ( 默認, 大端序, 小端序) 時,會以哪一種 format 輸出
//這裡的意思就是,默認的輸出,大端序的輸出,小端序的輸出,都是 elf64-x86-64 這個 format
// ld 的 -EB option 可以將我們的輸出設定成 "大端序" 來輸出OUTPUT_ARCH(i386:x86-64)
// 輸出的處理器體系結構ENTRY(_start)
// 將 _start 這個符號設定成程式的入口位址,即程式執行的第一條 instruction 的位址,我們的 _start 符號便是在 head.S 裡。SECTIONS
{ . = 0xffff800000000000 + 0x100000;
// 將定位器 '.' 設置在 "線性位址" 0xffff800000100000 .text :
{
_text = .;
// 標識符,紀錄 .text 段的起始位址 *(.text)
// 將所有輸入檔案 ( 在我們的例子裡,就是 "head.o" 以及 main.o ) 的 .text 段都放在這個地方。 _etext = .;
// 標識符,紀錄 .text 段的結束位址 } . = ALIGN(8); .data :
{
_data = .;
*(.data)
_edata = .;
} .rodata :
{
_rodata = .;
*(.rodata)
_erodata = .;
} . = ALIGN(32768);
.data.init_task :
{
*(.data.init_task)
} .bss :
{
_bss = .;
*(.bss)
_ebss = .;
} _end = .;
}
顯示結果
接下來就可以簡單的操作一些指令,將我們的 kernel.bin 放到 USB 裡。放在跟 uefi Boot&Loader 一樣的位置。
$ make
gcc -mcmodel=large -fno-builtin -m64 -c main.c -fno-stack-protector
gcc -E head.S > head.s
as --64 -o head.o head.s
ld -b elf64-x86-64 -z muldefs -o system head.o main.o -T Kernel.lds
objcopy -I elf64-x86-64 -S -R ".eh_frame" -R ".comment" -O binary system kernel.bin
$ cp ./kernel.bin /xxxxx( USB 掛載的資料夾 )
之後可以使用 USB 開機,並進入 uefi shell
$ fs0:
$ BootLoader.efi
太好了,老舊的筆電有正常顯示紅,綠,藍,白的色條!
結語
接下來,希望能寫一個筆記來紀錄在 long mode 下,怎麼從 virtual address 轉換成 linear address 。每看完一次資料,就忘記一次,已經來來回回複習好幾次了,希望寫成筆記可以加速我金魚腦的複習速度 QQ。
Reference
- 一個64位操作系統的設計與實現
- FS and GS registers in 64 bit mode from the intel architecture manual
- segmentation 和保護模式
- x86 記憶體區段
- CPU Registers x86-64
- x86 memory segmentation
- 分頁架構
TODO
回答 Q1 :
回答 Q2 :
回答 Q3 :