linux内核引导启动程序001:唤醒沉睡的猛兽——内核启动的“接力赛”

linux内核引导启动程序001:唤醒沉睡的猛兽——内核启动的“接力赛”

欢迎来到全书中最为硬核、也最命悬一线的全新篇章——第 3 章:内核引导启动程序。

在之前的旅程中,我们仿佛站在上帝视角,俯瞰了整座“内核城”的城建蓝图。我们了解了进程是怎么调度的、内存是怎么映射的、甚至知道了这块基石是拿什么工具(Makefile)铸造出来的。

但现在,我们要做的,是把这座蓝图上的城市,在现实中真正“砌”起来。我们需要把一台刚刚通电、处于蒙昧状态的 PC 电脑,一步步引导它进入 32 位保护模式,最终跳入 C 语言的殿堂——main()函数。

这整个过程,就像一场极其精密的“接力赛”,任何一棒交接失误,电脑都会直接黑屏死机。下面,请你坐稳,我们开始拆解这枚 386 时代的“硬核导弹”是如何发射的。


第一章:起跑线上的三个“火炬手”(表 3-1 分析)

翻开你拍的第一张照片,书上直接甩出了一张表 3-1。这里面藏着 Linux 0.11 启动阶段最重要的三个文件:

  1. bootsect.s(5052 字节):吹响第一声冲锋号的“引路人”。它只有 5KB 左右,却是系统能运转的第一步。
  2. setup.s(5938 字节):负责接管机器、收集信息的“硬件侦察兵”。
  3. head.s(5938 字节):真正进入 32 位世界的“保护模式先锋官”。

书中特别给了一个重要的警示:这三个文件虽然都是汇编,但用的“方言”完全不同!

  • bootsect.ssetup.s使用的是as86编译器和ld86链接器。它们运行在 16 位“实模式”下,代码风格类似于 Intel 的官方手册。
  • head.s使用的是GNU as编译器和gld链接器。它运行在 32 位“保护模式”下,代码风格是 Linux 内核标准的AT&T 汇编格式

这就好比你和你的队友,前两位讲的是“8086老方言”,第三位讲的是“386新普通话”。跨越两种汇编格式,正是早期内核工程师需要跨越的第一道天堑。


第二章:电脑刚通电的那一刻(3.1 总体功能描述)

第一章我们弄清楚了“源文件是谁”,现在我们要看看“它们是怎么动起来的”。

书中文字描述:“当 PC 的电源打开后,80x86 结构的 CPU 将自动进入实模式,并从地址0xFFFF0开始自动执行程序代码。”

这第一个地址 0xFFFF0 就是 CPU 固化的“永久闹钟”。每次电脑开机,CPU 就跑到这个地址上去找东西运行,而这里正好存放着BIOS(基本输入输出系统)

2.1 BIOS 的“裁缝”工作

BIOS 运行起来后,会先做一道非常传统的“炒冷饭”操作:

  1. 硬件自检(POST):检查内存、键盘、显卡等硬件是否正常。
  2. 寻找启动盘:去读取软盘或硬盘的0 磁道,0 磁头,第 1 个扇区
  3. 种子植入:把这512 字节的代码,原封不动地复制到内存的绝对物理地址0x7C00处。
  4. 交出指挥棒:CPU 执行一条跳转指令,跳进0x7C00开始运行。

为什么是 0x7C00?
这里有一个鲜为人知的历史渊源。IBM 在制定 PC 兼容机标准时,刻意保留了 0x7C00 这个地址空间。它是为了保护当时 DOS 系统的关键数据不被打扰,从而让引导程序(Bootloader)有一个安全的专属“跳板”。一直沿用到今天。


第三章:一场史诗级的“搬家与护航”(图 3-1 深度剖析)

这个由 BIOS 加载到0x7C00的代码,正是我们的一号火炬手:bootsect.s。但这时候出现了一个尴尬的问题:

内核的最终目标是要把自己加载到内存的最低端(0x00000开始),这样运行效率才最高。但是,0x7C00 本身所在的位置,正好位于内核最终要占据的内存区域内!

如果bootsect.s不挪窝,等一会儿系统内核被加载下来,就会直接把bootsect.s给覆盖掉。于是,启动最关键的第一步——自复制移动,开始了。

为了让你看清这场内存的“走位”,我为你完全重绘了书中的图 3-1

阶段4_Head [第四阶段:head.s 与 C 语言的世界]

head.s 设置 IDT、GDT、LDT、分页

跳转到 init/main.c 中的 main() 函数

阶段3_Setup [第三阶段:setup.s 接棒与保护模式开启]

setup.s 读取硬件参数 并将 system 从 0x10000 移动到 0x00000

关闭中断、开启 A20 地址线、加载 GDT

CPU 跳入 32位保护模式,执行 head.s

阶段2_bootsect [第二阶段:引导程序自复制与加载]

bootsect.s 将自己从 0x7C00 复制到 0x90000

腾出 0x7C00 之前的空间

