
实模式内核开发里真正让人卡住的往往不是“代码写不出来”而是“系统镜像放进 QEMU 之后完全不按预期执行”。这时没有操作系统、没有日志、没有调试器附加唯一可靠的信息源是把镜像反汇编出来让机器码自己和源代码对照。对新手来说系统镜像反汇编是排错的关键手段也是理解实模式寻址、段寄存器、org指令和 BIOS 引导流程的必经环节。这篇文章从一个最小引导扇区内核开始完整说明如何用 hexdump、ndisasm、objdump、QEMU 和 GDB 对实模式系统镜像进行反汇编排错并给出几个常见启动失败案例和可复用的检查清单。1. 实模式内核为什么离不开反汇编排错1.1 实模式下的执行模型和普通程序调试差别很大实模式是 x86 CPU 加电后最早进入的工作模式也是大部分引导程序、引导扇区和入门级内核运行的起点。在实模式下CPU 使用 16 位段寄存器加 16 位偏移量组成 20 位物理地址地址计算公式是物理地址 段寄存器 4 偏移量比如0x07c0:0x0000对应的物理地址是0x07c0 4 0x0000 0x7c00。BIOS 在开机自检后会把引导扇区从磁盘加载到物理地址0x7c00然后把控制权交给这条地址上的机器码。这和普通用户态程序调试有很大区别。用户态程序由操作系统加载有可执行文件格式、调试符号、系统调用和异常处理程序崩溃时至少还有 core dump 或错误码。实模式内核没有这些设施。它是一段被直接放到内存里执行的裸机器码没有重定位表没有运行时保护没有标准输出。如果代码写错表现往往只是黑屏、重启、乱码或卡死没有任何文字告诉开发者错在哪里。所以在实模式内核开发里开发者必须回到最底层用反汇编工具直接检查镜像里的机器码。机器码是 CPU 真正看到和执行的内容源码只是人的理解。当源码理解和实际机器码不一致时问题就只能从机器码这一层查。1.2 系统镜像反汇编在整个排错链路中的定位所谓系统镜像反汇编指的是对一个原始二进制文件而不是普通 ELF/PE 文件进行反汇编。引导扇区、磁盘镜像和二级引导器大多以平面二进制格式存在没有文件头也没有符号表。不能用常规的objdump -d直接反汇编必须告诉工具这是裸二进制、按 16 位指令宽度解析、并指定一个合理的基址。反汇编在排错链路中的定位可以分成三个层次静态字节层先确认镜像大小、引导签名、关键偏移上的数据。静态指令层把机器码还原成汇编指令检查跳转目标、立即数地址、段寄存器操作。动态运行层在 QEMU 里单步执行对照寄存器实际值验证反汇编判断。如果跳过静态反汇编直接开 GDB 单步很容易出现“跑了几百条指令但不知道自己为什么到这里”的情况。如果跳过动态调试只靠反汇编又可能把数据区误认为代码区得出错误结论。两套方法需要配合而系统镜像反汇编是第一步也是后续所有判断的基础。1.3 反汇编可以把哪些问题变成可确认结论实模式引导排错过程中常见的现象和反汇编结论可以对应起来字符串地址指向了0x0000区域说明源码里的org和实际加载地址不一致。引导扇区偏移510处没有55 AA说明签名丢失或少写了dw 0xaa55。反汇编结果里出现大量66前缀或 32 位寄存器说明代码位宽设置错误。跳转指令目标落在数据区中间说明数据排放或跳转地址计算有问题。第一条指令不是预期代码而是乱码说明镜像写入位置或加载扇区不对。这些现象如果只用眼睛看 C 语言或汇编源码很难发现。反汇编能直接暴露机器码里的地址值、操作数宽度和跳转偏移这也是它成为实模式内核排错首选方法的原因。2. 准备一套可复现的实模式内核实验环境2.1 工具清单和安装命令反汇编排错至少需要四类工具汇编器、十六进制查看工具、反汇编工具和模拟器。推荐在 Linux 环境或者 Windows 的 WSL 环境中操作命令兼容性最好。常用工具如下表。工具作用说明NASM汇编引导扇区源码自带 ndisasm既能汇编也能反汇编xxd / hexdump查看二进制镜像原始字节检查文件大小、签名、偏移binutils / objdump二进制反汇编支持-m i8086和 Intel 语法qemu-system-i386运行实模式内核镜像可开 GDB 服务和指令日志gdb动态调试配合 QEMU 的远程调试端口在 Debian/Ubuntu 系环境里一条命令可以装齐sudo apt-get install nasm binutils qemu-system-x86 gdb xxd如果系统提示xxd不存在通常它包含在vim-common包中sudo apt-get install vim-common学习环境里工具版本不需要完全一致只要保证 NASM 能生成 16 位二进制、QEMU 能运行 i386 镜像即可。实际项目中如果换了编译器或工具链要重新确认指令编码差异尤其是objdump对裸二进制默认地址的处理方式。2.2 最小引导扇区内核源码为了能复现反汇编过程这里给出一个最小引导扇区。它做的事情非常简单设置数据段用 BIOS 中断int 0x10打印一行字符串然后停机。org 0x7c00 bits 16 start: mov ax, 0x07c0 mov ds, ax mov si, msg print_char: lodsb or al, al jz done mov ah, 0x0e int 0x10 jmp print_char done: hlt jmp done msg db Kernel OK, 0 times 510-($-$$) db 0 dw 0xaa55这段代码每行都值得注意。org 0x7c00告诉汇编器代码中的标签地址从0x7c00开始计算。BIOS 把引导扇区加载到0x7c00所以代码里的msg会被解析成0x7c00 偏移。如果去掉这一行msg会被解析成文件内的偏移比如0x0016运行后mov si, msg就会指向错误地址。bits 16强制 NASM 生成 16 位指令。引导扇区默认运行在实模式CPU 按 16 位指令宽度解码。mov ax, 0x07c0和mov ds, ax是为了让ds指向0x07c0段。这样后面lodsb从ds:si读取字符串时实际物理地址等于0x07c0 4 0x7c16也就是物理地址0x7c16和org 0x7c00计算出的标签地址一致。int 0x10的0x0e功能号是在当前光标位置显示一个字符。这个中断是 BIOS 提供的实模式基础服务比较适合做最简输出验证。times 510-($-$$) db 0负责把代码填充到 510 字节。$表示当前位置$$表示当前段的起始所以510-($-$$)就是“还差多少字节到 510”。dw 0xaa55放在最后两个字节作为 BIOS 识别引导扇区的签名。需要注意的是dw 0xaa55在 x86 小端字节序下写到磁盘上的实际二进制顺序是55 AA不是AA 55。后面检查签名时要以字节顺序为准。2.3 构建镜像并启动验证用 NASM 生成二进制镜像再用 dd 写入一个空白软盘镜像nasm -f bin -o boot.bin boot.asm ls -l boot.bin xxd boot.bin | head -n 5 xxd boot.bin | tail -n 4 dd if/dev/zero ofdisk.img bs512 count2880 dd ifboot.bin ofdisk.img convnotrunc第一眼应该看到boot.bin大小是 512 字节。xxd boot.bin | tail -n 4会在最后一行看到类似000001f0的偏移以及结尾部分的55 aa字节。然后启动 QEMUqemu-system-i386 -drive filedisk.img,formatraw如果源码和环境都正确QEMU 窗口里会显示绿色文本Kernel OK。如果没有任何输出或者虚拟机直接重启就需要进入下一步用反汇编检查镜像内容。注意不要只验证“程序能启动”还要验证字符串内容、跳转地址、签名位置这些细节。很多问题不会阻止启动只会让运行结果偏离预期。3. 三类反汇编方法怎么配合使用3.1 先用 hexdump 确认镜像的字节级事实反汇编建立在一个前提上镜像里的字节序列确实和预想一致。如果镜像边界不对后面的反汇编就没有意义。所以第一步永远是查看原始字节。查看引导扇区开头内容xxd boot.bin | head -n 5正常会看到类似这样的内容00000000: b8c0 078e d8be 167c ac08 c074 06b4 0ecd .......|...t... 00000010: 10eb f5f4 ebfd 4b65 726e 656c 204f 4b00 ......Kernel OK.其中4b65726e656c204f4b00这一段就是 ASCII 字符串Kernel OK加结尾的00。这一步可以确认字符串确实写进了镜像而不是文件名或源码路径出问题。查看签名xxd boot.bin | tail -n 1正常最后一行结尾应该是55 aa。例如000001f0: 0000 0000 0000 0000 0000 0000 0000 55aa ..............U.xxd把两个字节显示成55aa这是小端正确写入的结果。如果这里是0000说明引导签名没写进去。3.2 用 ndisasm 按 16 位反汇编原始镜像NDISASM 是 NASM 自带的反汇编器专门处理平面二进制文件。对实模式引导扇区来说最常用命令是ndisasm -b 16 -o 0x7c00 boot.bin参数解释-b 16按 16 位指令宽度反汇编。-o 0x7c00把第一个字节的地址显示成0x7c00。这个参数不改变指令编码只影响相对跳转目标和地址显示。正常输出会类似于00007C00 B8C007 mov ax,0x7c0 00007C03 8ED8 mov ds,ax 00007C05 BE167C mov si,0x7c16 00007C08 AC lodsb 00007C09 08C0 or al,al 00007C0B 7406 jz 0x7c13 00007C0D B40E mov ah,0xe 00007C0F CD10 int 0x10 00007C11 EBF5 jmp 0x7c08 00007C13 F4 hlt 00007C14 EBFD jmp 0x7c13 00007C16 4B dec bx 00007C17 65726E gs jb 0x7c88从0x7c05这条mov si,0x7c16可以看出msg被正确解析到了0x7c16。这说明org 0x7c00生效了。从0x7c16开始的字符串区域会被 ndisasm 当成指令继续解码这属于正常现象。反汇编器无法区分代码和数据所以在看到Kernel OK附近出现奇怪的指令时不要怀疑工具坏了要结合 hexdump 判断这是数据区。如果只想反汇编某个片段可以用-s参数指定起始同步地址。比如只看引导扇区前 32 个字节ndisasm -b 16 -o 0x7c00 boot.bin | head -n 203.3 用 objdump 带 Intel 语法和调整地址反汇编另一个常用工具是 binutils 里的 objdump。它默认面向 ELF 文件但也可以处理裸二进制objdump -D -b binary -m i8086 -Mintel boot.bin参数含义-D反汇编所有段。-b binary输入是裸二进制文件。-m i8086按 Intel 8086 的 16 位指令集解码也就是实模式指令宽度。-Mintel使用 Intel 语法而不是 ATT 语法。上面的命令会把地址从0x00000000开始显示。如果想对齐0x7c00基址加--adjust-vmaobjdump -D -b binary -m i8086 -Mintel --adjust-vma0x7c00 boot.bin输出大概如下00007c00 b8 c0 07 mov ax,0x7c0 00007c03 8e d8 mov ds,ax 00007c05 be 16 7c mov si,0x7c16 00007c08 ac lodsb 00007c09 08 c0 or al,al 00007c0b 74 06 je 0x7c13 00007c0d b4 0e mov ah,0xe 00007c0f cd 10 int 0x10 00007c11 eb f5 jmp 0x7c08 00007c13 f4 hlt 00007c14 eb fd jmp 0x7c13objdump 的优势是输出格式稳定、适合写进脚本Intel 语法对新手也更友好。但在实模式调试时最容易犯的错是忘了-m i8086。如果只写-b binary -Mintelobjdump 会按 32 位指令解码把本来正确的引导扇区反汇编成完全看不懂的内容。所以每次执行前都要检查两条关键参数16 位指令集和正确的地址偏移。3.4 用 GDB 和 QEMU 对照运行时行为静态反汇编只能告诉我们“机器码长什么样”不能告诉我们“CPU 执行到哪一步之后出了问题”。要把两者结合起来就需要 QEMU 的 GDB 服务。先以调试模式启动 QEMUqemu-system-i386 -drive filedisk.img,formatraw -s -S参数-s表示打开 TCP 端口1234的 GDB 服务-S表示启动后暂停等待 GDB 连入。另开一个终端进入 GDBgdb -q在 GDB 内部执行set architecture i8086 target remote :1234 b *0x7c00 c x/8i 0x7c00 si info registers解释一下步骤set architecture i8086告诉 GDB 当前按 16 位实模式解码否则它可能按 32 位模式显示反汇编。target remote :1234连接 QEMU 的调试端口。b *0x7c00在物理地址0x7c00下断点也就是 BIOS 跳转到引导扇区的入口。c继续执行让 CPU 从 BIOS 一直跑到引导扇区。x/8i 0x7c00查看入口地址处的 8 条指令。si单步执行一条机器指令。info registers查看寄存器状态确认ds、si、ip等是否符合预期。如果 GDB 提示set architecture i8086不被识别可以先执行info architecture看当前 GDB 支持的架构列表。有些版本的 GDB 只显示i386这时候可以用set architecture i386但反汇编时要注意手动确认 16 位指令宽度。不过现代发行版里的 GDB 通常会支持i8086。GDB 的动态调试能看见寄存器实际值这是静态反汇编做不到的。比如静态反汇编显示mov si,0x7c16但 GDB 单步后si寄存器里可能实际是别的值。这两者一旦不一致说明要么断点地址不对要么代码在进入该指令前已经被某条指令覆盖。注意QEMU 在-S暂停时CPU 停在 BIOS 的第一条指令而不是引导扇区。如果不设断点直接si会进入 BIOS 的漫长执行流程。断点0x7c00是关键入口。4. 三个需要反汇编才能定位的真实排错案例4.1 案例一忘了 org