自制x86 Bootloader開發筆記(2)——— Bootloader設計與啟動區代碼實現
計算機啟動流程簡介
要知道如何設計bootloader,需要先了解一下計算機啟動的流程。具體可見引用1,這里只需要關注以下這一點即可:
- 系統啟動后會自動將硬盤的第一個扇區(主引導記錄,MBR)加載至內存0x7c00處,并檢查MBR的第511和第512個字節是否為0x55和0xaa,如果是,則跳轉至0x7c00出開始執行對應的代碼。
只要知道了這一點,我們的開發之路就可以正式啟動了,因為從代碼跳轉至0x7c00處開始,控制權就轉交到了我們所編寫的代碼之中。
總體設計
bootloader的作用就和它的名字一樣,一個作用是boot,另一個作用是loader,它負責電腦上電啟動后的一些系統準備工作,完成后加載內核并且跳轉到內核的入口,將控制權轉交給內核。這次要實現的bootloader比較簡單,主要完成以下幾個功能
編寫Makefile
我們使用Makefile來控制項目的構建,這里分成了三個Makefile文件。第一個最外層的Makefile負責生成硬盤鏡像、格式化文件系統、往硬盤寫入booloader以及內核等環境初始化工作。其他兩個Makefile則是子Makefile,負責bootloader的構建和內核的構建,項目結構如下:
|--最外層Makefile
|--bootloader
| |-- bootloader的Makefile
| |-- bootloader的源代碼
|--kernel
| |-- kernel的Makefile
| |-- kernel的源代碼
最外層Makefile
CORES = $(shell grep -c ^processor /proc/cpuinfo 2>/dev/null || sysctl -n hw.ncpu)
DISK = kernel.img
FSC_OBJ = ./bootloader/boot.o
LOADER_OBJ = ./bootloader/loader.o
FORMATOR = ../tools/mkmyfs/mkmyfs
KERNEL = ./kernel/arcus_kernel
all: build qemu
build: clean build_bootloader build_kernel
ifneq ($(FORMATOR), $(wildcard $(FORMATOR)))
$(MAKE) all -j$(CORES) -C ../tools/mkmyfs
endif
ifneq ($(DISK), $(wildcard $(DISK)))
dd if=/dev/zero of=$(DISK) bs=1024 count=524288
sleep 5
endif
$(FORMATOR) $(DISK) -f
dd if=$(FSC_OBJ) of=$(DISK) conv=notrunc
dd if=$(LOADER_OBJ) of=$(DISK) conv=notrunc seek=3
$(FORMATOR) $(DISK) -w $(KERNEL)
sleep 1
.PHONY: build_bootloader
build_bootloader:
$(MAKE) all -j$(CORES) -C bootloader
.PHONY: build_kernel
build_kernel:
$(MAKE) all -j$(CORES) -C kernel
.PHONY: qemu
qemu:
qemu-system-x86_64 -smp 8 -m 8g -hda $(DISK) -monitor stdio -no-reboot
.PHONY: clean
clean:
$(MAKE) clean -C bootloader
$(MAKE) clean -C kernel
這里介紹最關鍵的幾個部分:
- 硬盤鏡像生成:使用
dd命令生成空的文件,dd if=/dev/zero of=$(DISK) bs=1024 count=524288,命令表示生成1024*524288字節的文件,內部全部填充空數據0x00。 - Makefile遞歸執行:使用Makefile -C參數切換makefile工作目錄
- 硬盤格式化:自己實現了一個建議文件系統和格式化工具,代碼可見項目的tool目錄,這里不進行贅述
- 啟動區代碼寫入:同樣使用
dd命令寫入,dd if=$(FSC_OBJ) of=$(DISK) conv=notrunc,conv=notrunc表示不進行截斷,硬盤鏡像文件顯然比bootloader大很多,寫入bootloder后其他剩余的扇區當然要保留下來 - elf loader寫入:
dd if=$(LOADER_OBJ) of=$(DISK) conv=notrunc seek=3,依舊是dd命令,啟動區我們使用匯編實現,而elf loader因此邏輯更復雜,采用了C語言,因此編譯結果是一個獨立的二進制文件,需要額外寫入,seek=3表示跳過前三個扇區,即跳過之前寫入的bootloader
bootloader的Makefile
ASM = nasm
CC = x86_64-elf-gcc
OBJCOPY = x86_64-elf-objcopy
LD = x86_64-elf-ld
FST_PART_SRC = boot.asm
LOADER_C_SRC = $(shell find . -name "*.c")
LOADER_C_OBJ = $(patsubst %.c,%.o,$(LOADER_C_SRC))
LOADER_ASM_SRC = $(shell find . -name "*.asm" ! -path "./boot.asm")
LOADER_ASM_OBJ = $(patsubst %.asm,%.o,$(LOADER_ASM_SRC))
TARGET_FST_PART = boot.o
TARGET_LOADER_TMP = loader_tmp.o
TARGET_LOADER = loader.o
all: clean first_part kernel_loader
.PHONY: first_part
first_part: $(FST_PART_SRC)
$(ASM) $(FST_PART_SRC) -f bin -g -o $(TARGET_FST_PART)
.PHONY: kernel_loader
kernel_loader: link
$(OBJCOPY) -O binary $(TARGET_LOADER_TMP) $(TARGET_LOADER)
.PHONY: link
link: $(LOADER_C_OBJ) $(LOADER_ASM_OBJ)
$(LD) -nostdlib -Ttext 0x8200 $(LOADER_C_OBJ) $(LOADER_ASM_OBJ) -T scripts/loader.ld -o $(TARGET_LOADER_TMP)
%.o:%.c
$(CC) -c -w -ffreestanding -I ./include -o $@ $<
%.o:%.asm
$(ASM) -f elf64 -o $@ $<
.PHONY: clean
clean:
-rm $(TARGET_FST_PART) $(TARGET_LOADER_TMP) $(TARGET_LOADER) $(LOADER_C_OBJ) $(LOADER_ASM_OBJ)
同樣介紹最關鍵的幾個部分:
- 啟動區前三個扇區使用匯編實現,完成讀取硬盤內容,進入長模式等工作。elf loader因邏輯更復雜,使用C語言實現
- elf loader(即kernel_loader這個任務)編譯出來的產物不能直接使用,因為gcc編譯出來的64位可執行文件不是純二進制的,它有著自己的ABI,就是ELF格式,我們還指望elf loader來實現加載elf文件的功能呢,當前并沒有運行elf文件的能力,只能接受純二進制的代碼。因此這里需要一些trick,只把代碼里的可執行代碼和數據拿出來,這樣就得到了我們編寫的代碼的純二進制格式,使用objcopy命令即可完成這個工作
$(OBJCOPY) -O binary $(TARGET_LOADER_TMP) $(TARGET_LOADER) - 在最外層的Makefile我們知道elf loader的寫入位置是第四個扇區,即0x7c00 + (512 * 3) = 0x8200,因此需要指定elf loader的程序入口點是0x8200, 使用
-Ttext 0x8200,并配合鏈接器腳本,即可讓鏈接器將程序的入口點函數錨定到0x8200的位置,鏈接器腳本如下:
ENTRY(main)
SECTIONS
{
.text :
{
*(.text.main);
*(.text*);
}
}
*(.text.main)指定了.text.main段在text段中排在最前面的位置,配合C語言void loader_main() __attribute__ ((section (".text.main"))); ,將loader_main函數放置到.text.main中,即可讓代碼跳轉到0x8200就開始執行loader_main函數。由此就完成了匯編語言和C語言純二進制產物的融合。
編寫啟動區代碼
Makefile編寫完成之后,就終于可以開始bootloader的代碼編寫了,首先開發第一個扇區,就是啟動區的代碼:
[BITS 16]
org 7c00h
mov si, 0
mov ax, cs
mov ds, ax
mov es, ax
mov esp, 7c00h
jmp load_stage2
disk_rw_struct:
db 16 ; size of disk_rw_struct, 10h
db 0 ; reversed, must be 0
dw 0 ; number of sectors
dd 0 ; target address
dq 0 ; start LBA number
read_disk_by_int13h:
mov eax, dword [esp + 8]
mov dword [disk_rw_struct + 4], eax
mov ax, [esp + 6]
mov word [disk_rw_struct + 2], ax
mov eax,dword [esp + 2]
mov dword [disk_rw_struct + 8], eax
mov ax, 4200h
mov dx, 0080h
mov si, disk_rw_struct
int 13h
ret
load_stage2:
push dword 0x7e00 ; target address
push word 50 ; number of blocks
push dword 1 ; start LBA number
call read_disk_by_int13h
add esp, 10
jmp enter_long_mode
times 510-($-$$) db 0
dw 0xaa55
這段代碼編譯之后正好是512個字節,是一個扇區的大小,正好填滿啟動區。代碼中times 510-($-$$) db 0這行表示重復填0直到填滿510個字節位置,而dw 0xaa55則使得這個扇區的最后兩個字節滿足啟動區簽名的條件,使其能夠被識別為一個啟動區。
第一個扇區的功能很簡單,就是調用BIOS中斷中的擴展INT 13h中斷,讀取之后的幾個扇區,并且跳轉到第二個扇區。下面這張表摘自引用2,描述了INT 13h中斷需要使用的參數:
| Registers | Description | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| AH | 42h = 擴展讀功能函數編號 | ||||||||||||||||||
| DL | 驅動器編號 (e.g. 1st HDD = 80h) | ||||||||||||||||||
| DS:SI | segment:offset pointer to the DAP
|
我們將之后幾個扇區加載到啟動區之后的扇區,也就是0x8000的位置,讓后跳轉至第二個扇區,啟動區的工作就結束了。
之后進入長模式和文件系統以及ELF Loader的內容相對獨立,放在之后的章節描述。
項目地址:https://github.com/basic60/ARCUS

浙公網安備 33010602011771號