从0创建一个OS(十四) 裸内核框架

本节将创建一个没有内容的内核,并尝试启动该内核。

关键字: 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做了以下几件事:

  1. 从BIOS读取启动驱动器号到BOOT_DRIVE位置
  2. 建立从0x9000开始的栈
  3. 打印MSG_REAL_MODE代表的信息
  4. 打印MSG_LOAD_KERNEL代表的信息
  5. 从BOOT_DRIVE指示的硬盘/软盘中读取内核到内存0x1000位置
  6. 进入保护模式并打印MSG_PROT_MODE代表的信息
  7. 调用内核所在位置,也就是0x1000
  8. 死循环

编译该源文件:

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

运行结果:

从0创建一个OS(十四)  裸内核框架

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.

上一篇:Linux C: 内嵌汇编语法


下一篇:gcc asm