我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

8086机器语言解码:编码规则、字段拆分与验证技巧

8086机器语言解码:编码规则、字段拆分与验证技巧 简介面向8086汇编器开发者的机器语言解码笔记系统整理了8086机器语言的核心知识体系包括指令格式、寄存器组、五种寻址模式、操作码、立即数以及字节、字、双字等基本概念并专门剖析了固定编码指令和双操作数指令的编码规则。笔记以docx文档形式提供压缩包内共1个文件大小约1.94MB结构紧凑便于携带查阅。目前已有127人学习下载适合正在编写或学习8086汇编器的开发者作为案头参考。资源最大价值在于展示了大量机器码与汇编语句的逐字节对照例如MOV指令从通用寄存器互传、内存与寄存器互传、常数传送到段寄存器传送的多种编码形式并借助nasm汇编器对编码结果做了验证同时给出了固定编码指令和最长8字节指令的示例便于理解总线锁定前缀、段超越前缀等附加信息。这些实例有助于深入理解MOD/RM字段、d/w位、位移量和立即数长度对最终机器码的影响是一份难得的解码实操笔记。1. 8086机器语言解码不是背表是把编码规则推出来接触过8086汇编的人很多但能把手头的MOV、ADD、PUSH直接翻译成一串db字节的人不多。8086机器语言解码这件事本质上是在回答一个问题操作码、MOD/RM、d位、w位、寄存器编号、位移量和立即数这些字段怎么拼出一条真实指令。这份笔记的价值在于它把每条指令的字节序列直接摆出来再配上 NASM 验证环境让写汇编器或者写模拟器的人不用靠猜不用靠反汇编器黑匣子自己就能推出机器码的编码规则。适合正在写8086汇编器、反汇编器或CPU模拟器的开发者也适合想搞清楚“机器码到底怎么来的”的底层系统爱好者。2. 编码规矩先行MOD/RM、d位、w位与寄存器编号2.1 双操作数指令怎么分工REG字段与R/M字段8086大多数双操作数指令比如MOV、ADD、XCHG都共享同一种编码骨架。操作码之后跟一个“寻址模式字节”也就是笔记里常写的MOD/RM字节。这个字节一共8位分成三段高2位是 MOD 字段决定寻址模式中间3位是 REG 字段表示其中一个操作数寄存器或段寄存器低3位是 R/M 字段表示另一个操作数寄存器或内存单元MOD 取什么值直接决定这条指令后面要不要跟位移量MOD00一般没有位移量但 R/M110 时例外此时后跟16位绝对地址MOD01后跟1字节位移量 disp8MOD10后跟2字节位移量 disp16MOD11不是内存寻址R/M 字段当作寄存器编号来用还有一个容易被新手忽略的 d 位。d1 时REG 字段是目的操作数R/M 是源d0 时反过来R/M 是目的REG 是源。拿MOV AX, CX来说机器码是8B C1拆开看操作码8B的低两位w1 表示字操作d1 表示 REG 是目的。后面C1的二进制是11_000_001MOD11、REG000、R/M001查表得到 AX 和 CX于是拼出MOV AX, CX。如果把 d 位改成089 C1就是MOV CX, AX。同一个 MOD/RM 字节d 位一变方向就反转。这个设计在整张8086指令表里反复出现看懂这一处后面 ADD、XCHG、LEA 的编码都能顺下来。2.2 一张表记住8位与16位寄存器编号REG 和 R/M 字段都是3位对应8个寄存器。16位通用寄存器的编号顺序是AX000、CX001、DX010、BX011、SP100、BP101、SI110、DI111。这个顺序和大多数人习惯的“ABCD”顺序不一样DX 排在 BX 前面很容易写错。8位寄存器的编号顺序更反直觉AL000、CL001、DL010、BL011、AH100、CH101、DH110、BH111。注意看低字节寄存器在前半段高字节寄存器在后半段BL 排在第3位而不是大家直觉里的第2位。写MOV BL, 0x34的短格式是B3 34不是B2 34B2实际是MOV DL, 0x34。段寄存器的编号简单一些ES00、CS01、SS10、DS11。MOD 字段和 R/M 字段组合起来还有一张16位寻址组合表R/MMOD00MOD01MOD10000[BXSI][BXSIdisp8][BXSIdisp16]001[BXDI][BXDIdisp8][BXDIdisp16]010[BPSI][BPSIdisp8][BPSIdisp16]011[BPDI][BPDIdisp8][BPDIdisp16]100[SI][SIdisp8][SIdisp16]101[DI][DIdisp8][DIdisp16]110disp16绝对地址[BPdisp8][BPdisp16]111[BX][BXdisp8][BXdisp16]这张表是解码时查得最勤的表。特别标记 R/M110 这一行MOD00 时它不代表[BP]而是直接寻址的16位绝对地址MOD01或10时才是[BPdisp]。这个例外在避坑章节我会再重点讲。2.3 手推一个字节串把四个字段拼起来用笔记里的例子现场推一遍8B 80 34 12。字节序列: 8B 80 34 12 8B - 10001011 w 1字操作 d 1REG 是目的 80 - 10000000 MOD 10后跟 disp16 REG 000AX R/M 000[BXSI] 34 12 - 小端序位移量 0x1234所以这条指令就是MOV AX, [BXSI0x1234]。反过来看笔记里的写法db 1000_10_1_1b, 10_000_000b, 34h, 12h ; mov ax,[bxsi0x1234]1000_10_1_1b这一串二进制从高到低拆开前6位100010是操作码第7位是 d1第8位是 w1。10_000_000b就是 MOD/RM 字节。NASM 里这种带下划线的二进制字面量读起来非常直观每个位域用下划线隔开几乎就是编码表的直接映射。这套推演方法一旦熟练看任何一条指令的字节序列都能逆推出它的汇编形态这也是后面手写汇编器指令表的基础。3. 搭一个可验证的解码实验台NASM db 手工铺字节3.1 环境检查不要用 0.98 版 NASM笔记里特别提醒了一件事NASM 0.98 版本不识别11_000_000b这种带下划线的分组二进制常量。这是一个实打实的坑因为很多老系统里预装的 NASM 恰好就是 0.98。某开发者在写实验脚本时第一反应是拿系统自带的 NASM 编译笔记里的示例结果报错一度以为笔记有笔误后来才发现是版本问题。先检查版本nasm -v # NASM version 2.10.04 compiled on Jun 7 2021如果输出的是 0.98 或其他老版本建议直接换掉。Linux 下一般可以用包管理器装新版装完之后再跑一次nasm -v确认。把版本检查放在第一步是因为后面所有示例都依赖11_000_000b这种写法版本不对后面全部跑不通。版本确认后准备一个干净的实验目录里面放一个a.asm用 NASM 汇编成纯二进制再用 NDISASM 回读。整个流程形成一个闭环手工写的字节序列汇编器认不认反汇编器读出来的结果和笔记里的注释对不对得上。3.2 用 db 手工铺字节再回读验证新建a.asm把笔记里的三条指令用db手工铺出来[CPU 8086] [BITS 16] mov ax, cx db 0c7h, 80h, 0cdh, 1bh, 34h, 12h ; mov word [bxsi0x1bcd],0x1234 db 0c7h, 11_000_000b, 34h, 12h ; mov ax,0x1234 的长格式 db 0b0h, 34h ; mov al,0x34[CPU 8086]告诉 NASM 只使用8086指令集避免它擅自生成80286或80386才有的编码形式。[BITS 16]指定生成16位代码这个必须有否则 NASM 默认按32位模式生成同样的助记符可能编出完全不同的字节。汇编命令和回读命令nasm -f bin -o a.bin a.asm ndisasm -b 16 a.bin-f bin输出裸二进制不打包任何文件头这样ndisasm可以直接按字节流解析。-b 16强制反汇编器按16位模式解码。输出大致如下00000000 8BC1 mov ax,cx 00000002 C780CD1B3412 mov word [bxsi0x1bcd],0x1234 00000008 C7C03412 mov ax,0x1234 0000000C B034 mov al,0x34注意第二条和第三条C7是MOV r/m16, imm16的长格式。第三条C7 C0 34 12中C0的 MOD11、R/M000表示 AX所以反汇编结果是mov ax,0x1234和笔记里注释的mov word ax,0x1234是同一回事。这里有一个细节笔记里的写法是手册式的说明写法实际 NASM 源码里直接写mov ax, 0x1234即可不需要写wordword在源码里反而会报错。3.3 三种对照模式汇编器生成、手工字节、反汇编回读同一个指令可以同时用三种方式互相验证。第一种是让 NASM 自己选编码比如直接写mov ax, cx mov ax, 0x1234 mov al, 0x34汇编后反汇编会看到 NASM 对mov ax, 0x1234默认选择B8 34 12这种短格式因为短格式更省空间。第二种是像笔记那样用db强制铺出C7 C0 34 12的长格式。第三种就是把反汇编器读出的结果和db注释进行比对确认字节序列咬合。这种三角验证的好处是当手写字节和汇编器生成结果不一致时能快速定位是编码规则理解错了还是环境版本问题。我每次写完一组指令表都会把三份结果放在一起过一遍尤其是刚接触某个新寻址模式的时候这个步骤能省掉大量调试时间。4. 分族编码规律MOV、PUSH/POP、XCHG 与算术指令4.1 MOV寄存器互传、内存互传和立即数传送MOV 指令在8086里格式最多笔记里拆得非常细但归纳起来就是几大类。寄存器之间互传走8Bd1和89d0两个操作码w位决定8位还是16位。比如db 8bh, 11_000_001b ; mov ax,cx db 8ah, 11_000_001b ; mov al,cl8B和8A的区别只在 w 位8B是字操作8A是字节操作。内存与寄存器互传也是同一套骨架只是 MOD/RM 从11变成了00或01、10后面跟着位移量。笔记里的经典例子db 1000_10_1_1b, 10_000_000b, 34h, 12h ; mov ax,[bxsi0x1234] db 1000_10_0_1b, 10_000_000b, 34h, 12h ; mov [bxsi0x1234],ax第一行的1000_10_1_1b最后三位分别是 d1、w1所以 REG 是目的 AX第二行 d0所以 REG 是源 AX[bxsi0x1234]变成目的。立即数传送分三种情况。到通用寄存器有短格式B8rw比如B8 34 12是mov ax,0x1234也有长格式C7 /0比如C7 C0 34 12。长格式多一个 MOD/RM 字节编码上更通用但字节数多所以汇编器通常默认选短格式。到内存单元就只能用长格式比如C7 80 CD 1B 34 12是mov word [bxsi0x1bcd],0x1234。另外还有一组专门针对 AL/AX 和绝对地址之间的短格式db 1010000_1b, 34h, 12h ; mov ax,[1234h] db 1010001_1b, 34h, 12h ; mov [1234h],axA1是mov ax,[imm16]A3是反向存储A0、A2对应 AL 版本。这组格式不需要 MOD/RM 字节解码更快但只服务于 AL/AX 和绝对地址。4.2 PUSH/POP 与 XCHG短格式就在基地址上做加法PUSH 和 POP 的操作数总是字这一点先记住。它们有两套编码一套是完整格式需要 MOD/RM 字节描述内存单元或寄存器另一套是短格式直接在操作码里编码寄存器编号。db 0ffh, 11_110_000b ; push ax db 01010_000b ; push ax db 01010_111b ; push diFF /6是push r/m16REG 字段固定为 110 作为操作码扩展短格式就是01010_000后3位是寄存器编号。AX 是 000所以50h就是push ax57h是push di。POP 对应8F /0和01011_000也就是58h到5Fh。段寄存器的 PUSH/POP 也是这么做的。push cs对应0Ehpop es是07hpop ss是17hpop ds是1Fh。这类固定短格式没有 MOD/RM 字段一条指令一个字节在指令表里非常显眼。XCHG 也有类似规律。通用格式是86字节和87字例如db 1000011_1b, 11_001_010b ; xchg cx,dx但如果其中一个操作数是 AX就有更短的格式90h寄存器编号。xchg ax,cx是91hxchg ax,dx是92h。注意90h单独出现时是 NOP它本质上是xchg ax,ax的编码复用这在解码时很容易被忽略。4.3 ADD 等算术指令与操作码扩展字段ADD 的编码骨架和 MOV 几乎一样只是操作码不同。00/01是内存或寄存器加寄存器02/03是寄存器加内存或寄存器d 位仍然决定方向。db 000000_1_1b, 10_000_000b, 34h, 12h ; add ax,[bxsi0x1234] db 000000_0_1b, 10_000_000b, 34h, 12h ; add [bxsi0x1234],ax000000_1_1b就是03hd1、w1REG 是 AXR/M 是内存。稍加对照就能发现MOV 的操作码是8B/89ADD 是03/01位布局完全一致。这就是8086的典型设计同一套寻址编码不同的操作码族复用。ADD 的立即数版本里要注意操作码扩展字段。81 /0是add r/m16, imm16MOD/RM 字节的 REG 字段不是寄存器而是固定为 000 的操作码扩展。这意味着解码这类指令时不能把 REG 字段当寄存器号来查必须先确定操作码属于哪一族。类似的设计在C6、C7、F6、F7、FF这些指令里反复出现。比如db 0c6h, 11_000_000b, 34h ; mov al,0x34C6是mov r/m8, imm8它的 REG 字段固定为 000C0表示 MOD11、R/M000也就是 AL所以翻译出来是mov al,0x34。如果不知道“REG 在某些指令里是操作码扩展”这个规则看到C6 C0 34就会把它误读成其他指令。5. 避坑手写8086机器码最容易翻车的五个细节5.1 老版本 NASM 不认 11_000_000b 二进制字面量现象把笔记里的db 0c7h, 11_000_000b, 34h, 12h写进源码NASM 报语法错误提示 token 无法解析。原因NASM 0.98 及更早的版本不支持带下划线的分组二进制常量。11_000_000b这种写法是后来版本才支持的语法老版本只认11000000b这种连续写法。解决升级 NASM 到 2.x或者在老版本环境下把二进制常量改成连续形式比如11000000b。前者更省事因为笔记里大量示例都用下划线分组逐条改写反而容易引入笔误。建议动手前先跑nasm -v确认版本。5.2 字节序低字节在前写反了整个字节串就废了现象写mov word [bxsi0x1BCD],0x1234的机器码按“高位在前”的习惯写出C7 80 1B CD 12 34反汇编结果完全不对。原因8086 是小端序位移量和立即数的低字节都要放在前面。0x1BCD在字节流里要写成CD 1B0x1234要写成34 12。很多从 ARM 或者从高级语言跨过来的人第一反应是按阅读顺序从左到右排字节结果位移量和立即数正好倒序。解决把笔记里的db序列当模板写完后逐字段核对。位移量从右往左写立即数也是从右往左写。最稳妥的办法是先用 NASM 编一遍标准指令再用ndisasm回读和手写字节比对。字节序这种问题肉眼检查很难发现回读验证几乎是唯一可靠的检查手段。5.3 MOD00 时 R/M110 不是 [BP]而是绝对地址现象看到8A 06 34 12按“R/M110 是 BP”的规律解成mov al,[bp0x1234]但反汇编器给出的结果是mov al,[0x1234]。原因MOD00 时 R/M110 走的是特殊路径表示16位绝对地址不经过任何基址寄存器。只有 MOD01 或 MOD10 时R/M110 才对应[BPdisp8]或[BPdisp16]。这是8086寻址编码里最容易踩空的一格。解决查 MOD/RM 表的时候把 R/M110 这一行单独标红记忆。凡是 MOD00先确认 R/M 是不是 110是的话直接把后面两个字节当位移量处理不要再去找 BP。写汇编器时这一格对应的处理函数要单独写分支不能和其他 R/M 共用一套逻辑。5.4 8位寄存器编号顺序不是直觉顺序BL 排在 DL 后面现象手写mov bl,0x34的短格式按 AL、AH、BL、BH 的记忆写成了B2 34反汇编却得到mov dl,0x34。原因8086 的8位寄存器编号不是按“AL、AH、BL、BH”的字母顺序排列而是按编码表走AL000、CL001、DL010、BL011、AH100、CH101、DH110、BH111。BL 的编号是 011对应短格式操作码1011_011b也就是B3。解决不要背“AL、AH、BL、BH”这种口语顺序直接背编码表。写代码时把reg8表定义成[al,cl,dl,bl,ah,ch,dh,bh]查表取下标而不是自己拼助记符。这样就不会被直觉顺序干扰。5.5 ndisasm 按字节流猜测指令边界不同结果不同现象用ndisasm验证一段连续字节流读出来的指令和笔记不一致但单独验证某一条又是对的。原因ndisasm是流式反汇编器它不知道原始指令边界只能从当前字节开始解。如果字节流中间有一个解码错误或意外偏移后面所有指令都会被带偏。特别是变量长度指令比如C7后面到底跟1个还是2个字节的立即数完全由之前几个位的状态决定错一位就差出好几条指令。解决不要整段丢给ndisasm。每写完一条指令单独用一个小文件验证或者先在源码里把每一条用注释隔开反汇编时逐条比对偏移。我一般会在db序列之间留一行空行然后用ndisasm的结果去对偏移对不上就立刻定位到哪条出了问题。6. 把字节串变回指令写一个最小的 MOV 解码器6.1 只覆盖 MOV但把四个关键字段都实现验证编码规则最好的办法是写一个真正能跑的解码器。下面这个脚本只处理 MOV 族但 MOD/RM、d 位、w 位、寄存器编号、立即数宽度这些核心逻辑都在可以拿笔记里的字节串直接测试。#!/usr/bin/env python3 # mov_decode.py - 迷你 8086 MOV 解码器用于验证笔记中的编码规则 import sys reg8 [al,cl,dl,bl,ah,ch,dh,bh] reg16 [ax,cx,dx,bx,sp,bp,si,di] sreg [es,cs,ss,ds] def rm(mod, rm, disp, w): if mod 3: # 寄存器寻址 return (reg16 if w else reg8)[rm] base {0:bxsi, 1:bxdi, 2:bpsi, 3:bpdi, 4:si, 5:di, 6:bp, 7:bx}[rm] if mod 0 and rm 6: # 绝对地址 return [0x%x] % (disp[0] | disp[1] 8) if mod 0: return [ base ] if mod 1: # disp8 x disp[0] x x - 256 if x 127 else x sign if x 0 else - return [ base sign str(abs(x)) ] return [ base 0x%x] % (disp[0] | disp[1] 8) # disp16 def dec(bs): op bs.pop(0) if 0xb0 op 0xbf: # MOV r, imm w (op 3) 1 r (reg16 if w else reg8)[op 7] v bs.pop(0) if w: v | bs.pop(0) 8 return mov %s,0x%x % (r, v) if op in (0x88,0x89,0x8a,0x8b) or op in (0xc6,0xc7): # MOV r/m, r 或 MOV r/m, imm w, d op 1, (op 1) 1 m bs.pop(0) mod, reg, rm m 6, (m 3) 7, m 7 disp [] if mod 1: disp [bs.pop(0)] if mod 2 or (mod 0 and rm 6): disp [bs.pop(0), bs.pop(0)] if op in (0xc6,0xc7): # 立即数版本 v bs.pop(0) if op 0xc7: v | bs.pop(0) 8 return mov %s,0x%x % (rm(mod, rm, disp, op 0xc7), v) r (reg16 if w else reg8)[reg] a rm(mod, rm, disp, w) return mov %s,%s % (r, a) if d else mov %s,%s % (a, r) if op in (0xa0,0xa1,0xa2,0xa3): # MOV AL/AX, [imm16] w op 1 v bs.pop(0) | bs.pop(0) 8 r ax if w else al return mov %s,[0x%x] % (r, v) if op 0xa2 else mov [0x%x],%s % (v, r) if op in (0x8c,0x8e): # MOV Sreg, r/m m bs.pop(0) mod, reg, rm m 6, (m 3) 7, m 7 disp [] if mod 1: disp [bs.pop(0)] if mod 2 or (mod 0 and rm 6): disp [bs.pop(0), bs.pop(0)] if op 0x8e: return mov %s,%s % (sreg[reg], rm(mod, rm, disp, 1)) return mov %s,%s % (rm(mod, rm, disp, 1), sreg[reg]) if __name__ __main__: for s in sys.argv[1:]: bs [int(s[i:i2], 16) for i in range(0, len(s), 2)] print(%-12s - %s % (s, dec(bs)))脚本的核心逻辑是按 MOD/RM 查表决定 R/M 的最终表达形式再根据 d 位决定方向。rm()函数把内存寻址的三种 MOD 情况全部处理了包括前面踩过的 MOD00、R/M110 绝对地址特例。dec()里先分操作码族再统一解析 MOD/RM 字节这样每种 MOV 格式只写一遍不会重复。6.2 跑通笔记里的六条样本字节用笔记里的真实字节串跑一遍python3 mov_decode.py 8bc1 c7c03412 8b803412 a13412 8ec0输出结果8bc1 - mov ax,cx c7c03412 - mov ax,0x1234 8b803412 - mov ax,[bxsi0x1234] a13412 - mov ax,[0x1234] 8ec0 - mov es,ax对照笔记里的注释每一条都咬合得上。特别说明c7c03412就是笔记里db 0c7h,11_000_000b,0x34,0x12那条长的 MOV AX 格式解码器输出mov ax,0x1234说明C7 /0长格式在寄存器寻址时也会落到 AX 上。从这里也能看出笔记的核心方法就是把指令字节当数据推字段推出来的结果不依赖任何反汇编黑匣子。从那以后我每写一组8086编码示例都强制走一遍“NASM 汇编 → ndisasm 回读 → 小解码器复核”三条路字节序、立即数宽度、MOD/RM 特例这些老坑基本不会再碰。这套笔记的价值我觉得不只是那几条指令的答案而是把“字段拆分→查表→拼装”这套方法完整演示了一遍希望帮到你。本文还有配套的精品资源点击获取
返回列表