
1. 引导链路与 stage1 的定位1.1 从 BIOS 到 MBR为什么必须从这 512 字节开始凡是做过引导扇区调试的人都知道操作系统启动不是一个“开机即进操作系统”的过程而是一层一层交接棒的过程。机器上电以后CPU 实模式执行的第一段代码在 BIOS ROM 里BIOS 完成自检和基本设备初始化后会从启动设备读取第一个扇区到0x7c00然后跳过去执行。这个扇区就是 MBR它通常只有 512 字节最后两个字节必须是55 AA否则 BIOS 不认。GRUB 的第一阶段 stage1本质上就是这 512 字节里的可执行代码。很多人对 GRUB 的理解停留在“开机看到那个选操作系统的界面”却不知道这个界面实际是后面的 stage2 做的。而 stage1 的目标非常单纯把自己从 BIOS 手里接过来再把磁盘上真正干活的 stage2 读进内存最后跳过去。stage1.S 正是这一阶段所有汇编源码的集合它短小、精巧几乎每一行都踩在实模式编程和磁盘 I/O 的老规矩上。这篇是 GRUB 源码分析系列的第一篇我会把 stage1.S 从入口到交接逐段拆开讲。适合正在学操作系统引导、想搞懂 MBR 结构、或者准备自己写一个引导程序的人。读完之后你再去看 GRUB 的 stage2 或者内核的实模式代码都会觉得顺很多。1.2 stage1 真正负责的事别被“GRUB 很庞大”吓到stage1.S 编译出来的二进制通常不到 512 字节它要做的事少得让人怀疑保存 BIOS 传入的启动盘号。BIOS 在跳进 MBR 之前会把当前启动盘编号放在DL寄存器里后续所有磁盘读取都要靠它。建立干净的实模式运行环境。把段寄存器归零、设置栈指针、关开中断确保后面调用 BIOS 中断时不会因为栈或段环境混乱而出错。把自身重定位到另一个地址。因为后续要把 stage2 读入0x7c00附近的区域如果不把自己挪走读盘过程就可能把正在执行的代码覆盖掉。读取 stage2 的起始扇区到固定内存位置。这里用到的扇区位置不是扫描文件系统得来的而是在安装 GRUB 时写到 stage1 二进制里的固定偏移。校验并跳转到 stage2 入口。跳转之前基本不做复杂参数传递能留的信息都在寄存器里。可以想成是BIOS 把话筒交给 stage1stage1 喊了一声“下面有请 stage2”然后就把舞台让出去了。整个过程没有内存管理、没有保护模式、没有文件系统全是十六位汇编和 BIOS 中断。1.3 为什么选 GRUB Legacy 的 stage1.S 来分析现在大家用的 GRUB2 对应引导代码是boot.S代码写法更绕还引入了 LBA 支持、引导注入等一堆逻辑。而 GRUB Legacy 0.97 的stage1.S是经典的 MBR 引导代码结构非常“教科书”每一段都能对应到实模式启动的理论知识点非常适合作为入门分析对象。从代码规模来说GRUB Legacy 的 stage1.S 去掉注释和宏定义核心逻辑只有上百行。反汇编之后你能在地图上标出每一行指令的作用。而 GRUB2 的boot.S要考虑 core.img、嵌入扇区数量、UUID 传递等问题一上来就读它很容易懵。我建议的学习路径是先用 GRUB Legacy 把“512 字节内完成一次引导”这件事吃透再去看 GRUB2 的 boot.S 和 diskboot.S那时候你会发现 GRUB2 只是把同样的事情做得更灵活、更健壮。后面如果这个系列继续往下写我会接着讲 stage2 或者 GRUB2 的 core.img。2. 读代码前必须建立的基础2.1 实模式、段寄存器与 0x7c00stage1.S 里的所有代码都运行在 16 位实模式下。实模式下 CPU 访问内存靠“段寄存器左移 4 位加偏移”比如DS 0x0000、偏移是0x7c00那访问的物理地址就是0x7c00如果CS 0x07c0、偏移是0x0000访问的也是0x7c00。这给调试带来一个很隐蔽的坑同一个物理地址可以用不同的“段:偏移”组合表示反汇编和源码注释里写的地址经常看起来对不上。0x7c00这个地址不是随便定的它是早期 IBM PC 设计留给引导扇区的约定位置。BIOS 从设备读到 512 字节后直接把这个扇区搬到0x7c00再跳转。因此 stage1.S 的链接地址一定是基于0x7c00的如果你自己写引导程序却链接到别的地址跳转指令会全部失效。理解了这一点再去看下面这类代码就不会晕movw $0x7c00, %sp这行是把栈顶设在 MBR 自身代码的下方栈向低地址增长所以不会压到引导代码。实模式下栈是很危险的资源任何一个函数调用把返回地址压栈都可能改写内存stage1 里大部分函数不深因此栈需求量不大。另外在读完第一个扇区后stage1 自己也可以继续向内存高地址写数据所以它必须对内存布局有精确的预期。低端内存0x00000 ~ 0x9ffff全部可用但0x7c00附近这一段是 MBR0x8000或0x8200附近经常被用来放 stage2具体放在哪要看源码里的段定义。2.2 高频实模式指令一览stage1.S 里的指令不多但如果指令集不熟看汇编源码会被各种缩写劝退。我整理了几个高频出现的指令理解了它们再读源码会顺畅很多。指令作用在 stage1 里的典型用途ljmp远跳转同时修改 CS 和 IP确定 CS 的值让后续代码运行在期望的段上cli/sti关闭 / 开启中断初始化时关中断防止段寄存器未设好时被中断打断cld清方向标志保证stos/movs等字符串操作方向固定为向上增长xorw %ax, %ax实现寄存器清零快速把段寄存器置 0movw16 位移动设置段寄存器、栈指针、参数int调用 BIOS 中断读磁盘、显示字符等硬件操作全靠它jc进位标志为 1 时跳转检查磁盘读取是否失败loopCX 减一不为 0 则跳转循环读取多个扇区举个例子cli放在初始化段寄存器的最前面几乎所有引导代码都会这么做。原因很简单如果在DS还是乱的值的时候发生中断中断服务程序会用不正确的段访问内存系统可能直接卡死。但因为后面还要用 BIOS 中断来读盘而很多 BIOS 实现依赖中断机制所以初始化完成之后必须sti把中断重新打开。字符串操作指令lodsb和stosb也是常见面孔它们一次性完成“从DS:SI读一个字节”或“向ES:DI写一个字节”并自动增减指针。比如显示提示字符串、拷贝缓冲区都会用到它们。cld就是给这些指令定方向保证SI、DI是增加的。2.3 INT 13h 与磁盘读写的硬件约定stage1 最重要的活是读磁盘而读磁盘在实模式下绕不开 INT 13h。不熟悉 BIOS 磁盘服务的人看到AH 0x02这类赋值会一头雾水这里先讲清楚参数约定后面看代码就不需要频繁翻文档了。传统的 CHS 读扇区功能是 INT 13h 的AH 0x02参数如下寄存器作用说明AH功能号0x02 表示读扇区AL读取扇区数一次最多能读多少受 BIOS 限制CH柱面号低 8 位10 位柱面号中的低 8 位CL扇区号 柱面高 2 位bit0~5 是扇区号bit6~7 是柱面号高 2 位DH磁头号具体取决于磁盘几何参数DL驱动器号0x80 表示第一块硬盘ES:BX缓冲区地址读到的数据写到这个位置调用后如果进位标志 CF 0表示成功CF 1 表示失败AH 里是错误码。设计引导代码的人最怕的就是磁盘 I/O 失败所以jc跳转到错误处理几乎是固定动作。CHS 模式受限于“柱面/磁头/扇区”三个维度BIOS 里通常能访问的磁盘大小有限。后来磁盘大了就有了 LBA 和扩展 INT 13hAH 0x42用 64 位扇区号寻址。stage1 里往往两种都要支持先探测扩展 INT 13h 是否可用不可用再退回 CHS这也就是为什么源码里会有“探测 LBA”“走 CHS”之类的逻辑分支。3. stage1.S 核心路径逐段拆解3.1 入口关闭中断、建立干净的段环境MBR 被 BIOS 跳进来时CPU 的CS:IP指向0x0000:0x7c00但DS、ES、SS的值却不保证是零。不同 BIOS 环境下这些寄存器可能完全不同。为了避免后续访问数据和压栈时用到错误的段stage1.S 的第一步一定是把段环境重置。源码里典型的结构是这样.code16 .text .globl _start _start: .zero 8 /* 预留 8 字节让文件头保持固定偏移 */ ljmp $0, $real_prefix /* 远跳转强制 CS 归零 */ real_prefix: cli xorw %ax, %ax movw %ax, %ds movw %ax, %es movw %ax, %ss movw $0x7c00, %sp stiljmp是最容易忽略的重点。它不只是跳转还会同时把CS设置为立即数里的段值。因为我们接下来要求DS、ES、SS都等于 0如果CS不知道是多少后面某些基于CS的寻址尤其是符号引用就会算错。movw $0x7c00, %sp是在SS 0的前提下指定栈顶地址。为什么不把栈顶放在代码下方更空的地方因为这里需要保证后续读盘时设置的各种缓冲区不会与栈冲突。把栈放在0x7c00附近向低地址增长占用的是 MBR 代码下方的低端内存相对安全。注意这里的sti不能省。很多 BIOS 的 INT 13h 实现依赖中断如果你在关中断状态下直接调用读盘可能出现读不出来或者返回异常。先关中断是为了安全初始化初始化完立刻开中断之后所有 BIOS 调用都在正常中断环境下进行。3.2 自我重定位为什么代码要“搬个家”你已经发现stage1 后面要把 stage2 读到内存里而且读入地址很可能就在0x7c00的附近。如果在原地执行那么读盘数据一旦写入正在运行的代码区域后续指令就会被覆盖程序直接飞掉。所以“把自己搬走”是引导代码的标准操作专业说法叫重定位。在 stage1.S 里这段逻辑一般会把从0x7c00开始的自身代码复制到0x7000或更高一点的地址然后长跳转过去继续执行。接下来再读 stage2 时使用的缓冲区就能放心放在0x7c00区域。这个设计有点像什么呢就像你要拆旧房子但拆房设备必须开进屋里操作你不可能让设备一边拆一边把自己脚下的地也拆了所以得先把设备挪到旁边的空地上再动手。复制自身通常用movsw一类字符串指令完成循环次数由复制长度决定。为了保证目标地址兼容汇编源码里会有对应的宏定义比如STAGE1_ADDRESS、STAGE1_SEGMENT安装工具和编译脚本都会严格遵守这些常量。你在调试时如果把断点设在0x7000会发现逻辑上还是同一份代码这就证明重定位已经生效。3.3 保存启动盘号与首次磁盘探测读盘离不开“从哪个盘读”。BIOS 从硬盘启动时会把DL设置成 0x80从 U 盘或软盘启动时会有别的值。这个寄存器值如果不保存后面调用 INT 10h、INT 13h 都可能把它改变届时就不知道自己在读哪个设备了。所以源码里会有一个堪称“全局变量”的内存位置通常叫boot_drive一条movb %dl, boot_drive就解决了。以后每次准备调用 INT 13h 前都从boot_drive把值装回DL。保存之后stage1 会试着探测磁盘是否支持扩展读。探测方式是通过 INT 13h 的AH 0x41同时把BX设置为 0x55AADL设为盘号。调用完成后看进位标志如果成功说明支持 LBA。这个探测的意义在于CHS 模式要求调用方自己组装“柱面/磁头/扇区”参数而 stage2 安装后扇区位置在磁盘上是确定的用 LBA 直接算扇区号更省心。但如果用户的 BIOS 太老只能用 CHS那 stage1 必须能退回传统路径。所以这段逻辑就是引导代码里的一个“能力检测分支”。3.4 读取 stage2 扇区到内存探测完磁盘能力正式干活。stage1 需要从磁盘上把 stage2 的代码块搬到内存。这里的关键在于它怎么知道 stage2 在哪答案你可能已经猜到了不是在运行时扫描文件系统而是安装时把 stage2 的物理扇区位置写死在 stage1 的二进制数据里。GRUB 安装程序会分析文件系统找到 stage2 文件占用的扇区把起始扇区号、长度等信息填到 stage1 编译时预留的偏移处。读取过程大致是read_stage2: movb $0x02, %ah /* 功能读扇区 */ movb $0x01, %al /* 扇区数先读 1 个 */ movw $STAGE2_SEGMENT, %bx /* 设置 ES:BX 到目标缓冲区 */ int $0x13 jc read_error先读一个扇区通常是为了拿到 stage2 的头部信息比如 magic 值、剩余扇区数、入口偏移。确认是合法的 GRUB stage2 之后再循环读取后续扇区。这也解释了为什么 stage1 源码里会看到“读第一个扇区”和“循环读取剩余扇区”两段逻辑而不是一次性把数据全读完。读入地址也不是随便选的。STAGE2_SEGMENT往往定义为0x8000或者通过段寄存器指定要求ES:BX组合后的物理地址不与当前重定位后的 stage1 代码冲突。源码注释里会写明这些段的固定值因为它们背后是 stage2 入口代码对内存布局的依赖。3.5 跳转 stage2 前的交接协议stage1 的最后一个动作是跳到 stage2 的入口。但这不是普通函数调用不是“call一下然后返回”的思路。两个阶段属于不同程序没有返回关系。跳转前要保证环境处于一个 stage2 可以继续干活的状态。通常交接时要做几件事确保DL里仍然是当前启动盘号方便 stage2 后续继续访问磁盘。确保DS、ES段寄存器设置正确因为 stage2 的代码可能一开始就要访问内存数据。关闭中断如果后续要加载新 IDT 或进入保护模式甚至把栈切到 stage2 自己的栈。这段交接的本质是“定义一组寄存器约定”。和函数调用约定相比它更粗糙更像两套系统的对接协议。如果你自己写引导器一定要在文档里写明“跳到下一阶段时哪些寄存器是什么含义”否则后续开发者只能靠反汇编去猜。4. 安装期补丁stage1.S 里的数据占位4.1 为什么 stage1 能绕过文件系统stage1 里没有文件系统解析代码这是刻意的。512 字节的空间装下 FAT、EXT、NTFS 里任何一个文件系统的解析逻辑都不可能更别说要兼容多种文件系统。所以 GRUB 的安装策略绕了个弯让安装工具把“目标 stage2 文件在磁盘上的物理扇区位置”算出来直接当作二进制数据塞进 stage1。这样一来stage1 运行时不用关心文件系统它只认“扇区号”。这个设计也让“安装”这个动作变得很敏感。文件系统一旦有变化比如磁盘碎片整理后移动了文件或者分区结构调整导致文件物理位置变化原来写进 stage1 的扇区号就可能失效。这也是为什么 GRUB 安装之后不要随意做磁盘碎片整理的原因。4.2 关键偏移量与补丁时机stage1.S 源码里会有若干个固定偏移量的宏定义我见过的大致包括宏 / 偏移作用安装时的填充方式启动盘号保存位存当前启动设备运行时由movb %dl写入stage2 起始扇区记录 stage2 第一个扇区安装工具扫描文件系统后写入stage2 扇区数记录需要读多少扇区安装工具根据 stage2 文件大小计算stage2 加载段落地址指定加载目标段编译时由链接脚本固定安装工具做的事可以理解成“打开 stage1 的 ELF 或二进制文件按偏移改写几个数”。这就是为什么网上有人手工改 MBR 里的 GRUB 后系统起不来因为那些偏移里的数据如果和他自己的磁盘布局不一致等于把引导路径指到了错误位置。用hexdump去看安装好 GRUB 的 MBR你会发现前 446 字节里藏着一些看起来不像纯代码的数值那就是安装工具打进去的数据。这也是调试时判断“GRUB 有没有被正确安装”的一个切入点。4.3 BPB 与 MBR 位置兼容MBR 最后的 64 字节是分区表最后 2 字节是55 AA那么 stage1 的代码空间其实只有前 446 字节左右。但实际安装 GRUB 时还需要考虑目标磁盘是不是带 FAT 引导扇区格式的 BPB 区域。如果分区格式要求在引导代码区里有 BPB直接覆盖可能导致操作系统不认这个分区。GRUB 的做法是尽量跳过 BPB把自己真正执行的入口放在 BPB 之后代码内会主动计算偏移跳过去。如果你去翻 stage1.S会看到类似after_BPB的标签。这再次提醒我们一个看似“只是汇编代码”的引导扇区实际上要避让很多外部结构。调试这类代码时最怕的就是“代码逻辑看着没问题但和分区格式的 BPB 互相踩了”。手动修改 MBR 之前先备份原扇区已经是我的默认操作。5. 调试引导扇区的实战办法5.1 用 QEMU 和 GDB 观察 0x7c00 的执行纸上谈兵再多不如实际单步跑一遍。我调试 stage1 的首选工具是 QEMU 加 GDB因为 QEMU 可以暂停 CPUGDB 可以连接远程调试端口。比如先做一个磁盘镜像并安装 GRUB或者直接用现成的 GRUB 安装盘镜像然后qemu-system-x86_64 -hda grub.img -S -s-S让虚拟机在启动时暂停-s开一个调试端口默认是 1234。另开一个终端进 GDBset architecture i8086 target remote :1234 b *0x7c00 continue打到0x7c00之后就是 MBR 入口。这时候可以单步执行观察CS:IP的变化、DL的值、每次 INT 13h 前后寄存器的情况。配合反汇编工具把磁盘镜像的前 512 字节导出来dd ifgrub.img ofmbr.bin bs512 count1 objdump -D -b binary -m i386 -M i8086 mbr.bin反汇编出来的结果大部分能与 stage1.S 的源码对应上。这一步对理解源码特别有价值因为你能看到流水线上每一个跳转、每一次读取的真实地址。5.2 常见启动故障速查调试引导代码时遇到的现象通常就那么几类我整理成一张速查表遇到问题可以先对号入座。现象可能原因排查方向黑屏光标闪烁stage1 没有找到或没能完整读取 stage2检查安装时写入的扇区号是否正确确认镜像是否完整屏幕出现 GRUB 之后卡住stage1 已执行但读盘失败用 QEMU 单步停在 INT 13h 前后检查 CHS/LBA 参数直接回 BIOS 启动菜单MBR 末尾不是55 AA检查镜像尾部确认是否被其他程序改写改完 MBR 后系统不认盘BPB 被覆盖恢复原 MBR不要绕过分区表直接写前面所有字节重启后引导器丢失分区工具或系统重装覆盖了 MBR备份 MBR了解重装动作是否会覆盖引导代码“光标闪烁”这个现象在真实机器上非常常见往往是因为 stage2 文件被移动过而 stage1 里的扇区号还指向旧位置。解决办法就是重新执行一次 GRUB 安装让安装工具重新计算并补丁 stage1 的数据区。5.3 调试引导代码的几条经验调试引导代码不像调试普通用户程序不能随手用printf也不能依赖操作系统 API。我的经验集中在几个点上第一尽量用模拟器而不是真实机器。真实机器坏了只能拔盘模拟器可以随时打快照、回滚还能在任意指令处打断点。QEMU 的-no-reboot参数也很有用可以让重启变成退出避免陷入无限启动循环。第二先验证环境再怀疑代码。很多时候问题不是 stage1 逻辑错了而是 GDB 反汇编出来的地址和实际执行地址不一致或者是镜像前 512 字节根本没被加载到0x7c00。先用x/16xb 0x7c00看一眼内存里的字节确认确实是 MBR 再继续。第三磁盘 I/O 的错误码要会查。INT 13h 调用失败后AH 里的错误码很有价值比如0x01是参数错误0x04是扇区未找到。单步停在jc分支的地方直接看 AH能省下大量猜时间。最后再说一点我个人刚开始啃 stage1.S 的时候注意力全在指令上总觉得每一条汇编都得背下来后来才发现真正难的是那些没有写进代码里的信息BIOS 为什么把控制权交到 0x7c00磁盘参数是怎么在 CHS 和 LBA 之间切换的安装工具在 MBR 的哪些偏移里埋了数据。等你把这些问题都想明白stage1.S 看起来就不再是“一堆奇怪的汇编”而是一条有头有尾的引导流水线。遇到引导类问题先用 QEMU 加载镜像单步到 0x7c00看一眼内存和寄存器比翻论坛求助要快得多。