這系列的文章會記錄我嘗試做出一個 Linux OS kernel的過程,要主是依照 https://littleosbook.github.io/ 這系列的文章,加上自已踩下的坑,希望能幫助同樣有做一個作業系統夢想的各位。
進入第三章,這是一章介紹性章節,告訴我們可以開始使用 C 來為我們的 OS 服務了,主要關注於如何讓 Assembly 跟 C 可以連結在一起使用。
首先,要知道使用 C 的主要因原是因為 “Stack” —— 所有有意義的 C programs 都會使用到 Stack。
要在 Assembly 中呼叫 C 的 function,文章中提到是使用 cdecl calling convention (呼叫慣例) 方法,因為這是 GCC 在使用的,更多詳情可到 x86 calling conventions 查看。主要與 Stack 有關的是,function 的參數會以 Stack 的方式,從按照從右至左的順序依次推到 (push) Stack 裡面,而 function 結果儲存在 eax
register 中,如以下範例 :
/* The C function */
int sum_of_three(int arg1, int arg2, int arg3)
{
return arg1 + arg2 + arg3;
}; The assembly code
external sum_of_three ; the function sum_of_three is defined
elsewhere
push dword 3 ; arg3
push dword 2 ; arg2
push dword 1 ; arg1
call sum_of_three ; call the function, the result will be in
eax
然後我們需要了解一下,什麼是 Packed Structures。
先了解一下什麼是 Padding (可以參考這篇 stack overflow)。struct
中的每個成員都應該要位於可按其大小整除的地址,編譯器會在你的 struct
裡面為你的成員間或最後插入空間,目的是為了對齊 (align) 記憶體位置的邊界,這樣做是為了使硬件更輕鬆,更有效率。
而 Packing 則是把 Padding 都移除掉,使 struct
保持著原有的大小 。(在以下的內容,我們希望能刻意使 struct 的大小為 32 bits,但因為有 Padding 機制存在,所以我們需要 Packing 來控制大小)。
現在我們需要用 struct
來放置我們的 “configuration bytes”,以下的例子為 32 bits 系統的 configuration bytes :
Bit: | 31 24 | 23 8 | 7 0 |
Content: | index | address | config |
因為硬體會認為 struct
是一個 32 bits unsigned int
,我們只需要使用__attribute__((packed))
就可以強制 GCC 不加 padding。
struct example {
unsigned char config; /* bit 0 - 7 ,char:1-byte = 8-bits */
unsigned short address; /* bit 8 - 23 ,short:2-byte = 16-bits */
unsigned char index; /* bit 24 - 31 */
} __attribute__((packed));
我們現在可以開始試著編譯 C 了,因為我們的 OS 不能使用任個的 standard library,所以我們需要使用以下的 flags 來編譯 C :
-m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector
-nostartfiles -nodefaultlibs
-m32
: 可以在配置為默認編譯 64-bit 對象的編譯器上編譯 32-bit 對象。-nostdlib
: 不使用 standard library。-nostdinc
: 不要把系統提供的標頭檔給包進來。-fno-builtin
: 不使用C 中的內建 function。-fno-stack-protector
: 關閉 stack protector,stack protector 的作用是防止出現緩衝區溢位 (Stack buffer overflow)。-nostartfiles
: 不使用 standard system startup files。-nodefaultlibs
: 不使用 standard system librarie。
而且在寫 C 時,我們習慣盡量把所有警告變為 error,這可以使我們的 code 更安全且不易出錯 :
-Wall -Wextra -Werror
現在我們可以創建一個 C 檔 kmain.c
,並在裡面創建一個 kmain
function。這將會被 loader.s
使用。
void kmain(){} //kmain.c
最後,我們創建一個 makefile :
OBJECTS = loader.o kmain.o
CC = gcc
CFLAGS = -m32 -nostdlib -nostdinc -fno-builtin -fno-stack-protector \ -nostartfiles -nodefaultlibs -Wall -Wextra -Werror -c
LDFLAGS = -T link.ld -melf_i386
AS = nasm
ASFLAGS = -f elfall: kernel.elfkernel.elf: $(OBJECTS)
ld $(LDFLAGS) $(OBJECTS) -o kernel.elfos.iso: kernel.elf
cp kernel.elf iso/boot/kernel.elf
genisoimage -R \
-b boot/grub/stage2_eltorito \
-no-emul-boot \
-boot-load-size 4 \
-A os \
-input-charset utf8 \
-quiet \
-boot-info-table \
-o os.iso \
isorun: os.iso
bochs -f bochsrc.txt -q%.o: %.c
$(CC) $(CFLAGS) $< -o $@%.o: %.s
$(AS) $(ASFLAGS) $< -o $@clean:
rm -rf *.o kernel.elf os.iso
這會是現在的資料夾結構 :
.
|-- bochsrc.txt
|-- iso
| |-- boot
| |-- grub
| |-- menu.lst
| |-- stage2_eltorito
|-- kmain.c
|-- loader.s
|-- Makefile
|-- link.ld
(文章這邊少打了一個 link.ld
檔。)
我們在 makefile 所在的位置執行 make run
指令,編譯通過後就會啟動 Bochs。
下一章,我們來了解如何實作硬體跟 OS 之間的互動 :
自制一個 Linux 作業系統(5) — Output (上)