你以为“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 就是告诉链接器:“请按我说的方式布局内存”。
来看我们用的链接脚本:
ENTRY(_start)SECTIONS { . = 1M; .text BLOCK(4K) : ALIGN(4K) { *(.multiboot) *(.text) } .rodata : { *(.rodata) } .data : { *(.data) } .bss : { *(.bss) }}🔍 逐行解析:
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 文件空间,但会在内存中预留(链接器自动清零)
🧱 三、内存布局图(简化版)
0x00000000 ┌──────────────────────┐ │ BIOS 数据 │ ← 1MB 以下,GRUB/BIOS 使用 ├──────────────────────┤0x00100000 │ .text 段 │ ← 我们的代码(含 Multiboot 头) │ .rodata │ │ .data │ │ .bss │ ├──────────────────────┤ │ 栈 │ ← 向下增长(我们在 boot.asm 中设为高地址) └──────────────────────┘✅ 栈为什么单独设在高地址?
避免和代码/数据段冲突。我们的 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 状态
评论