☰
8086机器码解码:操作码、ModRM与位移立即数拆解
2026/10/27 19:05:43 网站建设 项目流程

简介:这份笔记面向正在编写8086汇编器、需要把汇编语句与机器码对应起来理解的技术人员,也可供学习x86底层编码的读者查阅。内容围绕8086机器语言解码展开,覆盖指令格式、通用与段寄存器、五种寻址模式、操作码、立即数及字节/字/双字等基础概念,并延伸到固定编码指令和双操作数指令编码。作者结合MOV指令的多类编码场景给出字节级示例,如寄存器互传、内存与寄存器互传、常数传送、段寄存器传送,并标注对应机器码与NASM写法,附有最长8字节指令、前缀与位移/立即数字节拆分等笔记。资源包为1个docx文档,大小约1.94MB,已有127人学习,适合按示例对照查证编码细节、梳理汇编器实现思路。

1. 一条六字节指令拆开之后:8086 机器语言解码的整体地图

c7 80 cd 1b 34 12这六个字节摆在一起,第一眼很难看出它是一条mov word [bx+si+0x1BCD],0x1234。8086 机器语言的解码,本质上就是把这串十六进制按固定规则切成几段:哪几位管操作类型,哪几位管寻址方式,哪几个字节是位移量,哪几个字节是立即数。切对了,指令自己就浮出来;切错一位,后面全乱。

这张地图大致分四层。最外层是前缀,总线锁定和段超越都挂在操作码前面;第二层是操作码,它决定这条指令属于哪个指令族;第三层是紧随其后的 ModRM 字节,操作数长什么样全由它的三个字段说了算;最后一层是位移和立即数,按小端顺序排在后面。往下才是具体指令族的编码表——MOV 的九种写法、PUSH/POP 的短码与长码、端口读写、地址运算和标志位操作。

适合正在写汇编器或反汇编器的人,也适合做指令级调试、复习微机原理的人。按这张图从上往下走,比抱着指令表硬背要省力得多。

2. 指令骨架:从 8 字节上限看操作码、ModRM 与位移立即数的排布

2.1 一条指令最长能有多长:把八个字节逐位拆开

8086 的指令长度在 1 到 8 字节之间浮动,不是定长。这个变长特性决定了解码器不能按固定步长切分,必须一个字节一个字节往后读,读到某一步才知道下一条指令从哪里开始。原文给的那个上限例子很典型:F0 3E 81 80 CD AB 34 12,八个字节把所有可能的成分都塞满了。

字节序号值角色解读
1F0总线锁定前缀让紧跟其后的读改写操作在总线上保持原子性
23E段超越前缀强制本条指令使用 DS 段
381操作码立即数与 r/m16 做算术运算,具体运算由 ModRM 的 reg 字段扩展位决定
480ModRMmod=10、reg=000、r/m=000,组合成 [BX+SI] 加 16 位位移
5-6CD AB位移量小端存放,实际值是 0xABCD,按有符号理解就是 -0x5433
7-834 12立即数小端存放,实际值是 0x1234

这张表值得反复看,因为它把「前缀可以叠加」「操作码之后未必是操作数」「位移和立即数可能同时出现」三件事一次讲完了。原文对这条指令的注释是lock add word [ds:bx+si-0x5433],0x1234,注意位移写成负数,是因为 0xABCD 在 16 位补码里就是 -0x5433,两个写法指的是同一个字节序列。

前缀部分有个容易写错的地方:前缀字节的数量和顺序都不固定。段超越前缀可能出现在锁定前缀前面,也可能一条指令压根没有前缀。稳妥的写法是让解码器进入一个循环,只要当前字节属于前缀集合就继续读,直到遇到第一个非前缀字节,把它当作操作码。这个循环少写一圈,后面所有字节的归属都会整体错位,反汇编出来的结果会离谱到让人怀疑人生。

提示:段超越前缀一共四个,26 对应 ES、2E 对应 CS、36 对应 SS、3E 对应 DS。原文只用到 3E,写解码器时这四个都要能识别。

2.2 操作码之后的第一个字节:ModRM 的三段式结构

绝大多数双操作数指令,操作码后面紧跟一个 ModRM 字节。它的三个字段从高位到低位依次切开,含义完全不同。

