本节将创建一个没有内容的内核,并尝试启动该内核。
关键字: 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的代码。
;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做了以下几件事:
编译该源文件:
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 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.