本节将创建一个没有内容的内核,并尝试启动该内核。
关键字: kernel,ELF format,makefile
目标: 创建一个简单的内核,并且使用一个bootsector来启动它。
内核
我们的C(语言)内核将会简单的在屏幕的左上角打印一个X
。
// kernel.c
void dummy_test_entrypoint() {
}
void main() {
char* video_memory = (char*) 0xb8000; // VGA显示的内存位置
*video_memory = 'X';
}
你可能注意到kernel.c中有一个奇怪的空函数dummy_test_entrypoint
,这是教程制作者故意安排的,因为这样的话,main函数就不会在kernel.c生成的kernel.o的地址0x0位置,这就迫使我们需要做一些额外操作,才能正确启动内核(我们这个例子里起始就是main)。这个问题先放在一旁,首先我们使用gcc将该kernel
编译成目标文件。
gcc -fno-pie -m32 -ffreestanding -c kernel.c -p kernel.o
接下来是内核的入口程序。
; kernel_entry.asm
; 32bit寻址
[bits 32]
; EXTERN在汇编中用来引用一个在其他模块中定义过的符号名,使得这个符号名所表示的数据或函数能在该模块中被使用。
[extern main]
call main
; 无限循环
jmp $
对kernel_entry.asm进行编译,不过这次我们要编译为elf(Executable and Linkable Format)格式,这种格式既能链接又能执行,用途比较广。
nasm kernel_entry.asm -o kernel_entry.o
链接器
将上面生成的2个目标文件(.o)文件链接成一个二进制文件,并且解决label的依赖问题,运行:
ld -pie -m elf_i386 -o kernel.bin -Ttext 0x1000 kernel_entry.o kernel.o --oformat binary
上面的这一串命令,能将kernel_entry.o和kernel.o放在kernel.bin的0x1000(.text)处,.text段通常用于放置内核代码,所以在启动内核时,会从镜像文件的0x1000处执行,首先执行kernel_entry.asm中的call main
,而call main又会jmp到kernel.c中的具体位置,而避免了kernel.c中dummy_test_entrypoint
的干扰。
需要注意的是,我们的内核放在0x1000处,而不是0x0处,所以在之后的bootsector中,需要指明需要启动的内核的位置。
bootsector(启动单元)
首先来看bootsector的代码。
;bootsect.asm
[org 0x7C00]
KERNEL_OFFSET equ 0x1000 ; 在这里定义一个宏来指定内核的位置
mov [BOOT_DRIVE], dl ; BIOS sets the boot drive in 'dl' register on boot
mov bp, 0x9000 ; build a stack whose stack base is 0x9000
mov sp, bp
mov bx, MSG_REAL_MODE
call print
call print_nl
call load_kernel ; read kernel from disk (actually from memroy)
call switch_to_pm ; disable interrupts, load GDT, etc. Finally jumps to 'BEGIN_PM'
jmp $ ; never executed
%include "../05-bootsector-functions-strings/boot_sect_print.asm"
%include "../05-bootsector-functions-strings/boot_sect_print_hex.asm"
%include "../07-bootsector-disk/boot_sect_disk.asm"
%include "../09-32bit-gdt/32bit-gdt.asm"
%include "../08-32bit-print/32bit-print.asm"
%include "../10-32bit-enter/32bit-switch.asm"
[bits 16]
load_kernel:
mov bx, MSG_LOAD_KERNEL
call print
call print_nl
mov bx, KERNEL_OFFSET ; read from disk and store into 0x1000
mov dh, 2
mov dl, [BOOT_DRIVE]
call disk_load
ret
[bits 32]
BEGIN_PM:
mov ebx, MSG_PROT_MODE
call print_string_pm
call KERNEL_OFFSET ; give control to the kernel
jmp $ ; stay here when the kernel returns controls to us(if ever)
BOOT_DRIVE db 0 ; we store boot drive in memory. just an example.
MSG_REAL_MODE db "Started in 16-bit REAL MODE", 0
MSG_PROT_MODE db "Landed in 32-bit Protected Mode", 0
MSG_LOAD_KERNEL db "loading kernel into memory", 0
times 510-($-$$) db 0
dw 0xAA55
从之前的教程中,我们知道bootsector占512个字节,上面的bootsect.asm做了以下几件事:
- 从BIOS读取启动驱动器号到BOOT_DRIVE位置
- 建立从0x9000开始的栈
- 打印MSG_REAL_MODE代表的信息
- 打印MSG_LOAD_KERNEL代表的信息
- 从BOOT_DRIVE指示的硬盘/软盘中读取内核到内存0x1000位置
- 进入保护模式并打印MSG_PROT_MODE代表的信息
- 调用内核所在位置,也就是0x1000
- 死循环
编译该源文件:
nasm bootsect.asm -f bin -o bootsect.bin
连接所有文件
到现在,我们拥有了bootsect.bin和kernel.bin,将这两个文件连接到一起,便形成了我们的第一个自制os镜像!
cat bootsect.bin kernel.bin > os-image.bin
运行
现在就可以用qemu对该镜像进行运行了,如果运行时发生了硬盘载入错误,那么可能需要对qemu的启动选项加上-fda
,也就是floppy disk a的意思。
启动os:
qemu-system-i386 -fda os-image.bin
注:译者没有编译qemu的i386版本,直接使用的x86_64版本,即:
qemu-system-x86_64 -fda os-image.bin
运行结果:
Makefile
上面输入编译各个源文件的过程是不是很繁琐,别怕,其实有更加自动化的方法,即makefile的使用。
makefile的好处见我的另一篇博客,即Makefile Tutorial。
# ===========常用宏=======================
# $@ = target file(目标文件) |
# $< = first dependency(第一个依赖) |
# $^ = all dependencies(所有依赖) |
# ========================================
# 第一个规则用于没有任何选项传给make的情况
all:run
# kernel.bin由kernel_entry.o和kernel.o两个文件合成
kernel.bin: kernel_entry.o kernel.o
ld -pie -m elf_i386 -o $@ -Ttext 0x1000 $^ --oformat binary
# kernel_entry.o由kernel_entry.asm编译而来
kernel_entry.o: kernel_entry.asm
nasm $< -f elf -o $@
# kernel.o由kernel.c编译而来
kernel.o: kernel.c
gcc -fno-pie -m32 -ffreestanding -c $< -o $@
# 定义用于反汇编的规则,在debug时有用
kernel.dis: kernel.bin
ndisasm -b 32 $< > $@
# bootsect.bin由bootsect.asm编译而来
bootsect.bin: bootsect.asm
nasm $< > $@
# os-image.bin由bootsect.bin和kernel.bin合成
os-image.bin: bootsect.bin kernel.bin
cat $^ > $@
# 为make run,即用qemu运行os-image提供规则
run: os-image.bin
qemu-system-x86_64 -fda $<
# 为make clean提供规则,即清除编译结果
clean:
rm *.bin *.o *.dis
以后直接运行make即可启动我们的自制os.