调试现场:段、双重偏移、512 字节红线
实模式这块,Cinux 踩过一串非常典型的坑。下面挑最致命的几个,都是真的调出来的。
症状一——屏幕一个字都不打,或者打出一串乱码。 根因几乎都是段没理顺——CS 被归零了,但 DS 还是 BIOS 留下的值,DS:SI 算出来的物理地址根本不指向字符串。修复就是"实模式地址模型"那套 DS=ES=SS=CS。判断方法很朴素:先只打印单个字符(INT 0x10 AH=0x0E 直接给 al),如果单字符能出来、字符串出不来,基本就锁定是段/指针问题。
症状二——Stage2 跳进去能执行,一访问数据就炸。 这是经典的"双重偏移"——早期版本把 Stage2 链接在 0x8000,同时运行时又设 DS=0x800,于是标号地址变成了 0x8000 + 0x80xx,double 了一下。正确的模型二选一:要么链接 . = 0x7C00 之类绝对地址 + DS=0;要么像 Cinux 现在这样,链接 . = 0x0 + 运行时 DS=CS=0x800,靠段寄存器来承载"实际载入位置"。后者更灵活,Stage2 不管被读到哪,只要段寄存器跟着改就行。
症状三——打印一两次之后就莫名其妙飞掉。 翻 print_string 的实现——是不是忘了 push 保护寄存器?BIOS 中断会破坏 ax/bx/si,不保护的话调用者的指针就被污染了。修复就是 serial.S 里那几行 push/pop。
症状四——MBR 里 call 一个函数就死机重启,但同样代码挪到 Stage2 就没事。 这是最阴的一个——MBR 的 .text 超过了 512 字节,多出来的部分根本没被 BIOS 加载进内存。call 跳过去,执行的是一坨随机内存,当然炸。修复就是那条铁律:MBR 只链 mbr.S,极简;所有重活搬进 Stage2。判断方法:objdump 看 mbr.bin 的大小,或者看 .org 510 那个魔数是不是被代码挤没了。
还有一个特别隐蔽的:把栈放在 0x7B00(紧挨着 MBR 下方)。看着合理,但 BIOS 自己也要用栈、你的函数也要压栈,几层压下来就踩进了 MBR 代码区,改掉了正要执行的指令。Cinux 现在把 MBR 栈放在 0x7000、Stage2 栈基址放在 0x9000,都是特意避开"可能被踩"的区域。