正常
调试档案 001 · 实模式引导的几个老坑
这份档案从
document/notes/001/notes_mbr.md的踩坑记录提炼而来,配套 001 · 实模式引导。实模式引导几乎每个坑都和"段式寻址"或"512 字节红线"有关,下面挑出最典型、最容易复发的三个,整理成可查的案例。
案例一:打印单字符正常,字符串却是一片乱码
- 症状:
INT 0x10 AH=0x0E直接给al打单字符,屏幕能出来;一旦movw $(msg), %si去打字符串,出来的全是乱码,或者一个字都不动。 - 原因:实模式下
物理地址 = 段 << 4 + 偏移。字符串标号msg链接后得到的是一个偏移,访问它靠的是DS:SI。如果CS已经归零,但DS还是 BIOS 留下的值,DS:SI算出来的物理地址根本不指向字符串,lodsb读到的是垃圾。 - 定位:先用单字符(
AH=0x0E直接给al)验证输出通路通不通;单字符通、字符串不通,基本就锁定是段/指针问题。挂 GDB 看DS和SI的实际值,手算DS<<4+SI是否落在字符串所在内存。 - 修复:在
_start开头把段寄存器统一理顺——movw %cs, %ax; movw %ax, %ds/es/ss/fs/gs,让"标号偏移"和"访问段"对得上。MBR 里还得先ljmp $0, $real_start把CS钉成确定值,因为不同 BIOS 跳进 MBR 时给的CS不一样。 - 防复发:把"段归一化"做成 MBR/Stage2 入口的固定开场动作,谁都不许省。写任何"标号 + 指针"的代码前,先确认指针的段寄存器指向正确。
案例二:Stage2 跳进去能执行,一访问数据就炸
- 症状:MBR 远跳到 Stage2,
_start里前几条指令(设段、设栈)能跑;一旦访问 Stage2 自己的数据标号(字符串、常量),立刻死机或重启。 - 原因:链接地址和运行时段没配合好,造成"双重偏移"。典型错误是把 Stage2 链接在
0x8000(绝对地址),运行时又设DS=0x800,于是标号地址被算成0x8000 + 0x80xx,double 了一次。 - 定位:看链接脚本
.的值和运行时设给DS的值,手算一个标号的最终物理地址,看是不是落在了 Stage2 实际加载的位置。GDB 单步到访问数据那条指令,看DS:SI算出的地址。 - 修复:二选一的地址模型,别混用——
- 绝对模型:链接
. = 实际载入地址,运行时DS=0。 - 相对模型(Cinux 现行做法):链接
. = 0x0,运行时DS=CS=载入地址>>4。这样 Stage2 不管被读到哪,只要段寄存器跟着改就行,更灵活。
- 绝对模型:链接
- 防复发:链接脚本里的
.和入口设段的指令是一对契约,改其一必须同步另一个。Stage2 的. = 0x0和DS=0x800是配死的,别只动一边。
案例三:MBR 里 call 一个函数就重启
- 症状:某段打印/工具函数,放进 Stage2 里调用一切正常;只要让 MBR 去
call它,就死机或三重故障重启。 - 原因:BIOS 只加载第 0 扇区的 512 字节。如果把功能较多、带 VESA/A20 的
common/serial.S也链进 MBR,MBR 的.text轻易就超过 512 字节,超出的部分根本没被读进内存。call跳过去,执行的是一坨未初始化的随机内存。 - 定位:
objdump -d mbr.bin或直接看mbr.bin文件大小;也可以看.org 510处的0xAA55魔数有没有被代码挤没(魔数错位 = 代码溢出)。 - 修复:MBR 只链
mbr.S自身,功能搬进 Stage2。MBR 里需要一个打印时,写一个极简、不 push 寄存器的版本(print_string_mbr),专为 512 字节预算服务;功能完整的print_string(带保护)留给 Stage2 用。 - 防复发:
boot/CMakeLists.txt里把这条红线钉死——add_executable(mbr mbr.S)只有mbr.S,common/serial.S只进stage2。构建期就可以用mbr.bin大小 = 512 来兜底,超了就让它构建失败,别留到运行时炸。
一句话总结这三个坑的共同根源
实模式引导阶段,90% 的玄学故障来自两件事:段式寻址把"地址"拆成了"段 + 偏移"两半(任一半错位都会算错地址),以及 512 字节的 MBR 是 BIOS 定的硬天花板(超出的代码等于不存在)。把这两条刻进脑子里,后面 002 进保护模式前还要再受它们最后一次折磨。