一、背景介绍

早期的 Linux(如 0.11)中,boot/ 目录下的启动代码基本全是汇编,主要由 bootsect.ssetup.shead.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

  1. 实模式启动:
  • BIOS 加载 MBR 到 0x7C00
  • 汇编设置 DS/SS/堆栈
  1. 进入保护模式(仍由汇编完成):
  • 设置 GDT
  • 设置 CR0.PE = 1
  • ljmp 切换到 32 位代码段
  1. 初始化继续交给 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 驾驶系统