正常
调试档案 002 · 实模式到保护模式切换的几个命门
从
document/notes/002/1.md、document/notes/002/2.md提炼,配套 002 · 进入保护模式。实模式 → 保护模式这一跳,要在几条指令内同时切换寻址模型、译码宽度、CS 来源,几乎每个环节都能独立制造一次三重故障。下面是三个最常要命的。
案例一:lgdt 之后直接崩
- 症状:
lgdt gdt_ptr这条执行完(或执行中),机器三重故障重启,根本走不到置 PE 那步。 - 原因:
lgdt在实模式下按DS << 4 + 偏移计算操作数地址。如果DS还是 BIOS/Stage2 留下的脏值,DS<<4会在gdt_ptr的链接偏移上再叠一段,lgdt从错误内存读 GDTR,base/limit 全是垃圾,后续一访问段就 #GP → 三重故障。 - 定位:GDB 在
lgdt前后查看 GDTR(info registers或相应的 monitor 命令),limit 应为 23(3 项 × 8 − 1),base 应落在0x81xx附近(stage2 链接在0x8000);base 一眼不对就是DS问题。 - 修复:
lgdt前先movw $0,%ax; movw %ax,%ds把DS清零,让实模式寻址退化成偏移 = 绝对地址。Stage2 同时要确保链接在. = 0x8000(=载入地址),这样gdt_ptr的偏移就是它真实的线性地址。 - 防复发:
lgdt前置DS=0写进 PM 切换的固定开场;链接脚本里 Stage2 的.必须等于载入地址,改其一必须确认另一个。记牢:lgdt只搬运 6 个字节、不校验 GDT 内容,错会延迟到用选择子时才爆。
案例二:置了 CR0.PE 就原地重启
- 症状:
movl %eax,%cr0把 PE 位置 1 之后,程序没走几条就三重故障重启,根本到不了pm_entry。 - 原因:置
CR0.PE那一刻,CPU "名义上"进了保护模式,但CS还是实模式遗留的、译码还是 16 位。必须有一条远跳(或远调用)带着新选择子强制重载CS,保护模式才算"生效"。漏了这条,后面.code32编码的 32 位指令被当 16 位解码,译码错位 → 非法指令 → 三重故障。 - 定位:在置 PE 的
movl %eax,%cr0之后、下一条设断点单步,看rip/eip走向——如果没走几步就乱跳或复位,基本就是没刷新CS。也可以看 QEMU-d int的中断日志,会看到 #UD/#GP。 - 修复:置 PE 后紧接着(中间别插别的要紧指令)
ljmp $0x08, $pm_entry。远跳用 GAS 在.code16下自动生成的 16 位编码,不要手拼ea <off16> <seg16>机器码——手拼最容易拼错。 - 防复发:把"置 PE"和"远跳"视为一个不可分割的动作对。任何 PM 切换代码,看到
CR0.PE=1却没有紧随的远跳,先当作 bug。
案例三:GDB 报 Invalid register / 反汇编一堆 (bad)
- 症状:GDB 报
Invalid register ip,或者反汇编窗口里出现一串(bad) + rex之类的乱码;寄存器值(比如SP显示成0x000a)明显不合理。 - 原因:译码宽度和 GDB 视角对不上。两种常见来源:(1)
.code16/.code32放错了位置——它们是汇编器指令不是 CPU 指令,只决定机器码编成 16 位还是 32 位;真正决定 CPU 译码的是CS指向段的 D 位。把lgdt之类错放到.code32后面,CPU 实际还在 16 位译码却拿到 32 位编码,错位崩溃。(2) 给 GDB 喂的是stage2.bin(裸二进制,无符号、无段信息)而不是stage2(ELF)。 - 定位:检查
.code16/.code32是否分布在远跳两侧(前 16、后 32);info registers看寄存器名,实模式是ip/sp,PM 是eip/esp——跨过远跳这个名字会变,既是症状也是判断"是否真进 PM"的旁证。 - 修复:对齐
.code16/.code32位置;GDB 一律file build/boot/stage2用 ELF,bin只给启动加载用,两者不可互相替代。 - 防复发:
.code16/.code32的切换点永远锁在远跳那条指令上;调试 bootloader 永远先file <ELF>。
一句话总结
PM 切换的三个命门是寻址基址(DS=0)、CS 刷新(远跳)、译码宽度(.code16/.code32 + 段的 D 位)——三者在置 CR0.PE 前后必须一致地翻过来,任何一处停在旧状态,表现都是三重故障。把这三条和"全程 cli(无 IDT)"绑成一套固定动作,这一跳就稳了。