从零写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)。

当按下电源键:

  1. CPU 从 地址 0xFFFFFFF0 开始执行(这是复位向量)。
  2. BIOS 代码(固化在主板 ROM 中)被加载运行。
  3. BIOS 自检硬件(内存、磁盘、键盘等)。
  4. 然后,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 状态

💬 写在最后

操作系统不是魔法,
它是由一行行代码、一个个寄存器、一段段内存精心构建的数字世界基石

而你的 "Hello World",
正是这个世界的第一缕光。

🌟 从 1MB 开始,你写的不只是代码,而是一个新世界的起点。


📬 喜欢这篇深度解析?欢迎点赞、转发、关注!
👇 评论区提问:你还想深入解析哪个部分?GDT?IDT?还是分页机制?


#操作系统 #内核开发 #x86 #BIOS #GRUB #链接脚本 #裸机编程 #从零开始