简介面向8086汇编器开发人员的机器语言解码参考笔记由作者根据实际调试与验证过程整理。内容覆盖机器语言基本概念、指令格式、寄存器编号、寻址模式、操作码与立即数规则并解释字节、字、双字的宽度区别对固定编码指令和双操作数指令编码做了重点拆解整理了多类MOV指令的机器码字节序列包括寄存器互传、内存寻址、立即数传送和段寄存器传送等可直接对照编写汇编器。笔记还说明了指令最长可达八字节由总线锁定前缀、段超越前缀、操作码、寻址模式字段、偏移量和立即数依次构成并列出AAA等固定编码的单字节指令。资源为单篇Word文档压缩包大小约1.94MB结构紧凑已经有一百二十七人学习浏览适合正在实现8086汇编器或研究x86机器码的开发者作为速查笔记。1. 8086机器语言解码示例别再对着黑匣子里的字节发呆调试一个老式引导程序时反汇编器把数据段当成了指令流满屏乱码而我手里的资料只有一段手抄的十六进制字节。那一刻我才意识到自己根本没把 8086 机器语言解码当作一项可复现的手艺来练。所谓解码就是把机器码按操作码、ModRM、位移、立即数逐字段拆开还原成汇编指令。这篇笔记是我把调试中验证过的示例和翻车记录整理成的流程适合学微机原理、写小型反汇编器以及想搞懂指令编码规则的读者照着推演一遍。2. 先看穿编码骨架操作码、ModRM 与 16 位寻址的配合规则2.1 一条完整指令的组成前缀、操作码、ModRM、位移、立即数8086 的指令编码不是简单地把“助记符”翻译成固定字节而是由若干可选字段拼接而成。常见的布局从上到下是段覆盖前缀可选、操作码、ModRM、位移、立即数。其中操作码决定这条指令“干什么”ModRM 决定“操作数怎么找”位移和立即数则是操作数的一部分。字段长度说明段覆盖前缀0 或 1 字节26HES、2EHCS、36HSS、3EHDS用于临时改默认段操作码1~2 字节决定指令功能例如 B8 表示 MOV AX, imm16ModRM0 或 1 字节出现与否由操作码决定表达寻址方式和寄存器编号位移 disp0/1/2 字节由 ModRM 的 mod 字段决定是否存在、是 8 位还是 16 位立即数 imm0/1/2 字节由操作码决定注意 83H 这类指令的立即数虽然只有 8 位却要符号扩展这里有一个新手最容易忽略的判定不是每条指令都有 ModRM。像 B8MOV AX, imm16这样的累加器专用编码操作码里已经写死了寄存器就不需要 ModRM而 8B、89、C7、83 这类指令必须靠 ModRM 才能表达操作数位置。解码的第一步不是读字节而是先判断“这一条需不需要 ModRM”判断错了后面所有字段都会错位。为什么 8086 要引入 ModRM因为操作码只有一个字节最多表达 256 种功能而 MOV 又要覆盖寄存器、内存、立即数各种组合。把寻址方式单独抽成一个字节后操作码只负责“功能”ModRM 负责“取数”这样编码密度更高解码时也能用同一套查表逻辑处理一批指令。2.2 ModRM 才是三字节指令的中枢mod、reg、rm 的拆解方法ModRM 字节长 8 位从高到低分成三段mod 占 2 位、reg 占 3 位、rm 占 3 位。比如 47H 展开成二进制是 01000111拆出来就是 mod01、reg000、rm111。mod 决定有没有位移、位移几字节reg 决定用哪个寄存器也可能被征用为扩展操作码rm 决定用哪个寄存器组合去访问内存或者直接指一个寄存器。reg 字段需要一张寄存器映射表。16 位操作时按 AX、CX、DX、BX、SP、BP、SI、DI 顺序编号对应 reg 值 000~1118 位操作时则是 AL、CL、DL、BL、AH、CH、DH、BH。注意编号顺序不是字母序很多初查表时把 reg100 误认成 SP 而不是 SI一错就错一整条指令。rm 值mod00无位移mod01disp8mod10disp16000[BXSI][BXSId8][BXSId16]001[BXDI][BXDId8][BXDId16]010[BPSI][BPSId8][BPSId16]011[BPDI][BPDId8][BPDId16]100[SI][SId8][SId16]101[DI][DId8][DId16]110直接地址 disp16[BPd8][BPd16]111[BX][BXd8][BXd16]这张表是整个解码笔记的核心。特别注意 mod00、rm110 那一格它表示后面直接跟一个 16 位绝对地址没有寄存器参与这和 mod01、rm110 的 [BPd8] 是两回事。表里只有 BX、BP、SI、DI 四种寄存器参与内存寻址AX、CX、DX 在 16 位寻址里不能当地址寄存器这与 32 位环境差异极大套用 32 位思维解码 8086 会很别扭。2.3 三条默认规则段寄存器、方向位与符号扩展拆完字段还要知道三条隐含规则。第一条是默认段BX、SI、DI 参与内存寻址时默认走 DS 段BP、SP 参与时默认走 SS 段。所以看到 ModRM 里出现 BP 相关的 rm 组合解码结果必须标上 SS不能一刀切写成 DS。第二条是方向位 d。同样是 MOV89H 和 8BH 的区别在于数据流向89H 是“寄存器送内存/寄存器”8BH 是“内存/寄存器送寄存器”。8AH 和 8AH 系列靠操作码 bit1 区分方向解码错了方向操作数位置就完全颠倒。第三条是符号扩展。mod01 的 8 位位移虽然是字节但地址计算时要当作有符号数扩展到 16 位指令 83H 后面的 imm8 同样要符号扩展。例如位移 FEH 不是 254而是 -2。这条规则直接影响解码结果的正确性后面第 3.4 节会专门演示。提示解码前先把 16 位寻址寄存器组合表抄在纸上遇到 ModRM 先写 mod/reg/rm 三段再查表。手写解码的多数错误都发生在“跳过了这张表”的时候。3. 四个解码示例的完整推导从机器码到汇编指令3.1 从 B8 34 12 开始操作码自带寄存器的简写编码第一个示例是最短的 MOV 立即数形式。机器码是 B8 34 12长度 3 字节。B8 的二进制是 10111000这里要注意它的位分配和普通指令不同低 3 位是寄存器编号 000bit3 是 w 位值为 1 表示 16 位操作。寄存器编号 000 在 16 位表里对应 AX所以操作码本身已经告诉我们是 MOV AX, imm16。后面两个字节 34 12 是立即数。8086 是小端序低字节在前所以内存里先放 34H、再放 12H拼出来的逻辑值应当是 0x1234而不是 0x3412。按字节流顺序直接拼数值、忘记高低倒序是新手第一个高频翻车点。有了这一步解码结果就是 MOV AX, 1234H。这个例子的价值在于揭示了 8086 编码中的一类简写操作码直接携带寄存器编号不经过 ModRM。B0~B7 是 MOV r8, imm8B8~BF 是 MOV r16, imm16它们的规则一致。以后看到这类操作码直接读后续立即数不要再去找 ModRM否则会把立即数误当成 ModRM 去解析。3.2 用 8B 47 02 拆解 ModRMmod、reg、rm 逐位手工解码第二个示例进入真正的 ModRM 解码。机器码 8B 47 02长度 3 字节。先看操作码 8B展开成 10001011它的方向位 d1、w1含义是 MOV reg16, r/m16也就是把右边操作数的值送到左边寄存器。接着读第二个字节 47H。展开成二进制 01000111按 mod、reg、rm 三段切开mod01、reg000、rm111。mod01 表示后面跟一个 8 位位移rm111 去查 16 位寻址表对应 [BXd8]reg000 结合 w1得到 AX。最后读取位移字节 02H正数不需要符号扩展。整条指令拼起来就是 MOV AX, [BX02H]。这里要刻意体会 ModRM 查表顺序先 mod 决定位移宽度再 rm 决定基址组合最后 reg 决定目标寄存器。如果反过来先读 reg把 rm111 忽略掉就会把 [BX02H] 错误理解成 BX 寄存器整条指令面目全非。再看一个对比如果操作码换成 8Aw 位变成 0解码结果就是 MOV AL, [BX02H]因此解码器必须把操作码和 ModRM 绑定起来看。3.3 用 C7 06 34 12 78 56 处理直接地址与立即数第三个示例把直接地址和立即数同时放进一条指令。机器码是 C7 06 34 12 78 56长度 6 字节。操作码 C7 是 MOV r/m16, imm16写作 C7 /0意思是它的 reg 字段不表示寄存器而是扩展操作码必须为 000。第二字节 06H 展开成 00000110mod00、reg000、rm110。mod00、rm110 对应查表结果是“直接地址 disp16”于是后续两个字节 34 12 是小端序地址逻辑值是 0x1234。再往后两个字节 78 56 是立即数逻辑值是 0x5678。完整解码结果是 MOV WORD PTR [1234H], 5678H。为什么这里必须写 WORD PTR因为 C7 的 w 位指示这是一个 16 位操作但 ModRM 里没有任何寄存器来体现操作数宽度汇编语法里只能靠 WORD PTR 把这个宽度显式标出来。许多初学者在这里漏掉 PTR 标记输出的汇编再交给汇编器时就会被当成歧义写法。另一个易错点是 reg000 在 C7 里不代表 AX它是扩展操作码的保留值前面 3.1 那种“操作码自带寄存器”的直觉在这里不再适用。3.4 用 83 C0 01 看符号扩展一条短指令和它等价的长写法第四个示例用来修正一个顽固直觉。机器码是 83 C0 01长度 3 字节。操作码 83 表示 ADD r/m16, imm8写作 83 /0关键的坑在于它后面的立即数长度为 8 位但结果要按有符号扩展到 16 位。第二字节 C0H 展开成 11000000mod11、reg000、rm000。mod11 表示操作数直接是寄存器rm000 在 16 位表里对应 AXreg 作为扩展操作码要求为 000。第三字节 01H 是 imm8符号扩展后仍是正数 0x0001解码结果是 ADD AX, 1。这条指令和 81 C0 01 00 功能完全一致后者用的是 imm16。汇编器倾向于生成 83H 短形式来省字节所以反汇编结果里常看到 83H。若此时解码器把 imm8 当成无符号数处理遇到 FFH 就会输出 ADD AX, 255而正确结果应当是 ADD AX, -1。符号扩展不是可选项而是 8086 对这个操作码定义的固定行为。提示遇到 83H、81H 这类带 /0 扩展操作码的指令reg 字段一律先置 0 再查语义不要把它当寄存器号。3.5 把推导固化成解码伪代码字段拆解与参数设定四个示例跑通后可以沉淀成一段伪代码。下面这一段只负责把机器码拆成字段语义表需要自己补全但它能保证边界判断不再凭感觉。def split_fields(instr: bytes): # 返回指令字段字典不做完整语义翻译 pos 0 seg_prefix None prefixes {0x26: ES, 0x2E: CS, 0x36: SS, 0x3E: DS} # 第一步吃掉可选段覆盖前缀 while pos len(instr) and instr[pos] in prefixes: seg_prefix prefixes[instr[pos]] pos 1 opcode instr[pos] pos 1 # 第二步根据操作码决定是否需要 ModRM needs_modrm (0x88 opcode 0x8B) or opcode in (0xC7, 0x81, 0x83) if not needs_modrm: # 例如 B8 这类累加器专用编码直接读立即数 imm instr[pos:pos2] return {opcode: opcode, modrm: None, disp: b, imm: imm} modrm instr[pos] pos 1 mod (modrm 6) 0x03 reg (modrm 3) 0x07 rm modrm 0x07 # 第三步按 mod 决定位移字段的宽度 disp b if mod 0b01: disp instr[pos:pos1] pos 1 elif mod 0b10: disp instr[pos:pos2] pos 2 elif mod 0b00 and rm 0b110: # mod00, rm110 表示直接地址 disp16 disp instr[pos:pos2] pos 2 # 第四步立即数长度由操作码族决定这里是示例约定 imm_len 2 if opcode in (0xC7, 0x81) else 1 if opcode 0x83 else 0 imm instr[pos:posimm_len] return {opcode: opcode, modrm: (mod, reg, rm), disp: disp, imm: imm}这段代码里 needs_modrm 集合写死了需要 ModRM 的操作码范围实际完整解码器应当用查表替代这个集合。第二步到第三步的顺序不能颠倒先定位 ModRM再读取位移。mod00、rm110 这个组合单独处理是因为它没有占用寄存器组合直接读 disp16。imm 的长度不由 w 位决定而由操作码决定尤其 83H 的 imm 固定为 1 字节这个参数很容易被误设成 2 字节。4. 解码翻车避坑5 个常见错误与排查路径4.1 小端序读反B8 34 12 被解成 MOV AX, 3412H现象手算 B8 34 12 时按字节流顺序把后两个字节读成 0x3412输出 MOV AX, 3412H。原因是内存里 34H 在低地址、12H 在高地址8086 取字时低地址字节是低 8 位所以数值应当反转拼接成 0x1234。解决方法是把十六进制字节流和逻辑数值分开写字节流按内存顺序抄写数值永远按“低字节在前”的规则拼。每做完一条解码先用 B8 34 12 这个基准测一次过不了这关后续所有带立即数和地址的指令都会偏。4.2 BP 参与寻址时默认段不是 DS8B 6E 02 读错段现象解码 8B 6E 02有人按惯性给所有内存操作数都加上 DS 段前缀输出 MOV BP, DS:[BP02H]。原因是 6EH 的 mod01、reg101、rm110rm110 在 mod01 时表示 [BPd8]而 BP 默认段是 SS。解决解码器里维护一张默认段表rm 组合里出现 BP 或 SP 就标 SS否则标 DS。段的差异平时不报错但一旦涉及栈数据的读写段地址取错会直接导致调试结果错误这类坑比指令解错更隐蔽。4.3 mod01 的 8 位位移要符号扩展FEH 不是 254 而是 -2现象解码 8B 5E FE 得到 MOV BX, [BXFEH]地址计算时按无符号数把 FEH 当 254 加进去。原因是 mod01 的 disp8 在 16 位寻址中是带符号的。解决读取 disp8 后用 Python 的 struct 模块或手动补码转成有符号数例如 FEH 转成 -2输出 MOV BX, [BP-2] 或 MOV BX, SS:[BP-2]。验证时可以用 83 C0 FF 这条指令它能同时检验 imm8 的符号扩展FFH 必须输出 -1 而不是 255。4.4 reg 字段被征用的指令C7 /0 和 83 /0 的经典误读现象看到 C7 06 34 12 78 56把第二字节 06H 里的 reg000 当成 AX解码成 MOV AX, [1234H]后面字段全部错位。原因是 C7、81、83 这类指令的 reg 字段是扩展操作码手册里写成 /0表示必须为 000它不参与寄存器编号。解决先维护一张“扩展操作码指令表”遇到表中操作码就把 reg 当扩展位处理而不是直接进寄存器表。这张表应该包含 C7/0、81/0、83/0 等常见项后续遇到其他指令再逐步补充。4.5 16 位寻址不支持 [AX] 和 [CX]别套 32 位思维现象尝试解码形如 88 00 的指令时有人用 [AX]、[CX] 这类思路去猜寻址方式结果发现无论怎么组合都对不上。原因是 8086 的 16 位寻址只允许 BX、BP、SI、DI 四个寄存器以固定组合出现AX、CX、DX 不能当地址寄存器这与 32 位环境下的通用寻址规则差异很大。解决解析 ModRM 后先做合法性检查rm 组合必须是上一章那张表里的 8 种之一否则直接报无效 ModRM。88 00 的正确解码是 MOV [BXSI], AL其中 reg000 表示 ALrm000 表示 [BXSI]可以先拿这条自查。5. 让解码笔记反向工作三张对照表校验汇编器输出5.1 从解码到编码用一张对照表反查机器码解码能力练熟之后最实用的是反着用。把已经验证过的解码结果整理成三列对照表左边写汇编指令中间写机器码字节流右边写解码回读结果。调试时遇到汇编器生成的新字节序列先查表看是否在已知范围再手推一遍两边结果一致才敢继续向下读。汇编指令机器码解码回读MOV AX, 1234HB8 34 12MOV AX, 1234HMOV AX, [BX02H]8B 47 02MOV AX, [BX02H]MOV WORD PTR [1234H], 5678HC7 06 34 12 78 56MOV WORD PTR [1234H], 5678HADD AX, -183 C0 FFADD AX, -1这张表还有一个额外用途写汇编代码时先估算机器码长度再与汇编器实际输出比较。比如 MOV AX, 1234H 一定生成 3 字节MOV WORD PTR [1234H], 5678H 一定生成 6 字节。长度对不上说明某个字段被汇编器优化了常见的是把 81 C0 01 00 缩短成 83 C0 01这时用解码笔记反查一下就能确认是不是等价指令。5.2 指令长度预判调试器看门的快速算法调试器逐条执行时需要知道当前指令占用几个字节才能跳到下一条。我的习惯是拿解码笔记里的规则反过来做长度预判操作码需要 ModRM 的先看 ModRM 的 mod 值mod00 且 rm110 时基础长度 4mod01 时基础长度 3mod10 时基础长度 4再加立即数长度不需要 ModRM 的基础长度就是 1 加立即数长度。这个算法能在不完整解码的情况下迅速定位下一条指令多字节指令逐字节查看时非常省事。这段笔记写成之后我最大的教训是解码不是背操作码表而是把 mod、reg、rm 三层关系拆熟再配上符号扩展和小端序这两条底层规则。当年我被 83 C0 01 坑了一整晚原因就是没意识到短立即数要扩展。现在每解一条指令我都会先写字段拆解再查表最后用汇编器反向验证一遍。希望帮到你。本文还有配套的精品资源点击获取