8086机器语言解码:从裸字节到汇编指令的手工之路

发布时间:2026/10/13 13:07:10
8086机器语言解码:从裸字节到汇编指令的手工之路
简介个人总结的8086机器语言解码示例笔记面向汇编器编写者与底层学习者系统梳理了8086机器语言的基本概念、指令格式、寄存器、寻址模式、操作码、立即数以及字节、字、双字的定义与适用范围。压缩包内为单个docx文档大小约1.94MB整包内容便于按知识点定位查阅。目前已有127人浏览学习适合需要理解指令编码规律并希望动手验证机器码的读者。笔记重点覆盖固定编码指令和双操作数指令编码围绕MOV指令给出大量机器码对照实例如mov word [bxsi0x1BCD],0x1234对应db 0c7h,80h,0xcd,0x1b,0x34,0x12并结合NASM环境说明最长指令可达8字节、总线锁定前缀与段超越前缀的影响对编写8086汇编器或排查编码错误具有直接参考价值。1. 8086机器语言解码从裸字节到汇编指令的手工之路调试一块跑8086的老板子时最头疼的不是逻辑是看不懂固件里dump出来的二进制。每一段字节流都是一个完整指令但直接看十六进制完全不知道它在干嘛。8086机器语言解码就是把这些字节拆成操作码、寻址字节、位移和立即数还原成MOV、ADD、JMP的过程。这篇笔记是我把这套解码方法整理成可复现示例的过程能把裸字节变成能读懂、能验证的汇编文本。适合刚开始碰汇编、想在逆向或底层调试里追上老固件的人。读完你会得到一套手算套路和一组能跑通的最小解码代码。2. 8086指令编码骨架操作码、ModRM与字段边界2.1 指令的骨架从字节序列到每个字段先建立整体认知。8086指令是变长的最短1个字节0x90就是NOP最长可以到6个字节前缀操作码ModRM位移立即数。解码的本质是确定边界先看前缀再看操作码然后根据操作码决定后面还有没有ModRM、位移和立即数。这个顺序不能乱你把操作码看错了后面全是黑匣子。常见格式是[前缀] [操作码] [ModRM] [位移] [立即数]注意ModRM后面还可能跟SIB字节但那是80386以后的设计8086指令集里根本没有SIB。解码纯8086时不需要处理SIB这省了一大半麻烦。你需要关心的是前缀、操作码、ModRM、位移和立即数这五类字段。下表是这些字段的长度和职责字段长度作用前缀1字节可多个修改段、重复行为操作码1字节确定指令类型和后续字段ModRM1字节确定操作数寻址方式位移0/1/2字节内存偏移量小端存储立即数0/1/2字节立即操作数小端存储这里你可以看到位移和立即数都是小端序也就是低字节在前。比如机器码34 12表示立即数 0x1234不是 0x3412。很多人第一次都在这里翻车。解码时最重要的一步是「先确定操作码再决定后面怎么读」。操作码本身可能包含位信息比如低3位经常代表寄存器编号。以 MOV 为例0xB0-0xB7 是MOV r8, imm80xB8-0xBF 是MOV r16, imm16两者的立即数宽度完全不同。只看开头一个字节是 B8 还是 B0就能知道该读 1 个还是 2 个立即数字节。这类操作码属于「指令自己带寄存器和立即数」后面不再跟 ModRM。还有一类操作码是「需要 ModRM 配合」比如 0x88/0x89/0x8A/0x8B 这四条 MOV。操作码只告诉你这是一条 MOV具体从哪个寄存器搬到哪个内存或寄存器由后面的 ModRM 字节决定。这种操作码解码起来最绕也是后面重点练习的部分。还有第三类操作码完全不涉及 ModRM比如 NOP、JMP short、条件跳转。短跳转EB offset后面跟一个8位带符号偏移E9 offset16后面跟16位偏移。这些只要读操作码就知道长度不需要额外字段。一个实用的分类方法把操作码按「是否携带 ModRM」分成两种。凡是 0x00-0x3B 的算术逻辑操作码基本都带 ModRM0x88-0x8B 的 MOV 带0xB0-0xBF 的立即数 MOV 不带0xC0-0xFF 里面有一半带一半不带。解码器里维护一张表列出每个操作码的字段需求比硬编码判断可靠得多。2.2 ModRM 字节寄存器表、mod 和 rm 的配合ModRM 是全解码过程中最容易卡住的地方。它一个字节分成三块mod最高2位位6-7决定寻址模式reg中间3位位3-5决定寄存器操作数或扩展操作码rm最低3位位0-2决定另一个操作数在内存寻址时决定组合方式用位运算取出来是这样mod (modrm 6) 0x03 reg (modrm 3) 0x07 rm modrm 0x07mod 的取值直接决定后续有没有位移00无位移。但 rm110 时例外它表示后面紧跟一个16位直接地址。018位位移需要读一个字节并做符号扩展。1016位位移需要读两个字节小端序。11rm 字段变成寄存器操作数不再是内存寻址。reg 字段永远是一个寄存器或者一个扩展操作码。到底代表什么取决于操作码。如果操作码是8位操作就查8位寄存器表16位操作就查16位寄存器表。表如下reg16位8位000AXAL001CXCL010DXDL011BXBL100SPAH101BPCH110SIDH111DIBH这张表建议抄在笔记本上或者写进代码的数组里。注意8位寄存器表里100-111是 AH/CH/DH/BH不是 AL/CL/DL/BL这是最容易记混的地方。我第一次查表时就把 AH 写成了 AL。当 mod11 时rm 字段也查同一张寄存器表。所以8B D8这个解码流程是操作码 0x8B 表示 MOV r16, r/m16方向是 reg 作为目的ModRM 0xD8 拆成 mod11、reg011、rm000查表得 reg 是 BXrm 是 AX于是指令是MOV AX, BX。方向要注意0x8B 的语义是 destreg所以 AX 是目的BX 是源。虽然机器码里 BX 的编码在 reg 字段AX 在 rm 字段。当 mod00/01/10 时rm 不再表示寄存器而是表示内存地址的组合方式。8086 只能用基址变址的固定组合不能像后来32位那样随便加。组合表是这样的rmmod00mod01/10000[BXSI][BXSIdisp]001[BXDI][BXDIdisp]010[BPSI][BPSIdisp]011[BPDI][BPDIdisp]100[SI][SIdisp]101[DI][DIdisp]110[disp16][BPdisp]111[BX][BXdisp]注意最后一行mod00 且 rm110 时是特例直接地址[disp16]没有基址寄存器而 mod01/10 时 rm110 又变回[BPdisp]。这个特例我至少遇到过三次以上每次都以为是 BP 的锅其实是 mod 不同导致的行为不同。解码内存寻址时拼字符串的规则是先写基址和变址的组合再按顺序加位移。位移为正就写成disp为负就写成disp例如[BPSI-2]。段前缀默认一般是 DS但如果 BP 出现在组合里默认段是 SS。这个属于语义层面不影响编码长度但影响你最终写的汇编助记符。解码器如果只是还原成文本可以不处理默认段如果要生成可汇编的文本建议在地址场景加提示。有了这两张表和 mod 的判断规则你就拿到了解码 8086 的一半钥匙。另一半是操作码表我们把几个典型指令串起来实际解码一遍。3. 手算解码到半自动脚本三个示例与最小Python实现3.1 手算解码示例拆字节流的三个标准动作我们拿三个最常见的指令现场走一遍。第一个例子是B8 34 12。看到 B8立刻想到操作码范围 0xB8-0xBF它表示MOV r16, imm16低3位 0 是 AX后面跟2个立即数字节。小端序34 12拼成 0x1234所以完整指令是MOV AX, 0x1234。这个例子没有 ModRM指令长度 3 字节。第二个例子是有 ModRM 的89 1E 34 12。0x89 是MOV r/m16, r16方向是 r/m 作为目的reg 作为源。ModRM 0x1E 拆开mod00reg011BXrm110。mod00 且 rm110 直接进入特例后面跟 16 位直接地址。读取34 12得到 0x1234。于是指令是MOV [0x1234], BX。注意这里目的操作数是内存源操作数是寄存器。第三个例子是带方向位和位移的8B 46 04。0x8B 是MOV r16, r/m16方向是 reg 作为目的。ModRM 0x46 拆成 mod01、reg000AX、rm110。mod01 表示有 8 位位移rm110 在 mod01 时是[BPdisp]。读取位移 0x04于是得到MOV AX, [BP0x04]。如果位移是负数比如8B 46 FC0xFC 要符号扩展成 -4输出MOV AX, [BP-0x04]。这三个例子正好覆盖了无 ModRM、ModRM 特例、ModRM位移三种情况。手算的标准动作可以总结成三步读操作码判断是否带 ModRM、立即数宽度、方向位。读 ModRM拆出 mod/reg/rm再按 mod 决定是否读位移。读立即数按小端拼接输出助记符和操作数。如果只是做逆向学习跟着这三个示例练几遍再去读指令手册基本能上手。但如果要批量解码一段固件手算太慢了。所以我把这套规则固化成一个小脚本。3.2 用Python把解码流程固化成小工具最小实现与字段说明下面给出一段可以直接运行的最小解码代码覆盖上面三个示例外加前缀处理和短跳转。它不是一个完整指令集解码器而是一个能看懂的骨架。我一般会在此基础上按需补操作码表。reg16 [AX, CX, DX, BX, SP, BP, SI, DI] reg8 [AL, CL, DL, BL, AH, CH, DH, BH] # 内存组合mod!11 时使用 rm_mem [BXSI, BXDI, BPSI, BPDI, SI, DI, , BX] def sign_extend(value, bits): # 把 bits 位二进制数按带符号解释 if value (1 (bits - 1)): value - (1 bits) return value def decode_modrm(modrm): mod (modrm 6) 0x03 reg (modrm 3) 0x07 rm modrm 0x07 if mod 3: # rm 是寄存器 return reg16[reg], reg16[rm], 0, False base rm_mem[rm] disp_len 0 if mod 0 and rm 6: # 直接地址特例后面跟 16 位位移 return reg16[reg], None, 2, True if mod 1: disp_len 1 elif mod 2: disp_len 2 return reg16[reg], base, disp_len, True def decode_8086(stream): pos 0 lines [] while pos len(stream): start pos prefix # 吃掉段/重复前缀 while pos len(stream) and stream[pos] in (0x26, 0x2E, 0x36, 0x3E, 0xF2, 0xF3): prefix_map {0x26: ES:, 0x2E: CS:, 0x36: SS:, 0x3E: DS:, 0xF2: REPNE , 0xF3: REP } prefix prefix_map.get(stream[pos], ) pos 1 opcode stream[pos] pos 1 if 0xB0 opcode 0xB7: reg opcode 0x07 imm stream[pos] pos 1 text fMOV {reg8[reg]}, 0x{imm:02X} elif 0xB8 opcode 0xBF: reg opcode 0x07 imm stream[pos] | (stream[pos 1] 8) pos 2 text fMOV {reg16[reg]}, 0x{imm:04X} elif opcode in (0x88, 0x89, 0x8A, 0x8B): modrm stream[pos] pos 1 reg_text, rm_part, disp_len, is_mem decode_modrm(modrm) disp 0 for i in range(disp_len): disp | stream[pos i] (8 * i) pos disp_len if disp_len 1: disp sign_extend(disp, 8) if rm_part is None: operand f[{disp:04X}] elif is_mem: if disp: operand f[{rm_part}{disp:#04X}] else: operand f[{rm_part}] else: operand rm_part if opcode in (0x88, 0x89): text fMOV {operand}, {reg_text} else: text fMOV {reg_text}, {operand} elif opcode 0xEB: offset sign_extend(stream[pos], 8) pos 1 target start 2 offset text fJMP 0x{target:04X} else: # 未知操作码保守输出原始字节 text fDB 0x{opcode:02X} bytes_hex .join(f{b:02X} for b in stream[start:pos]) lines.append(f{start:04X} {bytes_hex:12} {prefix}{text}) return \n.join(lines)这段代码很简略但已经能跑通前三个例子。我把几个关键设计说一下。先说decode_modrm函数它负责处理 ModRM。返回值里is_mem用于区分是寄存器操作数还是内存操作数。mod3 时直接查寄存器表返回两个寄存器mod00/01/10 时查内存组合表并给出位移长度。这里没有处理 8 位寄存器的场景因为示例里全用 16 位但结构上已经把 mod 的三种情况都覆盖了。如果你要支持 8 位只需要根据操作码是 0x88/0xB0 还是 0x89/0xB8 来选择reg8还是reg16。再说主循环里段前缀的处理。前缀字节 0x26/0x2E/0x36/0x3E 分别对应 ES/CS/SS/DS 段覆盖0xF2/0xF3 是重复前缀。我在循环开头把它们吃掉并拼到输出文本的前面。这个顺序必须在操作码之前否则3E 89 1E 34 12会被当成操作码 3E 处理后面的 89 就整个错位。方向位的处理是 MOV 族最容易错的点。0x88/0x89 表示目的在 r/m0x8A/0x8B 表示目的在 reg。代码里用opcode in判断 dst 和 src 的赋值顺序避免把MOV [0x1234], BX写成MOV BX, [0x1234]。disp 的拼接需要注意当 mod00 且 rm110 时返回的是空字符串作为 base外层再拼成[disp16]。mod01/10 时base 是BP等组合位移用%#04X格式化负数会变成-0x04的形式直观也被汇编器接受。代码的最后一块是elif opcode 0xEB处理短跳转。目标地址计算用到start它是当前指令在字节流里的起始偏移指令自身占2字节加上带符号偏移就得到绝对地址。注意偏移量做了sign_extend(stream[pos], 8)这里一定要扩否则EB FE会算成 0x00FE而实际是往回跳两位。运行这段代码把前面三个机器码喂进去code bytes.fromhex(B83412 891E3412 8B4604 EBFE) print(decode_8086(code))输出分别是0000 B8 34 12 MOV AX, 0x1234 0003 89 1E 34 12 MOV [0x1234], BX 0007 8B 46 04 MOV AX, [BP0x04] 000A EB FE JMP 0x000A最后一行JMP 0x000A正好跳回自己是死循环说明符号扩展生效了。你可以把EB FE改成EB 03会得到跳转到第 0x000F 行也就是越过后面三条指令这也是符合预期的。有了这个骨架后面要加 ADD、SUB、CMP 这类指令只要在操作码表里加一行并补充 reg 字段是扩展操作码的处理即可。下一章我会把常见坑单独拿出来讲因为这些坑不是查表能避免的。4. 8086解码避坑五个翻车点与排查方法这些坑不是从手册里看出来的都是在实际解码一堆固件时踩出来的。手册上每个特例都写得清清楚楚但你把字节抄下来时脑子里全是操作码列表哪还记得住 mod00 的 rm110 是特例。所以我这里把高频坑按「现象-原因-解决」固定下来方便排查。4.1 ModRM 特例mod00 且 rm110 的绝对地址现象解码89 1E 34 12时把 rm110 按常规内存组合输出成[BPSI]结果变成MOV [BPSI], BX完全看不懂。原因8086 的寻址组合表里rm110 在 mod00 时是特例表示直接地址[disp16]不是[BPSI]也不是[BP]。因为 BP 的默认段是 SS如果当成 BP 会连默认段都搞错。解决在解码函数里先检查mod 0 and rm 6再决定是否读16位地址。我在前面的decode_modrm里就单独返回了is_memTrue, rm_partNone让外层拼成[0x1234]。这一步如果漏掉后续所有绝对寻址指令都会错。排查时可以打印mod/reg/rm三值对照组合表一眼就能看出问题。你甚至可以在测试用例里显式加上一条assert decode_8086(bytes.fromhex(891E3412)) 0000 89 1E 34 12 MOV [0x1234], BX4.2 位移的符号扩展短跳转目标算错现象EB FE被解成JMP 0x00FE而实际应该跳回当前指令自身地址是 0x0000假设指令从0开始。原因位移字段是带符号的0xFE 表示 -2。如果按无符号数读就变成 254。解决对8位位移先做符号扩展再参与地址计算。代码里的sign_extend(stream[pos], 8)做了这件事公式是如果最高位为1则减去1 bits。16位位移不需要符号扩展因为按小端读出的 ushort 在 Python 里直接就是正数但如果是 32 位环境模拟 16 位运算也可能出问题。一个通用写法是def sign_extend(value, bits): if value (1 (bits - 1)): value - (1 bits) return value这行我建议直接封装成函数所有带符号位移和立即数都走它。排查时把0xFE传入函数确认输出是 -2 而不是 254。4.3 立即数宽度与寄存器宽度操作码范围是唯一标准现象B8 34 12被解成MOV AL, 0x34然后丢掉 0x12或者把 0x12 当作下一条指令。原因B8-BF 是 16 位立即数 MOV而 B0-B7 是 8 位立即数 MOV。两者操作码低3位都表示寄存器但立即数宽度不同。新手很容易只盯低3位忽略操作码高位范围。解决用if 0xB0 opcode 0xB7和elif 0xB8 opcode 0xBF两个条件区分。不要用opcode 0xF8这种掩码直接判断因为 B0 和 B8 的高位掩码不同。另一个细节是立即数宽度还受操作码影响比如 0xA0-0xA3 是 AL/AX 和内存之间的传送宽度看累加器决定。所以阅读操作码表时重点是看「后面跟几个立即数字节」而不是想当然按默认字长。排查时遇到 B 开头的操作码先查范围再定宽度。4.4 前缀字节混入操作码段覆盖和前导字节现象3E 8B 46 04被当作操作码 3E 处理后面的 8B 变成第二操作码整条指令解码结果从MOV AX,[BP4]变成DS:??。原因8086 的前缀字节在指令最前面段覆盖 0x3E 表示 DS 段不会单独成为操作码。解码时如果没先吃掉前缀主操作码就读错。解决在解码循环开头加一个前缀判断循环把所有已知前缀全部跳过并把前缀语义记录到输出文本里。注意多个前缀可以叠加比如F3 3E会同时有 REP 和 DS 覆盖所以不能只判断一个就完事。我之前就吃过亏用单次 if 判断遇到两个前缀时第二个前缀被当成操作码。正确的循环写法是while pos len(stream) and stream[pos] in (0x26, 0x2E, 0x36, 0x3E, 0xF2, 0xF3): prefix prefix_map.get(stream[pos], ) pos 1排查时如果发现解码结果的起始地址和预期不一致先看机器码开头几个字节是不是前缀。4.5 扩展操作码reg 字段不是寄存器现象80 C3 05被解成MOV BL, AL实际应该是ADD BL, 0x05。原因操作码 0x80 是 ALU 组的立即数指令它的 reg 字段不再表示寄存器操作数而是表示具体操作比如 000 是 ADD、101 是 SUB、111 是 CMP 等。这类指令把 reg 字段当「扩展操作码」用常见于 0x80-0x83、0xC0-0xC7、0xF6-0xFF 等。解决把操作码表分成两类一类是普通操作码一类是扩展操作码。普通操作码直接查指令助记符扩展操作码则先查它的子表再用 reg 字段索引子表得到真正操作名。比如83 C3 050x83 表示ALU r/m16, imm8reg000 查子表得到 ADD所以最终是ADD BX, 0x05符号扩展后的 imm8。解码器里遇到这种操作码要强制把 reg 当操作码索引而不是寄存器否则一定会翻车。排查时遇到 0x80、0x83、0xFF 这类操作码先确认 reg 字段在表里对应什么操作。5. 把笔记扩展成指令表与半自动验证5.1 从单条指令到指令族组织一份可维护的解码表第三部分的脚本只处理了 MOV 和 JMP。真正要解码一段固件至少需要把算术逻辑指令、跳转、栈操作、中断等都纳进来。手工在代码里写一堆 elif 会很难维护。我一般会做一个「指令表驱动」的解码器骨架用表来区分每条指令的行为。表的核心结构可以是这样操作码助记符是否需要ModRM立即数宽度方向位扩展操作码子表0x00ADD是0r/m无0x01ADD是0r/m无0x02ADD是0reg无0x03ADD是0reg无0x80ALU是1r/m是0x83ALU是1r/m是0xB0MOV否1寄存器无0xB8MOV否2寄存器无0xEBJMP否1无无这张表在 Python 里可以直接用一个字典表示。下面是提取出来的核心部分opcode_map { 0xB0: (MOV, r8, imm8), 0xB8: (MOV, r16, imm16), 0x88: (MOV, r/m16, r16), 0x8B: (MOV, r16, r/m16), 0xEB: (JMP, rel8), }但这里有个问题像 0x00-0x3B 这一大串模式高度一致只是助记符不同。我们可以把它们归并成「ALU 模板」操作码高3位决定助记符方向位由低3位决定。比如 0x00-0x01 是 ADD0x08-0x09 是 OR0x20-0x21 是 AND0x38-0x39 是 CMP。这种模板化处理比逐条罗列更接近硬件编码逻辑。模板的写法是alu_prefix { ADD: 0x00, OR: 0x08, ADC: 0x10, SBB: 0x18, AND: 0x20, SUB: 0x28, XOR: 0x30, CMP: 0x38, }然后根据操作码的最低两位判断方向和宽度bit0 是方向bit1 是宽度0表示8位1表示16位。这样 0x00 对应 ADD r/m8,r80x01 对应 ADD r/m16,r160x02 对应 ADD r8,r/m80x03 对应 ADD r16,r/m16。用表驱动比一大串 elif 清晰多了。组织解码表时还要处理「扩展操作码」的情况也就是 0x80、0x81、0x83 这类。它们的 reg 字段是操作码索引。我们需要再建一张子表比如alu_ext { 0x00: ADD, 0x01: OR, 0x02: ADC, 0x03: SBB, 0x04: AND, 0x05: SUB, 0x06: XOR, 0x07: CMP, }子表按 reg 值索引解码时alu_ext[reg]得到真实助记符。这一步我最初漏了导致 0x83 C3 05 解出来是 MOV折腾了半天才反应过来。所以在落地时建议把扩展操作码和普通操作码分开存储并在注释里标记清楚哪些操作码走子表。5.2 用交叉对照验证解码结果避免一错到底解码器写完最大的问题是不知道自己解得对不对尤其是手写解码规则时很容易一个符号扩展错误导致全错。我一般会用「汇编-解码」回路来交叉验证。做法很简单写一小段带明确地址的汇编程序用你手边的 8086 汇编器编成二进制然后把这个二进制作为解码器的输入看解码结果和你写的原始汇编是否一致。如果不一致基本能锁定是哪条指令、哪个字段出错。下面是一个最小验证脚本的思路它把解码器的输出重新声明成汇编源文件再用汇编器反向编译# decode_output.txt 是解码器逐行输出的汇编文本 # 注意需要给每行补上起始地址汇编器才能生成同样的机器码 # 这一步可以用脚本自动完成在每行前面加 org 0x0000然后逐行写入不过更直接的做法是写一个 Python 断言测试把「机器码 - 期望汇编文本」对一组一组喂进去。我建议把测试用例和指令表写在同一个文件里每修一条就补一个用例。举几个用例B8 34 12-MOV AX, 0x123489 1E 34 12-MOV [0x1234], BX8B 46 FC-MOV AX, [BP-0x04]EB FE-JMP 0x0000从偏移0起算83 C3 05-ADD BX, 0x05这些用例全部通过后再拿真实的固件 bin 片段试跑。试跑阶段我一般保留DB 0x..的兜底输出遇到未知操作码时先记录不中断这样能一次性看到所有覆盖不到的指令。等收集够了再补充操作码表。验证时不光要看指令文本还要看每条指令的字节长度。因为解码器的核心逻辑就是「确定边界」长度错了后面所有指令的地址都会偏移。所以输出模板里要打印起始地址这样你能直观看到地址是否连续。如果中间跳了一段说明前一条指令的长度读错了。比如我前面输出0000、0003、0007地址是连续递增的如果出现断档就说明某条指令长度被少读或多读了一个字节。5.3 把笔记沉淀成可复用的练习集把解码规则整理成笔记时不要只抄手册而是做成示例集。每个示例包含机器码、解码过程、输出汇编、对应的地址长度。这种示例集比表格更容易记忆。我们可以给出一个示例条目的形式示例 0x8B 46 FC 机器码8B 46 FC 分析0x8B MOV r16, r/m16方向reg 0x46 mod01, reg000(AX), rm110 mod01 8位位移 0xFC 符号扩展为 -4 rm110 在 mod01 时 [BPdisp] 输出MOV AX, [BP-0x04]这个示例一目了然。维护一大批这样的示例后即使很久不看汇编回头翻笔记也能在几分钟内恢复解码手感。这是我觉得比单纯存代码更有长期价值的部分。你甚至可以把这个 markdown 文件直接当作测试用例文档和 Python 测试脚本放在同一个目录里每次改代码就跑一遍示例集确保没有回归。6. 把解码器变成带注释的 hexdump批量梳理二进制片段前面代码只能处理一条指令真正拿到固件 bin 文件时需要逐地址解码并输出成列表。我把这个功能封装成一个函数输入是 bytes输出是带地址和机器码的对齐文本。这样直接重定向到文件就能当纯文本手册看。下面是一个可直接用的批量输出函数基于前面的 decode_8086 改造def hexdump_with_decode(stream): text decode_8086(stream) print(text) if __name__ __main__: with open(fw.bin, rb) as f: hexdump_with_decode(f.read())把输出保存成fw.lst配合文本编辑器就能逐行对照地址检查。如果某些指令显示为DB 0x..就说明操作码表还有缺口。我的习惯是把这些缺口按地址归档等攒到一批再统一补表。一个更实用的技巧是在每行输出前打印指令长度。decode_8086里的start和pos之差就是本指令长度可以加进输出格式。这样你能一眼看出当前指令占几个字节比如 9 字节的长指令在列表里会特别明显。对排查边界问题非常有帮助。修改方式很简单把lines.append那一行加上长度字段length pos - start lines.append(f{start:04X} {length:02X} {bytes_hex:12} {prefix}{text})不过要注意如果你用了这个格式前面测试用例的断言文本也要同步更新否则对不上。结尾想说的是我把自己踩过的这些坑写进笔记后再解码老固件时心里就有底了。一个小教训解码结果一定要回汇编器验证哪怕只是几个简单用例也能及时挡住方向位和符号扩展这两大类错误。希望帮到你。本文还有配套的精品资源点击获取