从磁盘读取 setup.s 到内存 0x90200

从磁盘读取 system 模块到内存 0x10000

阶段1_BIOS [第一阶段:BIOS 加载]

BIOS 从磁盘读取 boot sector 512字节

放入物理内存 0x7C00

CPU 跳入 0x7C00 执行 bootsect.s

阶段1_BIOS

阶段2_bootsect

阶段3_Setup

阶段4_Head

核心原理解密(书中有明确的高亮标注):

  • 0x7C00->0x90000bootsect搬到了内存的安全上方(640KB 以内),防止被覆盖。
  • 0x90200:紧接着bootsect的新家,后面紧挨着放置setup.s
  • 0x10000:负责把几百 KB 的system模块(包含head.s和后面所有的 C 语言代码)从软盘搬运到这里暂存。
  • 最终大挪移(0x10000->0x00000:当setup.s准备接管时,它把system模块再次从0x10000搬到最底部的0x00000处。因为只有这样,内核代码才能运行在物理地址的绝对低位,这对 386 的段式内存管理效率是最高的。

注意书本上的那句绝杀评价:“这可能是整个内核中最有决窍的代码了。如果在尚未正确操作之前,计算机就会死锁。”为什么?因为这一步步的内存移动,全靠手写汇编控制。哪一行内存搬运的rep movsw指令把目标地址写错了,或者lgdt(加载全局描述符表)指令少了几个字节,CPU 就会瞬间跑飞,导致系统强制冷启动。


第四章:保护模式的“惊险一跃”与head.s

setup.s完成了系统参数的读取(如显示模式、VGA 卡类型),并设置了全局描述符表(GDT)后,它要把最后的接力棒交给head.s

为什么把head.s叫做“先锋官”?因为它完成了最后一步极限操作:

  1. 它建立了中断描述符表(IDT),让 CPU 能听懂以后键盘、时钟发出的“呼喊”。
  2. 重新加载了GDT、LDT(局部描述符表),彻底割裂与实模式的联系。
  3. 开启了 386 的分页机制
  4. 最终,跳入init/main.c中的main()函数

到此为止,所有汇编的粗活、累活全干完了。操作系统终于从 16 位的泥沼中爬了出来,进入 32 位保护模式的 C 语言新纪元。


第五章:跨越时代的对比——现代电脑为什么不这么玩了?

虽然我们在深度剖析 Linux 0.11 的启动过程,但你可能心里会犯嘀咕:“现在的电脑一开机就是 Windows 或者现代 Linux,哪有这么复杂的手工搬运?”

是的,我们现在处于UEFI(统一可扩展固件接口)时代。

  • Linux 0.11 时代(1989-1995):没有文件系统概念。直接读软盘的第 2 个扇区,硬编码偏移量把setupsystem搬运出来。这是硬编码的、极其脆弱的方式。一旦磁盘格式有变,内核就启动不了。
  • 现代 Linux + UEFI/GRUB 时代:UEFI 本身就是一个微型的现代操作系统。它不再依赖0x7C00这个地址,而是直接在磁盘的 EFI 分区(ESP)找到/EFI/boot/bootx64.efi这个文件,直接作为可执行文件加载进内存。现代的引导程序(如 GRUB2)本身就跨越了实模式,直接在 32 位或 64 位保护/长模式下,读取 Linux 内核的 ELF 格式文件,并把它映射到内存高位。那些复杂的自我复制、0x10000 的临时中转,统统不需要了。

那我们为什么还要死磕 Linux 0.11 的启动?
因为这是硬核底层的教科书。它向学习者展示了在没有现代固件辅助下,计算机是如何从零开始、一步步协调 CPU 寄存器、内存条和磁盘控制器的。这不仅是知识的延伸,更是理解“计算机自治”本质的巅峰体验。


第六章:手搓一个微缩的“BIOS 引导加载器模拟”

为了让你亲手感受一下“0x7C00 复制到 0x90000”这种底层搬运是什么感觉,我写了一段精简的 C 语言模拟程序。它不会真正去动内存,但会模拟这些物理内存地址的转移和数据的交接。

📁bootloader_sim.c

/** * @file bootloader_sim.c * @brief 模拟 Linux 0.11 引导程序 bootsect.s 的内存搬运与接力。 * * 本程序模拟了: * 1. 物理内存区块 (0x7C00, 0x90000, 0x10000)。 * 2. bootsect 的自复制动作。 * 3. 读取 setup 和 system 到指定地址。 * 4. 执行完毕,移交控制权至 main()。 * * @version 1.0 */#include<stdio.h>#include<string.h>#include<unistd.h>// 定义模拟的物理内存空间大小#defineMEMORY_SIZE(1*1024*1024)// 模拟 1MB 内存charphysical_memory[MEMORY_SIZE];#defineADDR_BIOS_7C000x7C00#defineADDR_BOOTSECT0x90000#defineADDR_SETUP0x90200#defineADDR_SYSTEM0x10000#defineADDR_MAIN_START0x00000// 模拟函数指针typedefvoid(*boot_func)(void);/** * @brief 模拟 bootsect.s 完成后的最终跳转 */voidjump_to_main(void){printf("\n[内核] 汇编引导已完成。\n");printf("[内核] IDT、GDT、LDT 已设置。\n");printf("[内核] 进入 32 位保护模式!\n");printf("[内核] 即将跳转到 `init/main.c` 中的 `main()` 函数...\n\n");}/** * @brief 模拟硬件 BIOS 的启动引导 */voidsimulate_bios_boot(void){printf("=== 模拟 BIOS 加载开始 ===\n");printf("1. CPU 从 0xFFFF0 唤醒 BIOS 程序。\n");printf("2. BIOS 执行 POST 硬件自检。\n");printf("3. BIOS 找到磁盘,读取第 1 个扇区 (bootsect.s, 512字节)。\n");printf("4. 将 bootsect.s 加载到物理地址 0x7C00。\n");printf("5. 跳转到 0x7C00 开始执行...\n\n");}/** * @brief 模拟 bootsect.s 的第一阶段行为 */voidsimulate_bootsect(void){printf("=== 模拟 bootsect.s 执行 ===\n");printf("[bootsect] 1. 检测到当前我在 0x7C00,我需要马上搬家!\n");printf("[bootsect] 2. 将自己从 0x7C00 复制到安全地址 0x90000。\n");// 模拟移动// memcpy(physical_memory + ADDR_BOOTSECT, physical_memory + ADDR_BIOS_7C00, 512);printf("[bootsect] 3. 成功搬家到 0x90000!继续在 0x90000 处执行后续代码。\n");printf("[bootsect] 4. 从磁盘读取 setup.s (2KB) 到地址 0x90200。\n");printf("[bootsect] 5. 从磁盘读取 system 模块到地址 0x10000。\n");printf("[bootsect] 6. 准备工作做完,向 setup.s 移交控制权!\n\n");}/** * @brief 模拟 setup.s 的第二阶段行为 */voidsimulate_setup(void){printf("=== 模拟 setup.s 执行 ===\n");printf("[setup] 1. 检测硬件参数,读取显卡、内存等信息。\n");printf("[setup] 2. 关闭中断,启动 A20 地址线。\n");printf("[setup] 3. 准备进入保护模式:将 system 模块从 0x10000 移动至 0x00000。\n");printf("[setup] 4. 跳转到 system 模块的头部,即 head.s 开始执行!\n\n");}/** * @brief 模拟 head.s 的第三阶段行为 */voidsimulate_head(void){printf("=== 模拟 head.s (system 模块开头) 执行 ===\n");printf("[head] 1. 重新设置 IDT (中断描述符表) 和 GDT (全局描述符表)。\n");printf("[head] 2. 建立页目录和页表,开启 386 分页机制。\n");printf("[head] 3. 一切的汇编级准备工作全部结束!\n");}intmain(void){printf("========== Linux 0.11 启动引导模拟 ==========\n\n");// 1. BIOS 阶段simulate_bios_boot();// 2. Bootsect 阶段simulate_bootsect();// 3. Setup 阶段simulate_setup();// 4. Head 阶段simulate_head();// 5. 最终汇聚jump_to_main();printf("========== 模拟结束 ==========\n");return0;}

📁Makefile

CC = gcc CFLAGS = -Wall -g -O2 TARGET = bootloader_sim all: $(TARGET) $(TARGET): bootloader_sim.c $(CC) $(CFLAGS) -o $(TARGET) bootloader_sim.c clean: rm -f $(TARGET) run: $(TARGET) ./$(TARGET) .PHONY: all clean run

🚀 操作与解读

  1. 编译运行make run
  2. 观察输出:你会发现,控制台文字精确还原了书本上bootsect.s的“自复制”、“搬运setup”、“搬运system”的过程。每一行的文字输出,对应的是书页底部长达数百行汇编代码所实现的功能。

终章:一段无法犯错的历史

这一章是整个 Linux 0.11 源码的**“登陆跳板”。如果这几百行汇编代码出错了,后面的内存管理、进程调度、文件系统,连编译都不会有问题,但在真实的物理机上,你的电脑会直接卡死在黑屏状态**。

这也是为什么后来的 UEFI 架构要彻底摒弃 0x7C00 引导模式的原因之一——这种实模式下的手工内存搬运,容错率太低了,且没有回滚机制。

然而,正是这种“如履薄冰”的手工操作,恰恰是计算机工程师最迷人的地方。理解了bootsect.s -> setup.s -> head.s的三级跳跃,你就拥有了从计算机最底层硬件的角度来看待操作系统的能力。这是一种“向下穿透”的直觉。

接下来的篇章中,我们将走进3.2.1 bootsect.s 程序的具体代码拆解,去亲自看看那行rep movsw是如何进行“乾坤大挪移”的!我们下一章(linux内核引导启动程序002)再会。