前三章全是汇编。到这里,CPU 已经跑在 64 位长模式下了,但还是什么都做不了。

这一章要完成最关键的一步:把控制权从汇编交给 C。之后的内核全部用 C 写,汇编只在最必要的地方出现。


磁盘要怎么布局

512 字节的 Bootloader 放不下 C 内核,所以内核单独编译成一个二进制文件,放在磁盘的后续扇区(扇区,Sector,磁盘读写的最小单位,传统上每个 512 字节):

扇区 0(512B):Bootloader
扇区 1~8     :内核二进制

Bootloader 启动后,用 BIOS 的磁盘读取中断把扇区 1 开始的内容读到内存地址 0x10000,然后跳过去执行。


一个容易踩的坑:内核入口不在 0x10000

把内核编译好、加载到 0x10000,然后 call 0x10000——看起来没问题,实际上很可能直接崩。

objdump(反汇编工具,可以查看二进制文件的结构和函数地址)一查才发现:kernel_main 的实际地址是 0x10109,不是 0x100000x10000 开头是另一个函数 outb,跳过去执行完全是错的。

解决方案:加一个 entry.asm 作为内核最开头,内容只有一行:跳转到 kernel_main。然后在链接脚本(Linker Script,告诉链接器如何拼装各个目标文件、每段放在哪个地址)里把 entry.o 排在最前面,保证 _start 恰好落在 0x10000

这样 Bootloader 跳到 0x10000,第一条指令就是 jmp kernel_main,稳了。


还有一个坑:栈

进入 64 位模式之后,rsp(栈顶指针寄存器)是未初始化的。C 函数一用栈就崩。

在跳入内核之前,要先设好栈:

mov rsp, 0x90000

0x90000 是因为这块内存没有被占用,往下增长也不会覆盖内核代码。


内核编译参数

内核不能用普通方式编译,要加几个关键选项:

选项 原因
-ffreestanding 没有标准库(libc),不能依赖它
-fno-stack-protector 栈保护需要额外运行时,内核里没有
-mno-red-zone 红区(Red Zone,栈指针下方 128 字节的临时区域,用户态可用)在内核中断处理时会被破坏,必须关闭
-fno-pic 关闭位置无关代码(PIC,Position Independent Code,地址运行时才确定),内核加载地址固定,不需要

少一个都可能出莫名其妙的问题。


磁盘镜像也有坑

cat boot.bin kernel.bin > myos.img 拼出来的镜像,会导致磁盘读取失败。

原因:BIOS 按扇区(512字节)读盘,cat 直接拼接不保证边界对齐。必须用 dd 来构造镜像:

dd if=/dev/zero of=myos.img bs=512 count=20   # 先创建空镜像
dd if=boot.bin of=myos.img bs=512 conv=notrunc         # 写入 Bootloader
dd if=kernel.bin of=myos.img bs=512 seek=1 conv=notrunc # 从第1扇区写内核

seek=1 表示从第 1 扇区开始写,跳过 Bootloader 占的第 0 扇区。


跑起来

make && qemu-system-x86_64 -drive format=raw,file=myos.img -nographic -serial mon:stdio

输出:

Kernel loaded! Welcome to myOS
Now running in 64-bit C kernel.

这行字是从 C 代码里打出来的。启动链路彻底打通了:

BIOS → Bootloader → 保护模式 → 长模式 → kernel_main()

这一章之后

有了 C 内核,后面可以做更多事情了。下一章是中断处理——让内核能响应键盘输入、时钟信号、甚至程序异常,是操作系统最核心的机制之一。

源码在这里:github.com/tongpengfei/learn-with-ai