位区间字段长度含义
7-6MOD2 位00 表示无位移、01 表示 8 位位移、10 表示 16 位位移、11 表示操作数是寄存器
5-3REG3 位寄存器编号,或者作为操作码的扩展位 /digit
2-0R/M3 位寄存器编号,或者内存寻址的寄存器组合
def split_modrm(byte): """把一个 ModRM 字节拆成三段,返回值都是整数""" mod = (byte >> 6) & 0b11 # 高两位,决定有没有位移、位移多长 reg = (byte >> 3) & 0b111 # 中间三位,可能是寄存器号,也可能是 /digit 扩展位 rm = byte & 0b111 # 低三位,可能是寄存器号,也可能是内存组合 return mod, reg, rm # 以 c7 80 cd 1b 34 12 这条指令为例,第二个字节是 0x80 print(split_modrm(0x80)) # 输出 (2, 0, 0)

这段代码只做位移和掩码,没有任何查表逻辑,因为查表要放到后面结合操作码一起做。参数上唯一需要注意的是位移方向:mod 用右移 6 位,reg 用右移 3 位,rm 直接取低三位,顺序错了得到的字段就完全对不上。函数返回的 reg 值到底是寄存器编号还是扩展位,调用方必须自己根据操作码判断,这也是解码器最容易写成一大堆分支的地方。

REG 字段的双重身份是整个解码流程的分水岭。在 MOV、ADD 这类指令里,它是实打实的寄存器编号;在 PUSH、POP、移位、以及 0x81 这种「一组指令共用一个操作码」的场景里,它是扩展位,用来区分同一组里的具体运算。判断依据只有一个:操作码本身属于哪一组。写代码时先按操作码分组,再决定 reg 怎么解释,比在每个分支里临时判断要清晰得多。

2.3 位移和立即数谁先谁后:小端、长度与 MOD 的联动

ModRM 之后如果还有字节,顺序是固定的:先位移,后立即数。绝不会反过来。这个顺序在解码器里必须是硬编码的,不能靠猜。

长度的判断分两头。位移的长度看 mod:mod=01 时占 1 字节,mod=10 时占 2 字节,mod=00 且 r/m=110 时也占 2 字节,但含义是直接地址而不是位移。立即数的长度看操作数宽度位 w:w=1 表示字操作,立即数占 2 字节;w=0 表示字节操作,立即数占 1 字节。原文里那句「当 disp 为字时,位移量为 2 个字节;同理当立即数为 16 位时,立即数占 2 字节,否则 1 字节」,说的就是这件事。

几个典型长度放在一起对比,规律会清楚很多:

汇编写法字节序列总长位移立即数
mov word [bx+si+0x1BCD],0x1234C7 80 CD 1B 34 1262 字节2 字节
mov word ax,0x1234C7 C0 34 124无2 字节
mov al,0x34B0 342无1 字节
mov al,0x34(长格式)C6 C0 343无1 字节
add ax,[bx+si+0x1234]03 80 34 1242 字节无
push word [bx+si+0x1234]FF B0 34 1242 字节无

小端这件事在纸面上容易轻视,真上手时却是高频翻车点。0x1BCD 在内存里低字节在前,写成 CD 1B;0x1234 写成 34 12。反汇编时如果读出来的位移是 0xABCD 而期望值是 -0x5433,那不是解码错了,只是显示时没按有符号数处理。同理,如果发现一条指令后面多出一个字节,先怀疑是不是把立即数当成位移读了两遍。

注意:mod=01 时哪怕位移的实际值是 0,那一个字节也要老老实实占位,写成 00。少了它,后面的立即数就会被顶到错误的位置上。

3. 寻址与编号:MOD、R/M、REG 三张表怎么联合查

3.1 十六位寻址的八种 R/M 组合与 BP 的特殊地位

当 mod 不等于 11 时,r/m 字段不再是寄存器编号,而是八种内存寻址组合之一。这张表是 8086 解码的核心,背不下来基本没法往下写。

