Operating Systems : simple Operating System from scratch (4) — boot from uefi, part 3

吳建興
15 min readMay 5, 2021

--

Hello!

上一篇我們寫了一個簡單的 UEFI Boot&Loader , 這時候就要開始製作出那個被載入的 "kernel.bin" 了!之後就是跟系統核心相關,越來越有趣的部份了!

編譯與執行一個簡易的系統核心

這邊分為幾個部份介紹:

  1. head.S
    在 head.S 裡,我們需要為作業系統做好段 ( section ) 與頁表 ( paging ) ,設置中斷的默認處理函式...等等,作業系統的前置作業。
  2. main.c
    main.c 就是作業系統的主程式了!可以開始構築"字串,螢幕顯示"、 “例外處理” 、"記憶體管理"、"中斷"、"行程 ( process ) 管理" 、"user space & kernel space"、"系統呼叫"、 "檔案系統" 等功能。
  3. Makefile
    讓我們可以很方便的對 source code 進行編譯,而且有編譯過但沒改動的檔案,會幫我們自動略過,以加速編譯速度。
  4. Link Script
    通常我們編譯一般的程式的時候,都不會自己寫 link script , 但是作業系統核心是比較特殊的程式,所以也需要自己設計下 link script 了。
  5. 掛載 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
lretq
switch_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
lretq
go_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_Table
TSS64_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 script
head.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 guard
clean:
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

  1. 一個64位操作系統的設計與實現
  2. FS and GS registers in 64 bit mode from the intel architecture manual
  3. segmentation 和保護模式
  4. x86 記憶體區段
  5. CPU Registers x86-64
  6. x86 memory segmentation
  7. 分頁架構

TODO

回答 Q1 :

回答 Q2 :

回答 Q3 :

--

--

吳建興
吳建興

Written by 吳建興

I want to be a good programmer.(´・ω・`)

No responses yet