
简介《个人总结的8086机器语言解码示例笔记》是一份面向汇编器编写者与x86底层学习者的专题笔记重点梳理8086机器指令从汇编语句到机器码的对应关系帮助读者理解编码规则并用于手写或校验汇编器实现。内容围绕指令格式、通用与段寄存器编号、五种寻址模式、操作码与立即数展开并说明字节、字、双字的操作数尺寸固定编码指令及双操作数指令中目的/源组合、d与w位、位移量与立即数字节占用等细节文中还以MOV的多类传送形式给出机器码示例便于对照分析最长可达8字节的指令构成。压缩包内为1个docx文档约1.94MB适合按知识点查阅和做编码速查。已有127人学习可作为8086指令解码、汇编器开发与底层调试的参考材料。1. 手写机器码这件事比想象中更考验耐心第一次把mov word [bxsi0x1BCD],0x1234手动编码成0C7 80 CD 1B 34 12的人大多会经历同一个阶段以为看懂指令格式就够了真动手写才发现 ModR/M 字节、d 位、w 位、位移量长度、立即数字节顺序任何一环错一点整条指令在调试器里就会变成完全不同的东西。这份 8086 机器语言解码笔记围绕自写汇编器时会遇到的编码规则展开覆盖 MOV、PUSH、POP、XCHG、IN、OUT、XLAT、LEA、LDS、LES、ADD 等指令的机器码级示例。它更适合正在手写 encoder 的读者不适合只想背汇编语法的人。2. 指令结构拆解6 字节指令背后的字段分布8086 指令长度并不固定笔记里提到了最长可达 8 字节的形式例如F0 3E 81 80 CD AB 34 12包含总线锁定前缀、段超越前缀、操作码、ModR/M、位移量和立即数。写汇编器时如果先假设指令长度固定后面几乎一定要返工。2.1 前缀、操作码、ModR/M、位移量与立即数指令可以按“前缀 操作码 寻址字节 位移 立即数”的顺序理解。笔记中的例子F0 3E 81 80 CD AB 34 12对应lock add word [ds:bxsi-0x5433],0x1234。其中F0是 LOCK 前缀3E是 DS 段超越前缀81是 ADD 的操作码80是 ModR/M 字节CD AB是 16 位位移量34 12是按小端序存放的立即数。这种拆分方式适合直接映射到汇编器的编码流程先判断是否需要前缀再生成操作码接着生成 ModR/M 和 SIB8086 没有 SIB只有 ModR/M 与位移最后追加立即数。每一步都可以单独写函数也方便做单元测试。2.2 长度判断规则笔记强调当 disp 是字时位移量占 2 字节立即数为 16 位时占 2 字节否则占 1 字节。这个规则看起来简单但实现时容易在“根据操作数大小决定 w 位”和“根据 w 位决定立即数长度”之间绕晕。稳妥的做法是先生成 ModR/M 和位移再根据操作数类型决定立即数长度最后统一拼装字节流。3. 寄存器编号与 ModR/M 编码汇编器 encoder 的核心8086 的寄存器编号和 ModR/M 字节是机器码编码里最容易写错的部分。笔记把通用寄存器、段寄存器、8 位寄存器的编号和 ModR/M 的 mod、reg、r/m 字段拆开列出适合直接转成表驱动实现。3.1 16 位寄存器与 8 位寄存器编号16 位通用寄存器 AX、CX、DX、BX、SP、BP、SI、DI 按顺序编号 0 到 78 位寄存器 AL、CL、DL、BL、AH、CH、DH、BH 也按类似顺序编号。段寄存器 ES、CS、SS、DS 的编号分别是 0、1、2、3。写 encoder 时可以把这些编号放进数组或字典通过寄存器名查编号。REG16 { ax: 0, cx: 1, dx: 2, bx: 3, sp: 4, bp: 5, si: 6, di: 7, } REG8 { al: 0, cl: 1, dl: 2, bl: 3, ah: 4, ch: 5, dh: 6, bh: 7, } SEG {es: 0, cs: 1, ss: 2, ds: 3} # 编码时根据操作数类型选择寄存器表 def reg_code(name, width): table REG16 if width 16 else REG8 return table[name.lower()]这个表结构简单但要注意 8 位寄存器高半部分 AH、CH、DH、BH 的编号是 4 到 7和 16 位寄存器编号不是同一套语义。写错会导致mov ah, 0x12被编码成别的寄存器。3.2 ModR/M 字节拆分ModR/M 字节由 mod2 位、reg3 位、r/m3 位组成。笔记中大量使用二进制字面量展示字段比如10_000_000b表示 mod2、reg0、r/m0。对于mov ax, [bxsi0x1234]reg 字段表示目的寄存器 AXr/m0 表示[bxsi]mod2 表示后面跟 16 位位移。def modrm(mod, reg, rm): return ((mod 0x3) 6) | ((reg 0x7) 3) | (rm 0x7) # mov ax, [bxsi0x1234] # mod2, reg0(AX), rm0(bxsi) b modrm(2, 0, 0) # 0x80 # 后面再追加位移 0x34, 0x12实现时要特别注意 mod0 且 r/m6 表示直接地址不是[bp]。8086 的寻址模式里[bp]必须用 mod1、位移 0 表示。这个特殊规则在很多简易汇编器里会被漏掉导致mov ax, [bp]编码错误。3.3 d 位与 w 位双操作数指令里d 位表示 reg 字段是目的还是源w 位表示字节操作还是字操作。笔记里用000000_1_1b这种形式展示 ADD 指令的编码其中倒数第二位是 d最后一位是 w。写 encoder 时可以把 d 和 w 作为参数传入避免手写拼接。def opcode_with_dw(base, d, w): return base | ((d 1) 1) | (w 1) # add ax, [bxsi0x1234] # 操作码 000000d1w1 - 0x03 op opcode_with_dw(0x00, 1, 1)这种写法比直接写0x03更直观也方便批量生成指令。4. 典型指令编码MOV、PUSH、POP 与 XCHG笔记里 MOV 的示例最多也最能体现 8086 编码的多样性。MOV 至少有短指令格式、长指令格式、段寄存器专用格式、特定地址内存格式等多种形式写汇编器时需要按操作数组合分支处理。4.1 MOV 的短格式与长格式mov bx, 0x1234可以用短指令格式1011_1_011b, 0x34, 0x12也就是BB 34 12而mov word ax, 0x1234用长指令格式0C7 11_000_000b 0x34 0x12也就是C7 C0 34 12。两种格式都能表达常数到寄存器的传送但短格式只适用于 BX 等部分寄存器长格式通用性更强。def mov_reg_imm16(reg_name, imm): code REG16[reg_name] # 短格式1011 1 rrr只适用于 AX/CX/DX/BX if code 3: return bytes([0xB8 | code, imm 0xFF, (imm 8) 0xFF]) # 长格式C7 /0 return bytes([0xC7, modrm(3, 0, code), imm 0xFF, (imm 8) 0xFF])短格式的判断条件不是所有 16 位寄存器都能用SP、BP、SI、DI 不能用B8reg这种短格式。笔记里mov bx, 0x1234的示例对应BB说明 BX 编号 3 加上0xB8得到0xBB。4.2 段寄存器传送段寄存器传送使用8E和8C操作码。mov es, ax编码为8E C0mov ax, es编码为8C C0。笔记里列出了mov cs, ax的编码但 8086 实际上不允许直接向 CS 传送数据写汇编器时应对 CS 做特殊处理或直接报错。def mov_seg_reg(dst, src): if dst cs: raise ValueError(cannot mov to cs on 8086) return bytes([0x8E, modrm(3, SEG[dst], REG16[src])])这类限制在笔记里有示例但没有展开说明实现时需要结合 8086 的硬件约束补上。4.3 PUSH 与 POPPUSH 和 POP 的操作数总是 16 位。PUSH 有内存形式FF /6、通用寄存器长格式FF /6和短格式50reg段寄存器形式06、0E、16、1E。POP 对应8F /0、58reg和段寄存器形式07、17、1F。def push_reg(name): code REG16[name] # 短格式 01010 rrr return bytes([0x50 | code]) def push_mem(mod, rm, disp0): # FF /6 return bytes([0xFF, modrm(mod, 6, rm)]) encode_disp(disp)写 PUSH 时要注意短格式只适用于 16 位通用寄存器内存操作数必须走FF /6。POP 的内存形式是8F /0不是/6这两个字段很容易记混。4.4 XCHG 与短格式互换xchg ax, dx可以用短格式10010_010b也就是92xchg cx, dx用通用格式87 D1。短格式只适用于 AX 与其它寄存器交换写 encoder 时可以优先尝试短格式失败再回退到通用格式。def xchg_reg_reg(a, b): if a ax and b ! ax: return bytes([0x90 | REG16[b]]) if b ax and a ! ax: return bytes([0x90 | REG16[a]]) return bytes([0x87, modrm(3, REG16[a], REG16[b])])xchg ax, ax编码为90也就是 NOP。这个细节在优化或生成指令时有用。5. 避坑与排查编码器调试时最容易翻车的几处5.1 位移量长度判断错误现象mov ax, [bxsi0x12]被编码成 4 字节而不是 5 字节反汇编结果偏移。原因是 mod 字段选择了 16 位位移但实际位移可以用 8 位表示。8086 的 mod1 表示 8 位位移mod2 表示 16 位位移。解决方法是根据位移范围选择 mod位移为 0 且 r/m 不是 6 时用 mod0位移在 -128 到 127 时用 mod1否则用 mod2。5.2 立即数字节序写反现象mov ax, 0x1234编码成B8 12 34执行后 AX 变成0x3412。原因是 8086 是小端序低字节在前。解决方法是统一用imm 0xFF和(imm 8) 0xFF生成字节不要直接按十六进制字符串拆分。5.3 忘记处理[bp]的特殊编码现象mov ax, [bp]编码成8B 06实际被解释成mov ax, [0x0006]。原因是 mod0、r/m6 表示直接地址不是[bp]。解决方法是遇到[bp]时强制使用 mod1、位移 0编码为8B 46 00。5.4 段寄存器方向写反现象mov es, ax被编码成8C C0实际执行成mov ax, es。原因是8E是“到段寄存器”8C是“从段寄存器”。解决方法是把方向作为独立参数不要靠操作码隐式判断。5.5 汇编器版本不识别分组二进制常量笔记里提到nasm-0.98不识别11_000_000b这种分组二进制常量需要用nasm 2.10.04或更新版本。写测试用例时如果遇到语法报错先检查汇编器版本不要急着改编码逻辑。6. 二次开发把笔记变成可测试的 encoder这份笔记的价值不在“背编码”而在“把编码规则变成可验证的代码”。我一般会先写一个最小的 encoder 框架把寄存器表、ModR/M 生成、位移编码、立即数编码拆成独立函数再用笔记里的示例做对照测试。def encode_mov(dst, src): # 只处理寄存器到寄存器、立即数到寄存器两种简单情况 if dst in REG16 and src in REG16: return bytes([0x8B, modrm(3, REG16[dst], REG16[src])]) if dst in REG16 and isinstance(src, int): code REG16[dst] if code 3: return bytes([0xB8 | code, src 0xFF, (src 8) 0xFF]) return bytes([0xC7, modrm(3, 0, code), src 0xFF, (src 8) 0xFF]) raise NotImplementedError验证方法很直接把笔记里的每条db示例写成一个测试用例编码结果和预期字节流逐字节比较。比如mov word [bxsi0x1BCD],0x1234预期是C7 80 CD 1B 34 12push ax预期是50pop es预期是07。测试跑通之后再把内存寻址、段超越前缀、LOCK 前缀逐步加进去。改造时建议先支持 8086 的 16 位寻址模式不要一上来就做 32 位。8086 没有 SIB 字节ModR/M 的 r/m 字段直接对应[bxsi]、[bxdi]、[bpsi]、[bpdi]、[si]、[di]、[bp]、[bx]这几种组合先把这张表写死反而更稳。等 16 位 encoder 稳定后再考虑扩展到 32 位寻址和更多指令。从那以后我每次写指令编码器都会先把笔记里的示例整理成字节级测试集再动 encoder 代码。没有这层对照调试器里看到的错误指令根本不知道该从哪里查。希望这份拆解能帮到正在写 8086 汇编器或机器码解码工具的你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。