r/m有效地址默认段备注
000[BX+SI]DS基址加变址
001[BX+DI]DS基址加变址
010[BP+SI]SS栈段
011[BP+DI]SS栈段
100[SI]DS纯变址
101[DI]DS纯变址
110[BP],mod=00 时为 disp16 直接地址SS 或 DS唯一的双重身份
111[BX]DS纯基址

规律其实可以压缩成一句话:只有含 BP 的组合默认段才是 SS,其余都是 DS。这个默认段不是摆设,它直接决定实际访问哪一段内存,写错段寄存器,读出来的数据就跟预期差出一整段地址空间。

r/m=110 是整张表里唯一的特例。mod=00 时它不代表 [BP],而是代表后面跟一个 16 位直接地址,形如mov ax,[1234h]。而如果真想访问 [BP] 指向的单元,编译器必须用 mod=01 加一个值为 00 的 8 位位移,编码成8B 46 00。这条规则没有任何捷径,只能单独记住。

3.2 寄存器编号的三套表:十六位、八位、段寄存器

mod=11 时,r/m 和 reg 都退化成寄存器编号。8086 有三套编号体系,各自独立,混用就会出错。

编号16 位通用寄存器8 位通用寄存器段寄存器
000AXALES
001CXCLCS
010DXDLSS
011BXBLDS
100SPAH—
101BPCH—
110SIDH—
111DIBH—

十六位这一行是 AX、CX、DX、BX、SP、BP、SI、DI,顺序不是按字母排的,而是按编码值排的。八位这一行最容易背错:前四个是 AL、CL、DL、BL,后四个是 AH、CH、DH、BH,不是 AL、AH、CL、CH 这样交错。段寄存器只占两位,所以只有 00 到 11 四种取值,注意它是两位而不是三位。

举个对比例子:8B C1的 ModRM 是 11_000_001,mod=11、reg=000、r/m=001,所以是mov ax,cx;8B 01的 ModRM 是 00_000_001,mod=00、reg=000、r/m=001,所以是mov ax,[bx+di]。同一个字节差一位 mod,就从寄存器寻址变成了内存寻址,这就是为什么拆字段时不能凭感觉。

3.3 段超越前缀怎么改默认段

默认段只解决「不写前缀时用哪一段」,真要跨段访问,靠的是段超越前缀。四个前缀值分别是 26、2E、36、3E,对应 ES、CS、SS、DS。

典型场景是栈操作。[BP+SI]默认走 SS,如果想用同样的寻址方式去读数据段,就要在前面加 3E 强制切到 DS。反过来,[BX+SI]默认走 DS,想读栈上的内容就得加 36。

写解码器时,段超越前缀不影响指令长度之外的其他判断,但它会改变最终的有效地址。如果只解析指令格式、不计算物理地址,可以先把前缀值记下来,等到需要算地址时再合并;如果要做完整的地址还原,就得在解析 ModRM 之前把前缀状态准备好,别等到算地址时才发现前缀已经被丢掉了。

4. 指令族拆解:MOV 的九类写法与 PUSH/POP、端口指令的固定编码

4.1 MOV 的九类:一张表看完所有传送方向

MOV 是 8086 里出现频率最高的指令,也是编码形式最多的一条。原文把它拆成九类,覆盖了寄存器与寄存器、寄存器与内存、常数与寄存器、常数与内存、以及段寄存器参与的所有方向。

序号传送方向操作码ModRM 形态字节示例对应汇编
1通用寄存器之间(16 位)8B /r11 rrr rrr8B C1mov ax,cx
2通用寄存器之间(8 位)8A /r11 rrr rrr8A C1mov al,cl
3内存到通用寄存器8B /rmod≠11 加位移8B 80 34 12mov ax,[bx+si+0x1234]
4通用寄存器到内存89 /rmod≠11 加位移89 80 34 12mov [bx+si+0x1234],ax
5常数到内存C7 /0(C6 /0 为 8 位)mod rrr rrr 加位移加立即数C7 80 CD 1B 34 12mov word [bx+si+0x1BCD],0x1234
6常数到通用寄存器(短格式)B8+r(B0+r 为 8 位)无 ModRMBB 34 12mov bx,0x1234
7直接地址到 AX/ALA1(A0 为 8 位)无 ModRM,后跟 2 字节地址A1 34 12mov ax,[

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询