从零写OS内核-第二篇:Hello World 解析
你以为“Hello World”只是打印一句话? 在操作系统内核里,它背后藏着启动流程、内存布局和硬件协议的秘密。
上一篇,我们成功让自己的内核在 QEMU 上输出了 “Hello from the kernel!”。 看起来很简单,对吧? 但你有没有想过:
为什么代码要放在
1MB 地址
?
为什么需要一个奇怪的
linker.ld
文件?
BIOS(或 GRUB)到底做了什么?
为什么不能直接
printf
?
今天,我们就来 拆解这个“Hello World”内核 ,揭开 x86 裸机启动的底层逻辑。
🧩 一、BIOS:PC 启动的第一步
在 x86 PC 世界里,一切始于 BIOS (Basic Input/Output System)。
当按下电源键:
- CPU 从
地址 0xFFFFFFF0
开始执行(这是复位向量)。 - BIOS 代码(固化在主板 ROM 中)被加载运行。
- BIOS
自检硬件
(内存、磁盘、键盘等)。 - 然后,BIOS
加载引导扇区
(512 字节)到内存
0x7C00
,并跳转执行。
⚠️ 但现代系统很少直接用 BIOS 引导自定义内核——因为太麻烦! 所以我们通常借助 GRUB (一个更强大的 bootloader),它支持 Multiboot 协议 ,能直接加载 ELF 格式的内核。
✅ GRUB 的作用 :
解析内核 ELF 文件
将代码段、数据段放到指定内存位置
设置好 CPU 状态(已处于保护模式)
跳转到
_start
入口
这就是为什么我们的内核不需要处理“实模式→保护模式”转换—— GRUB 已经帮我们搞定了!
🔗 二、链接脚本(Linker Script):内核的“户型图”
编译后的代码不会自动知道该放哪。 链接器(Linker) 负责把 .text (代码)、 .data (初始化数据)、 .bss (未初始化数据)等段拼接到一起。
而 linker.ld 就是告诉链接器:“请按我说的方式布局内存”。
来看我们用的链接脚本:
🔍 逐行解析:
1️⃣ . = 1M;
表示
加载地址(VMA)从 1MB(0x100000)开始
。
为什么是 1MB?
- 早期 PC 的
实模式
只能访问 1MB 以下内存(地址线 20 位)。 - 1MB 以上是“扩展内存”,
保护模式才能使用
。 - GRUB 默认将内核加载到 1MB 以上,避免与 BIOS 数据冲突。
💡 注意:这不是物理限制,而是 历史约定 + GRUB 默认行为 。
2️⃣ .text BLOCK(4K) : ALIGN(4K)
.text
段(代码)按
4KB 对齐
。
为什么对齐?
- 便于后续启用
分页(Paging)
(x86 页大小为 4KB) - 提升内存访问效率
- 某些硬件要求对齐
3️⃣ (.multiboot)
把所有目标文件中的
.multiboot
段放在这里。
这个段包含
Multiboot 头
(Magic Number 等),GRUB 会扫描它来识别可引导内核。
4️⃣ 其他段( .rodata , .data , .bss )
按顺序排列,符合常规程序内存布局:
- 代码 → 只读数据 → 已初始化数据 → 未初始化数据
.bss
不占 ELF 文件空间,但会在内存中预留(链接器自动清零)
🧱 三、内存布局图(简化版)
✅ 栈为什么单独设在高地址? 避免和代码/数据段冲突。我们的 boot.asm 手动分配了 16KB 栈空间( .bss 中的 stack_top )。
🛠️ 四、为什么不能用
printf
?
因为:
没有 C 标准库
(libc):
printf
是库函数,依赖操作系统提供的“系统调用”(如
write
)。
没有文件描述符、没有终端抽象
:我们的“输出”是直接写
VGA 显存(0xB8000)
,属于
裸硬件操作
。
所以,内核开发中:
所有 I/O 都要自己实现(串口、显存、磁盘)
不能使用动态内存(没有
malloc
,除非自己写内存管理器)
不能使用异常、全局构造函数等高级特性
📌 内核是“站在硬件之上的第一层软件”,它 自己就是操作系统的一部分 ,不能依赖操作系统!
🌱 五、下一步:从 Hello World 到真正的内核
现在你已经理解了:
BIOS/GRUB 如何启动内核
链接脚本如何控制内存布局
为什么输出要直接操作显存
接下来,你可以尝试:
在链接脚本中加入
.eh_frame
(避免 GCC 优化报错)
添加
堆(heap)
支持动态内存
实现
串口输出
(比 VGA 更可靠,适合调试)
配置
GDT(全局描述符表)
,正式接管 CPU 状态
评论