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

发布时间:2026/7/2 20:54:55
linux内核引导启动程序001:唤醒沉睡的猛兽——内核启动的“接力赛” linux内核引导启动程序001唤醒沉睡的猛兽——内核启动的“接力赛”欢迎来到全书中最为硬核、也最命悬一线的全新篇章——第 3 章内核引导启动程序。在之前的旅程中我们仿佛站在上帝视角俯瞰了整座“内核城”的城建蓝图。我们了解了进程是怎么调度的、内存是怎么映射的、甚至知道了这块基石是拿什么工具Makefile铸造出来的。但现在我们要做的是把这座蓝图上的城市在现实中真正“砌”起来。我们需要把一台刚刚通电、处于蒙昧状态的 PC 电脑一步步引导它进入 32 位保护模式最终跳入 C 语言的殿堂——main()函数。这整个过程就像一场极其精密的“接力赛”任何一棒交接失误电脑都会直接黑屏死机。下面请你坐稳我们开始拆解这枚 386 时代的“硬核导弹”是如何发射的。第一章起跑线上的三个“火炬手”表 3-1 分析翻开你拍的第一张照片书上直接甩出了一张表 3-1。这里面藏着 Linux 0.11 启动阶段最重要的三个文件bootsect.s(5052 字节)吹响第一声冲锋号的“引路人”。它只有 5KB 左右却是系统能运转的第一步。setup.s(5938 字节)负责接管机器、收集信息的“硬件侦察兵”。head.s(5938 字节)真正进入 32 位世界的“保护模式先锋官”。书中特别给了一个重要的警示这三个文件虽然都是汇编但用的“方言”完全不同bootsect.s和setup.s使用的是as86编译器和ld86链接器。它们运行在 16 位“实模式”下代码风格类似于 Intel 的官方手册。head.s使用的是GNU as编译器和gld链接器。它运行在 32 位“保护模式”下代码风格是 Linux 内核标准的ATT 汇编格式。这就好比你和你的队友前两位讲的是“8086老方言”第三位讲的是“386新普通话”。跨越两种汇编格式正是早期内核工程师需要跨越的第一道天堑。第二章电脑刚通电的那一刻3.1 总体功能描述第一章我们弄清楚了“源文件是谁”现在我们要看看“它们是怎么动起来的”。书中文字描述“当 PC 的电源打开后80x86 结构的 CPU 将自动进入实模式并从地址0xFFFF0开始自动执行程序代码。”这第一个地址 0xFFFF0 就是 CPU 固化的“永久闹钟”。每次电脑开机CPU 就跑到这个地址上去找东西运行而这里正好存放着BIOS基本输入输出系统。2.1 BIOS 的“裁缝”工作BIOS 运行起来后会先做一道非常传统的“炒冷饭”操作硬件自检POST检查内存、键盘、显卡等硬件是否正常。寻找启动盘去读取软盘或硬盘的0 磁道0 磁头第 1 个扇区。种子植入把这512 字节的代码原封不动地复制到内存的绝对物理地址0x7C00处。交出指挥棒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 地址线、加载 GDTCPU 跳入 32位保护模式执行 head.s阶段2_bootsect [第二阶段引导程序自复制与加载]bootsect.s 将自己从 0x7C00 复制到 0x90000腾出 0x7C00 之前的空间从磁盘读取 setup.s 到内存 0x90200从磁盘读取 system 模块到内存 0x10000阶段1_BIOS [第一阶段BIOS 加载]BIOS 从磁盘读取 boot sector 512字节放入物理内存 0x7C00CPU 跳入 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叫做“先锋官”因为它完成了最后一步极限操作它建立了中断描述符表IDT让 CPU 能听懂以后键盘、时钟发出的“呼喊”。重新加载了GDT、LDT局部描述符表彻底割裂与实模式的联系。开启了 386 的分页机制。最终跳入init/main.c中的main()函数。到此为止所有汇编的粗活、累活全干完了。操作系统终于从 16 位的泥沼中爬了出来进入 32 位保护模式的 C 语言新纪元。第五章跨越时代的对比——现代电脑为什么不这么玩了虽然我们在深度剖析 Linux 0.11 的启动过程但你可能心里会犯嘀咕“现在的电脑一开机就是 Windows 或者现代 Linux哪有这么复杂的手工搬运”是的我们现在处于UEFI统一可扩展固件接口时代。Linux 0.11 时代1989-1995没有文件系统概念。直接读软盘的第 2 个扇区硬编码偏移量把setup和system搬运出来。这是硬编码的、极其脆弱的方式。一旦磁盘格式有变内核就启动不了。现代 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 */#includestdio.h#includestring.h#includeunistd.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;}MakefileCC 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 操作与解读编译运行make run。观察输出你会发现控制台文字精确还原了书本上bootsect.s的“自复制”、“搬运setup”、“搬运system”的过程。每一行的文字输出对应的是书页底部长达数百行汇编代码所实现的功能。终章一段无法犯错的历史这一章是整个 Linux 0.11 源码的**“登陆跳板”。如果这几百行汇编代码出错了后面的内存管理、进程调度、文件系统连编译都不会有问题但在真实的物理机上你的电脑会直接卡死在黑屏状态**。这也是为什么后来的 UEFI 架构要彻底摒弃 0x7C00 引导模式的原因之一——这种实模式下的手工内存搬运容错率太低了且没有回滚机制。然而正是这种“如履薄冰”的手工操作恰恰是计算机工程师最迷人的地方。理解了bootsect.s - setup.s - head.s的三级跳跃你就拥有了从计算机最底层硬件的角度来看待操作系统的能力。这是一种“向下穿透”的直觉。接下来的篇章中我们将走进3.2.1 bootsect.s 程序的具体代码拆解去亲自看看那行rep movsw是如何进行“乾坤大挪移”的我们下一章linux内核引导启动程序002再会。