恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
8086机器语言解码实战:从MODRM到MOV指令编码全解析
首页
资讯中心
/
8086机器语言解码实战:从MODRM到MOV指令编码全解析
8086机器语言解码实战:从MODRM到MOV指令编码全解析
发布时间:2026/10/11 21:18:26
简介这是一份个人总结的8086机器语言解码示例笔记面向正在编写8086汇编器、反汇编器或希望从机器码层面深入理解x86指令编码的开发者。笔记系统梳理了指令格式、16位通用寄存器编号、寻址模式、操作码、立即数、字节/字/双字等基础概念重点讲解固定编码指令与双操作数指令编码规则并覆盖d/w位、MOD与R/M组合关系等关键细节MOV指令部分给出了寄存器互传、内存与寄存器互传、立即数传送、段寄存器传送等多种机器码示例如mov word [bxsi0x1BCD],0x1234对应0C7h 80h 0CDh 1Bh 34h 12h便于对照学习。笔记还整理了固定编码的1字节指令如AAA并提示NASM汇编验证时的版本兼容问题帮助避免踩坑。整份资源打包为一个docx文档压缩包仅1.94MB已有127人学习适合需要快速掌握8086机器码编码规律并落实到汇编器编写中的技术人员查阅参考。1. 8086 机器语言解码笔记从一条 mov 反推出六个机器字节做汇编器、调试器或者逆向 16 位启动代码的人早晚会卡在同一道坎上手头只有 Intel 手册里那些 mod/reg/rm 抽象规则真到了要把mov word [bxsi0x1BCD],0x1234写成0xC7 0x80 0xCD 0x1B 0x34 0x12这六个字节时方向位、字节序、位移长度任何一个记错机器码就全错。这份 8086 机器语言解码笔记的价值在于它把 MOV、ADD、PUSH、POP、XCHG、IN/OUT 这些常用指令从操作码到 modrm 逐字节拆开每条都给了等价的db伪指令写法还能用 NASM 编译后做二进制对照。适合三类人写汇编器或者反汇编器需要查编码表的人研究 x86 启动流程想读懂原始字节的人以及被 16 位寻址模式绕晕的学生。2. 编码骨架MOD/REG/R/M 三位与 W/D 位怎么决定一条指令的形态2.1 一条 8086 机器指令由几块拼成8086 的机器指令不像 ARM 那样定长它是可变长的最短 1 字节比如AAA就是固定的0x37最长能到 8 字节。拆开看一条完整的指令通常由这几块组成操作码opcode在最前面告诉 CPU 这条指令要干什么寻址模式字节modrm紧跟其后描述操作数在哪再往后是位移量disp和立即数imm这两块不一定都有有的话注意长度为 8 位或 16 位。还有一类前缀字节比如总线锁定前缀F0h、段超越前缀3Eh它们出现在整条指令的最前面不参与操作数编码。笔记里那个最长的例子很有代表性F0 3E 81 80 CD AB 34 12 lock add word [ds:bxsi-0x5433],0x1234这里F0是 lock 前缀3E是 DS 段超越前缀81是 ADD r/m16,imm16 的操作码80是 modrm 字节CD AB是 16 位位移量小端序实际值是 0xABCD按有符号数解读就是 -0x543334 12是 16 位立即数小端序实际值 0x1234。数一下正好 8 字节一条指令把前缀、操作码、寻址模式、偏移常量、立即数全占了。理解这个骨架是解码的前提。所谓解码就是拿到一串字节后先认前缀再认操作码然后看 modrm 决定操作数最后按长度切出位移和立即数。反过来编码就是把这个过程倒着做一遍。2.2 MOD 字段与 R/M 字段操作数的地址怎么算modrm 字节是整个 8086 编码体系里最绕的部分它只有 8 位却要同时表达三样信息高位 2 位是 MOD寻址模式中间 3 位是 REG/分组号低位 3 位是 R/M寄存器或内存操作数。MOD 两位的含义如下MOD含义位移量00无位移或直接地址0 字节018 位位移量需符号扩展1 字节1016 位位移量2 字节11操作数是寄存器而非内存无当 MOD 不是 11 时R/M 三位决定基址与变址的组合方式这是 8086 特有的「基址 变址 位移」寻址R/M有效地址MOD00/01/10000[bxsi]disp001[bxdi]disp010[bpsi]disp011[bpdi]disp100[si]disp101[di]disp110[bp]dispMOD00 时变为直接地址 disp16111[bx]disp注意最后一行 R/M110 的例外当 MOD00 时它不表示[bp]而是表示直接地址也叫做绝对地址位移量固定 16 位。这是 8086 编码里最容易被忽略的一个旁路写汇编器时 mod00、r/m110 组合要单独处理成内存直接寻址。当 MOD11 时R/M 不再表示内存而是直接对应寄存器编号。16 位通用寄存器编号是 AX000、CX001、DX010、BX011、SP100、BP101、SI110、DI1118 位寄存器编号是 AL000、CL001、DL010、BL011、AH100、CH101、DH110、BH111。2.3 W 位、D 位与方向三个 bit 决定指令形态除了 modrm操作码自身还藏着两个关键位。W 位决定操作宽度W1 是字操作16 位W0 是字节操作8 位。D 位决定方向D1 时 REG 字段表示目的操作数内存或 R/M 表示源D0 时反过来REG 是源R/M 是目的。这两个位不是独立出现的它们以 bit 形式嵌在操作码里。拿 MOV 举例100010dw是通用寄存器与内存/寄存器互传的基本模板8Bh10001011b D1、W1REG 是目的89h10001001b D0、W1REG 是源8Ah10001010b D1、W0字节操作88h10001000b D0、W0笔记里给了这么一对写法db 8bh,11_000_001b ; mov ax,cx db 8ah,11_000_001b ; mov al,cl第一个8B的 D/W 位是 11REG001CXR/M000AXD1 所以结果是mov ax,cx。第二个8A的 D/W 位是 10REG001CLR/M000AL结果mov al,cl。同一份 modrm操作码差一个 bit操作数宽度和方向全变了。我一般建议初学者先背规则而不是背编码看操作码最后两位倒数第二位是 D最后一位是 W。D1 时先查 REG 字段是哪个寄存器再按 MOD/R/M 找另一个操作数方向是从 REG 到 M/RD0 时反着读。这个习惯养成后后面解 ADD、SUB、CMP 这些同族指令会快很多。2.4 双操作数指令的编码约束与手工解码实例双操作数指令有几个硬性约束笔记里罗列得很清楚目的和源不能同时是内存单元当位移量是字时占 2 字节立即数是 16 位时同样占 2 字节D1 时 REG 是目的源由 MOD 与 R/M 组合决定W1 表示字操作否则是字节操作。拿mov ax,cx反推机器码就是8B加0xC1。0xC1展开成二进制是11_000_001bMOD11 表示操作数是寄存器R/M001 表示 CXREG000 表示 AXD1 表示 AX 是目的。所以完整写法是db 8bh,11_000_001b。用 NASM 验证的话[cpu 8086][BITS 16]环境下这条指令汇编出来恰好两个字节和 db 伪指令完全一致。提示MOD11 时 REG 与 R/M 都是寄存器编号与 8 位/16 位寄存器表一一对应。解码时看到 MOD11 就不用再关心内存地址怎么算了直接查寄存器编号表即可。3. MOV 解码实战九类编码、长短格式与 NASM 对照验证3.1 MOV 九类编码拆解MOV 是学习 8086 编码最好的入口因为它覆盖面最广笔记里按操作数组合把它分成了九类。我整理成一张速查表左侧是 Intel 语法右侧是等价 db 写法操作等价机器码说明mov ax,cx8B C116 位寄存器互传通用模板mov al,cl8A C18 位寄存器互传mov ax,[bxsi0x1234]8B 80 34 12内存到 16 位寄存器mov [bxsi0x1234],ax89 80 34 1216 位寄存器到内存注意 D0mov word ax,0x1234C7 C0 34 12立即数到 16 位寄存器长格式mov word [bxsi0x1BCD],0x1234C7 80 CD 1B 34 12立即数到内存mov bx,0x1234BB 34 12立即数到寄存器短格式mov bl,0x34B3 34立即数到 8 位寄存器短格式mov ax,[1234h]A1 34 12直接地址到 AXmov [1234h],alA2 34 12AL 到直接地址mov es,ax8E C0通用寄存器到段寄存器mov ax,es8C C0段寄存器到通用寄存器这套表看起来很散但背后规律清晰寄存器互传走8B/89模板立即数与内存/寄存器用C6/C7组操作码立即数到累加器之外某特定寄存器时用B0~BF的短格式直接地址与 AX/AL 之间用A0~A3专用操作码段寄存器单独走8E/8C。有个细节值得单独提mov word ax,0x1234在笔记里的写法是db 0c7h,11_000_000b,0x34,0x12也就是C7 C0 34 12。这里的C0是 modrmMOD11、R/M000 表示 AXREG 字段虽然是 000但对于C7这个操作码来说它不是寄存器编号而是功能分组号 /0表示 MOV。理解这一点很关键同样是 modrm0xC0跟着8B时 REG 表示 CX跟着C7时 REG 只是分组号。所以解码时操作码决定 modrm 的解读方式不能死记。3.2 跟着做一遍用 NASM 把指令编译成原始字节笔记最实用的地方在于每一条编码都能用 NASM 验证而不是只给结论。我按笔记的步骤完整走一遍[rootORA9 cs7]# nasm -v NASM version 2.10.04 compiled on Jun 7 2021注意这里版本是 2.10.04笔记特别标注了不要用 nasm-0.98。旧版不识别11_000_000b这种带下划线分组的二进制常量直接用会把汇编器搞崩或者编出错误字节。建议环境里装 NASM 2.x 以上。接着创建测试文件 a.asm[cpu 8086] [BITS 16] mov word [bxsi0x1BCD],0x1234 db 0c7h,80h,0xcd,0x1b,0x34,0x12 mov al,0x34 db 0B0h,0x34这里的db是 NASM 的 define bytes 伪指令直接把后面的十六进制数原样写进输出文件。[cpu 8086]告诉 NASM 只允许 8086 指令集[BITS 16]指定 16 位模式。然后用-f bin生成纯二进制文件再用xxd看每个字节nasm -f bin a.asm -o a.bin xxd a.bin-f bin参数很关键它让 NASM 输出不含 ELF 头、不含段表的裸字节这样才能和 db 伪指令逐字节对比。预期输出里你会看到c7 80 cd 1b 34 12出现两次——一次来自 mov 指令本身一次来自 db 伪指令两者完全相同才算验证通过。如果不用-f bin而用默认的 ELF 格式xxd看到的是一大堆文件头机器码混在里面根本对不上这是新手最常见的验证失败原因。3.3 从 db 反推指令把笔记当解码手册用笔记的另一个用法是反查。比如你手头有一段未知机器码0xC6 0xC0 0x34不知道对应什么指令可以翻笔记这一条db 0c6h,11_000_000b,0x34 ; mov al,0x34C6是 MOV r/m8,imm8 的操作码C0的 MOD11 表示操作数是寄存器R/M000 对应 AL立即数34就是值本身。所以结论是mov al,0x34。注意C6与C7只差最后一位 WC6表示 8 位立即数目标C7表示 16 位。这套反推方法对读 BIOS 代码尤其有用因为很多老固件直接就是裸二进制没有符号表可用。我自己的习惯是把一个字节段复制出来先数前缀再按操作码区间查表最后用ndisasm验证一遍三分钟能解五六条指令。3.4 长短格式怎么选不只是省一个字节的问题MOV 立即数到寄存器有两套编码B0~BF短格式只需 2~3 字节C6/C7长格式需要 3~6 字节。为什么 Intel 要设计两套因为 8086 时代内存带宽和取指时间都很珍贵短格式能让最频繁的常量装载指令少占一半空间。比如mov bx,0x1234用短格式BB 34 12只有 3 字节用长格式C7 C3 34 12要 4 字节多了一个 modrm 字节。但是短格式只对特定寄存器生效B8~BF的高 3 位编码死的寄存器编号从 AX 到 DI。如果目的是内存单元就只能走C6/C7。写汇编器时通常会做个优化判断目的操作数是寄存器且立即数可用短格式时优先用短格式否则回退长格式。注意mov al,0x34的短格式是B0 34但笔记里出现了db 0c6h,11_000_000b,0x34这种等价长格式写法两者汇编结果语义相同、字节不同。验证时如果预期字节对不上先检查是不是长短格式混用了。4. 算术与堆栈指令解码ADD/PUSH/POP 的 d 位翻转规律4.1 ADD 同族指令一个 opcode 模板带出四个变体ADD 的编码模板是000000dw比 MOV 的模板简单因为 ADD 固定不涉及段寄存器操作数。笔记里的几个例子把 d 位的作用展示得很完整db 000000_1_1b,10_000_000b,34h,12h ; add ax,[bxsi0x1234] db 000000_1_0b,10_000_000b,34h,12h ; add al,[bxsi0x1234] db 000000_1_1b,11_000_001b ; add ax,cx db 000000_0_1b,10_000_000b,34h,12h ; add [bxsi0x1234],ax第一行000000_1_1b即03hD1、W1REG 是目的。modrm80h展开是10_000_000bMOD10 表示带 16 位位移REG000 表示 AXR/M000 表示[bxsi]所以是add ax,[bxsi0x1234]位移量 0x1234 按小端序放在后面。第四行000000_0_1b即01hD0方向反转REGAX 变成源内存单元变成目的所以是add [bxsi0x1234],ax。这里有个很实用的观察03h与01h、02h与00h之间只是 D 位不同方向完全相反。解码时看到操作码是01、03、29、2B这类成对出现的数字直接套 D 位规则不用再翻手册。4.2 PUSH/POP短格式与长格式并存的操作数族PUSH/POP 有个特殊规则操作数总是 16 位不存在字节压栈所以编码里根本没有 W 位的位置。它同时提供寄存器短格式和通用长格式两条路。短格式编码是01010_xxxxxx 是 16 位寄存器编号汇编指令机器码说明push ax5001010_000push di5701010_111pop ax5801011_000pop di5F01011_111长格式则用FF /6PUSH和8F /0POP后面跟一个 modrm 字节描述内存或寄存器操作数db 0ffh,11_110_000b ; push ax db 8fh,11_000_000b ; pop ax db 8fh,10_000_000b,34h,12h ; pop word [bxsi1234h]FF是单字节操作码但 REG 字段的 110 被手册定义为 PUSH 的功能号8F的 REG000 定义为 POP。这套「操作码 modrm 的 REG 字段分组号」的机制在 8086 里非常普遍C6/C7、F6/F7、80/81 都用同一套路。看到FF开头不要直接猜是 INC 或 DEC先看 modrm 的 REG 三位再定指令。段寄存器的 PUSH/POP 编码也值得记一下push cs是0Epop es是07pop ss是17pop ds是1F。这些是固定编码不经过 modrm。4.3 XCHG 与 IN/OUT固定编码与隐含操作数XCHG 有两套编码。与 AX 交换的短格式是90reg90本身是xchg ax,ax91是xchg ax,cx92是xchg ax,dx依此类推。用笔记中的写法db 10010_010b ; xchg ax,dx 即 0x92 db 10010_001b ; xchg ax,cx 即 0x91而通用寄存器与内存/寄存器交换用1000011w加 modrm比如86h/87hdb 1000011_1b,10_001_000b,34h,12h ; xchg cx,[bxsi1234h] db 1000011_0b,10_001_000b,34h,12h ; xchg cl,[bxsi1234h]IN/OUT 属于典型的固定编码加隐含操作数。IN 从端口读数到 AX/AL端口号可以立即数给出也可以放在 DX 寄存器里汇编指令机器码说明in al,20hE4 20固定端口到 ALin ax,20hE5 20固定端口到 AXin al,dxECDX 端口到 ALin ax,dxEDDX 端口到 AXout 20h,alE6 20AL 到固定端口out 20h,axE7 20AX 到固定端口out dx,alEEAL 到 DX 端口out dx,axEFAX 到 DX 端口这类指令特征很明显操作码低 3 位不同组合对应 AX/AL 与 IN/OUT 方向且E4/E5/E6/E7带一个立即数字节EC/ED/EE/EF不带。解码时先看第二个字节是否存在就能区分是立即数端口还是 DX 端口。4.4 遇到未见过指令时怎么定位笔记里还整理了一批 1 字节固定编码指令解码效率极高37是 AAAD7是 XLAT把[ds:bxal]的内容送到 AL9F是 LAHFFLAG 低字节送 AH9E是 SAHF9C是 PUSHF9D是 POPF。遇到单字节且不在寄存器编码范围内的操作码优先查这张表。LEA/LDS/LES 可以作为理解「modrm 语义随操作码变化」的补充例。8D是 LEA只取内存有效地址送入寄存器C5是 LDS从内存连续取两个字分别送通用寄存器和 DSC4是 LES目标段寄存器换成 ESdb 10001101b,10_001_000b,34h,12h ; lea cx,[bxsi0x1234] db 11000101b,10_001_000b,34h,12h ; lds cx,[bxsi0x1234] db 11000100b,10_001_000b,34h,12h ; les cx,[bxsi0x1234]三个操作码不同后面的 modrm 一模一样REG001 表示 CXR/M000 表示[bxsi]位移 0x1234。区别只在 LEA 不访问内存LDS/LES 要读两次内存分别装载段寄存器和通用寄存器。5. 解码避坑指南字节序、方向位与 NASM 版本的四个翻车点5.1 现象NASM 编译 db 伪指令报错且带下划线的二进制常量解析异常我照着笔记第一次跑的时候用了一个老的 NASM 0.98编译db 0c7h,11_000_000b,0x34直接报错换成11000000b不带下划线又能过。原因NASM 0.98 不支持二进制常量内部的下划线分组语法11_000_000b被当成非法 token而 NASM 2.x 才加入这个特性。解决升级到 2.x 版本或者统一把下划线去掉写成11000000b。笔记里特别标注了版本要求实测 NASM 2.10.04 编译这些编码没有任何问题。如果项目要跨机器跑建议在 Makefile 里加版本检查避免别人用旧版 NASM 编出错误字节。5.2 现象mov word [bxsi0x1BCD],0x1234 手工排字节时位移量或立即数高低字节颠倒我第一次手工编码这条指令按直觉把位移写成了1B CD立即数写成12 34结果反汇编出来的地址和值完全不对0xCD1B和0x3412整整错了一个数量级。原因8086 是小端序16 位位移量和立即数都是低字节在前、高字节在后0x1BCD 的正确排布是CD 1B0x1234 是34 12。解决写编码器时先做一个统一的小端序处理函数所有 disp16 和 imm16 都经过它输出不要人在脑子里倒序。验证时用xxd逐字节比对机器码里CD 1B出现在 modrm 之后、立即数之前顺序是 opcode、modrm、disp 低、disp 高、imm 低、imm 高。5.3 现象两个寄存器互传的 MOV 机器码方向搞反mov cx,ax和mov ax,cx的 modrm 都是0xC1附近方向由操作码 D 位决定。我最初按「REG 是源」来解读 8B结果把8B C1解成了mov cx,ax实际它应该是mov ax,cx。原因没记住 D1 表示 REG 是目的而不是源。解决把 D 位的语义写死在注释里D1 时 REG 是目的、M/R 是源D0 时反过来。记忆锚点8B 的二进制是1000 1011末尾11中第二个 1 是 DD1 时目的 REG 在 modrm 里这条可以作为默认假设先查确认不对再看 D0 的情况。5.4 现象add esp,0x1234这类 32 位寄存器指令在 8086 模式下汇编失败或生成异常字节如果往[cpu 8086]文件里混入 32 位寄存器操作数NASM 会直接报错因为 8086 没有 esp/eax 这些扩展寄存器。原因[cpu 8086]限制了指令集范围32 位指令属于后续 x86 扩展。解决研究 8086 编码时强制坚持[cpu 8086][BITS 16]成对出现需要验证 32 位指令时另建一个编译环境不要混在一个文件里。另一个坑是 BITS 设置如果忘了写[BITS 16]NASM 在某些输出格式下默认按 32 位模式编码同样的mov ax,cx会被编成带 66 前缀的变体字节数完全不同。5.5 现象用 objdump 反汇编裸 bin 时结果乱套指令识别全错我拿objdump -D -b binary去反汇编一个 16 位裸二进制输出的指令全是乱码比如把push ax解成inc bp。原因objdump 默认按当前架构位宽反汇编二进制文件没有 ELF 头它不知道这是 16 位代码按 32 位模式把两条 1 字节指令拼成了一条畸形指令。解决用 NASM 自带的ndisasm指定-b 16反汇编它不依赖文件头纯按字节流解析。例如ndisasm -b 16 a.bin输出的每一行都会标出偏移和机器码比 objdump 适合验证 8086 裸字节。如果非用 objdump 不可需要加-m i8086参数指定架构。6. 一条八字节指令的手工解码F03E8180CDAB3412 的完整验证6.1 用 xxd -r 把十六进制回灌成二进制拿到一段十六进制机器码最直接的验证方式是把回灌成二进制再反汇编看结果。命令如下echo F03E8180CDAB3412 | xxd -r -p raw.bin ndisasm -b 16 raw.binxxd -r -p把纯十六进制文本按对转成字节-b 16让 ndisasm 按 16 位模式解析。输出应该是一条指令F03E8180CDAB3412 lock add word [ds:bxsi-0x5433],0x1234。如果 ndisasm 输出两条以上指令说明你给的字节序列有问题重新检查前缀个数和立即数长度。6.2 逐字节拆解F0 是 LOCK 前缀告知 CPU 这条指令执行期间锁定总线3E 是 DS 段超越前缀显式指定段寄存器为 DS去掉 3E 编码相同但加上可以让反汇编输出带ds:前缀。81 是 ADD r/m16,imm16 的操作码W 位隐含在 81 这个分组中按字操作处理。80 是 modrm 字节MOD10 表示地址含 16 位位移REG000 表示功能分组 /0ADDR/M000 表示基址加变址[bxsi]。CD AB 是位移量小端序按 AB CD 读回 0xABCD作为有符号数是负数0x10000 - 0xABCD 0x5433所以写成 -0x5433。34 12 是立即数 0x1234。合起来就是完整的lock add word [ds:bxsi-0x5433],0x1234。6.3 核对负位移量的方法位移量是负数时最容易核对出错因为小端序的原始字节和数学上的负数形式对不上。我的习惯是先用无符号数读出0xABCD再判断它是否大于 0x8000大于则按补码取负数0xABCD - 0x10000 -0x5433。另一个交叉验证技巧是手工改一个字节把 CD 改成 00AB 保持重新反汇编看地址是否变回0xAB00这种方法能快速定位位移量切错了位置。从那以后我每次手工排完一段 8086 机器码都会强制走一遍xxd -r -p回灌加ndisasm -b 16反汇编的对照流程确认反汇编结果与原始汇编指令完全一致才继续往下走。解码这种事一条指令错了后面全跟着错早发现早省事。希望帮到你。本文还有配套的精品资源点击获取