一、背景介绍
早期的 Linux(如 0.11)中,boot/
目录下的启动代码基本全是汇编,主要由 bootsect.s
、setup.s
和 head.s
构成
而现代 Linux 内核则将启动流程模块化,位于 arch/x86/boot/
下,汇编部分大幅缩减,大量初始化逻辑转交 C 实现
这背后体现的是编译器、链接器、硬件平台的进化,也展现了 Linux 启动代码组织方式的哲学变化
二、为什么早期启动只能使用汇编?
- CPU 初始状态为实模式,只能寻址 1MB 内存,无法执行标准 C 函数(没有堆栈、运行时环境等)
- 段寄存器设置、保护模式启用等底层操作,C 无法精确控制
- 编译器能力有限,链接器不易控制内存布局,裸机程序难以生成
✅ 因此早期启动代码必须用汇编手写,才能安全进入下一阶段
三、为何后来逐步使用 C?
随着工具链能力增强,C 成为理想选择:
- 可读性、可维护性强
- 结构化代码减少 bug
- 支持跨架构移植
- GCC 支持
-ffreestanding
、裸函数、内联汇编等特性 - 链接器支持自定义内存布局(ld 脚本)
四、现代 Linux 启动代码结构(x86)
位于 arch/x86/boot/
:
- 汇编文件(如
pm.S
,head.S
):- 初始化段寄存器
- 设置 GDT
- 开启保护模式(设置 CR0、
ljmp
)
- C 文件(如
main.c
,video.c
):- 初始化内存、设备
- 处理内核参数
- 复制内核镜像
- 最终跳转到内核入口
五、从实模式到 C
- 实模式启动:
- BIOS 加载 MBR 到 0x7C00
- 汇编设置 DS/SS/堆栈
- 进入保护模式(仍由汇编完成):
- 设置 GDT
- 设置 CR0.PE = 1
ljmp
切换到 32 位代码段
- 初始化继续交给 C:
- 内存页表、堆初始化
- 调用
startup_32()
,最终跳转start_kernel()
六、使用 C 的优势
优势项 | 汇编方式 | C + GCC方式 |
---|---|---|
内存布局控制 | 手写偏移地址 | 链接器脚本 .ld + 符号变量控制 |
段/页初始化 | 全手动 | 可用结构、函数封装 |
字符串/数组操作 | 手动 ASCII + 显存偏移 | 直接使用标准 C 表达式 |
逻辑结构 | 跳转/标签 | 条件分支、函数、循环语义清晰 |
可移植性/复用性 | 架构相关性强 | GCC 支持交叉编译,适配多平台 |
七、现代工具链的支持:freestanding C、链接器脚本等
-ffreestanding
:生成不依赖 libc 的代码(无main()
、无堆栈假设)__attribute__((naked))
:生成裸函数.S
汇编入口 → C 初始化逻辑ld
脚本精细控制.text/.data/.bss
的布局与加载地址
💡 举个例子:你可以在 0x7C00 开始布局一段 bootloader,前面是 _start
汇编入口,后续就是 C 的 kernel_main()
八、总结:从汇编之力到 C 的优雅
启动阶段 | 使用语言 | 作用说明 |
---|---|---|
实模式初始化(BIOS 加载) | 汇编 (.S 文件) | 设置段寄存器、堆栈,准备最小运行环境(如 bootsect.s ) |
切换到保护模式 | 汇编 | 设置 GDT,修改 CR0 寄存器 PE 位,使用 ljmp 进入 32 位模式 |
保护模式运行初始化阶段 | C + 汇编 | 设置分页、复制内核映像、内核参数处理、设备初始化等 |
内核主流程(start_kernel) | C | 内核主逻辑、驱动管理、调度器初始化、用户空间启动等 |
虽然现代内核启动中 C 扮演了越来越多角色,但真正的“模式切换”仍然是汇编控制的核心步骤 —— 汇编点燃引擎,C 驾驶系统