8086机器码解码:操作码、ModR/M与寻址模式解析 简介面向编写8086汇编器、需要吃透机器指令编码细节的开发者这份个人整理笔记以示例驱动梳理8086机器语言解码要点重点解决操作码与寻址模式组合难对应、机器码生成过程不易核对的问题。内容从指令格式、通用与段寄存器编号、寻址模式切入结合操作码、立即数、字节/字/双字、固定编码单字节指令及双操作数指令编码展开并借MOV指令说明寄存器、内存、常数与段寄存器之间的多种编码组合预览中的db字节序列与NASM写法对照便于理解机器码生成与校验思路。资源包为1个docx文档约1.94MB篇幅集中、便于检索适合作为查表式和复盘式参考资料。目前已有127人学习下载可用于汇编器开发时的编码对照、指令拆解、易错点复核也能帮助读者从具体示例反推指令编码规则。1. 从一串十六进制说起8086机器语言解码到底在解什么拆快递时面单上只有一串条码和几段缩写却能还原出收件人、地址和物品。8086机器语言解码做的事很像把C7 80 CD 1B 34 12这样的字节串还原成mov word [bxsi0x1BCD],0x1234。它不是在“翻译英语”而是在按一套固定规则拆分字节哪一位表示操作类型哪几位表示寄存器哪几个字节是地址偏移哪几个字节是立即数。很多人第一次看到机器码会觉得它像黑匣子同样叫 MOV有时是B8 34 12有时是C7 C0 34 12有时又变成8B 80 34 12。搞懂 8086 机器语言解码就能明白这些字节不是随便排的而是操作码、寻址模式、寄存器编号和操作数长度一起拼出来的结果。对手写汇编器、读反汇编结果、排查老程序的人来说这套规则是绕不开的地基。2. 一条指令为什么有长有短8086指令格式里的操作码、寻址与操作数8086 指令不是固定长度。最短的指令只有一个字节比如AAA的编码是0x37最长的指令可以到 8 个字节里面可能同时塞进总线锁定前缀、段超越前缀、操作码、寻址模式字节、偏移常量和立即数。刚接触时容易疑惑为什么不让每条指令一样长原因很朴素8086 面向的是存储空间紧张的环境常见操作要尽量短复杂寻址才多花字节。变长编码就是用“常用短、少用长”的办法省空间。一条典型指令可以拆成几层前缀、操作码、ModR/M 字节、位移量、立即数。前缀不是每条都有操作码决定大类ModR/M 决定寄存器或内存地址怎么来位移量是地址里的偏移立即数是直接写在指令里的常数。把这些层分开看机器码就不再是一整块。2.1 操作码只是开头前缀、操作码、寻址字节、位移与立即数先看一个被拆开的例子F0 3E 81 80 CD AB 34 12。这串字节对应的汇编写法是lock add word [ds:bxsi-0x5433],0x1234。其中F0是总线锁定前缀3E是段超越前缀表示后面用 DS 段81是 ADD 的一类操作码表示对 16 位内存或寄存器加一个 16 位立即数80是 ModR/M 字节CD AB是 16 位位移量按小端序读成0xABCD也就是负数-0x5433最后的34 12是立即数0x1234。一条指令里同时出现这么多东西长度自然就上去了。可以把这几层想成寄快递时的信息分层前缀像“加急/指定路线”的附加标记操作码像“这是寄件还是退件”的业务类型ModR/M 像收件地址的写法位移量像地址里的门牌偏移立即数像包裹里自带的物品。缺哪层信息就不完整多哪层长度就增加。组成作用是否必需例子前缀总线锁定、段超越等修饰可选F0、3E操作码指定指令大类与部分方向/宽度必需81、C7、8BModR/M指定寄存器编号或内存寻址方式多数双操作数指令需要80、C0位移量内存地址中的偏移常数看寻址方式CD AB立即数指令里直接给出的常数看指令形式34 12这里要特别注意“操作码”不是整条指令的全部。有人看到C7就以为已经解完了其实C7只说明“要把一个立即数送到 16 位内存或寄存器”到底送到 AX 还是送到[BXSIdisp16]还得看后面的C0或80。同理FF也不是一个孤零零的 PUSH它在不同/reg扩展下可以表示 PUSH、CALL、JMP 等不同操作。2.2 寻址模式不是“几种写法”地址到底从哪里来教材里常把 8086 的寻址模式列成寄存器寻址、立即数寻址、内存寻址、寄存器间接寻址、基于寄存器的变址寻址。这个分类对入门有帮助但真到解码时最常打交道的不是名字而是 ModR/M 字节里的mod和r/m两块。它们合起来告诉解码器操作数是在寄存器里还是在内存里如果是在内存里地址是由哪几个寄存器相加后面跟不跟位移量。mod占 2 位r/m占 3 位。mod11时操作数在寄存器里r/m直接给出寄存器编号mod00、01、10时操作数在内存里区别是后面跟不跟位移、位移是 8 位还是 16 位。r/m的 3 位则决定基址和变址的组合比如000表示[BXSI]001表示[BXDI]010表示[BPSI]011表示[BPDI]100表示[SI]101表示[DI]110表示[BP]111表示[BX]。这些组合不是背下来就完事真正的坑在mod00且r/m110时它不表示[BP]而是表示一个 16 位直接地址。很多人第一次写解码器就在这里翻车把[0x1234]错解成[BP]。举个短例子8B 80 34 12。8B是 MOV 的一类操作码80的二进制是10 000 000所以mod10、reg000、r/m000。mod10表示后面跟 16 位位移r/m000表示基址变址组合[BXSI]位移是34 12按小端序读成0x1234。合起来就是mov ax,[bxsi0x1234]。如果只看80这个十六进制数很容易以为它和数值 0x80 有什么关系其实它只是三个位段拼出来的寻址描述。提示看到内存操作数时先看mod再看r/m最后才读位移量。顺序反了很容易把位移字节当成操作码。2.3 字节、字、双字与立即数长度不是拍脑袋定的8086 里常说的字节 B 是 8 位字 W 是 16 位双字 D 是 32 位。这里要分清B/W/D 是操作数宽度的标记不是三个具体寄存器。8086 的通用寄存器是 16 位的但可以拆成两个 8 位寄存器使用比如 AX 可以拆成 AH 和 AL。指令里的w位就是用来区分“这次做字节操作还是字操作”。w1表示字操作w0表示字节操作。看到mov al,0x34和mov ax,0x1234它们的操作码形式可能相近但立即数长度和寄存器编号范围不一样。立即数的长度也跟着操作数宽度走。8 位操作通常跟 1 字节立即数16 位操作通常跟 2 字节立即数。位移量则看modmod01跟 1 字节位移mod10跟 2 字节位移。注意小端序34 12读成0x1234不是0x3412。这一点在解码时必须固定下来否则每个字节都对拼出来的数却是反的。双字 D 在 8086 原生指令里不是常规操作数宽度更多是后来 x86 体系里的说法。读 8086 材料时看到“字节/字/双字”要结合上下文它可能是在讲数据宽度概念不一定意味着 8086 有一条现成的 32 位加法指令。把概念宽度和指令实际支持宽度分开后面看 32 位扩展时才不会混。双操作数指令编码还有一个硬规则目的和源不能同时为内存单元。也就是说mov [bxsi], [di]这类写法在 8086 指令编码里没有对应的“内存到内存”形式。要完成这种传送得拆成两步借助寄存器中转。这个限制不是汇编器故意为难人而是编码空间和硬件数据通路决定的。解码器看到两个内存操作数时基本可以判断字节流有问题或者前面的边界已经读错了。3. 寄存器编号与ModR/M为什么AX常写成000机器码里不会直接写“AX”这三个字母它只认编号。8086 的 16 位通用寄存器有 AX、CX、DX、BX、SP、BP、SI、DI编号从000到111。8 位通用寄存器有 AL、CL、DL、BL、AH、CH、DH、BH也是 3 位编号。段寄存器 ES、CS、SS、DS 同样有 2 位编号。把这些编号表放在手边再看 ModR/M 字节很多“玄学字节”就会变成可读的字段。3.1 三张编号表16位、8位与段寄存器先看 16 位通用寄存器。AX 的编号是000CX 是001DX 是010BX 是011SP 是100BP 是101SI 是110DI 是111。这个顺序不是按字母排的而是按硬件编码排的。8 位寄存器的编号则是 AL000、CL001、DL010、BL011、AH100、CH101、DH110、BH111。注意 AH、CH、DH、BH 的编号落在高 4 位这解释了为什么mov al,cl和mov ah,ch看起来都是寄存器传送字节里的编号却不一样。段寄存器的编号是 ES00、CS01、SS10、DS11。这个编号常出现在8C、8E、06、0E、16、1E、07、17、1F这类指令里。比如8E C0表示mov es,ax其中C0的mod11、reg000表示 ES、r/m000表示 AX8C C0则表示mov ax,es。同样是C0操作码换了方向就反了。类型编号 00000101001110010111011116位通用AXCXDXBXSPBPSIDI8位通用ALCLDLBLAHCHDHBH段寄存器ESCSSSDS无无无无这张表看起来简单实际解码时最容易错的是“同一个 3 位编号在 8 位和 16 位语境下指向不同寄存器”。000在 16 位里是 AX在 8 位里是 AL100在 16 位里是 SP在 8 位里是 AH。判断依据是操作码里的w位或者当前指令明确要求 8 位/16 位操作。只看 ModR/M 的reg000就写 AX遇到字节操作就会错。3.2 ModR/M字节mod、reg和r/m三块拼图ModR/M 是一个字节拆成三块最高 2 位是mod中间 3 位是reg最低 3 位是r/m。reg这个字段的名字容易让人误解因为它不一定永远表示“寄存器”。在普通 MOV、ADD 里它通常表示一个寄存器操作数但在C7 /0、FF /6这类指令里它被当作扩展操作码使用/0、/6就是写在这个字段里的。r/m也不一定永远是内存mod11时它就是寄存器编号。拿C7 80 CD 1B 34 12来说。C7是操作码80是 ModR/M。80的二进制是10 000 000mod10、reg000、r/m000。mod10表示后面有 16 位位移r/m000表示[BXSI]reg000在C7这条指令里表示/0扩展不是 AX。接着读位移CD 1B小端序为0x1BCD再读立即数34 12小端序为0x1234。合起来就是mov word [bxsi0x1BCD],0x1234。如果把reg000当成 AX就会解出完全错误的操作数。再看C7 C0 34 12。C0的二进制是11 000 000mod11、reg000、r/m000。mod11表示操作数在寄存器里r/m000表示 AXreg000仍然是/0。所以这条是mov ax,0x1234的长格式。它和B8 34 12的结果一样但字节数不同编码路径也不同。3.3 d位与w位方向、宽度藏在操作特征字节里很多双操作数指令的操作码字节里带两个关键位d和w。w决定字节操作还是字操作d决定reg字段表示目的还是源。以 MOV 的8A/8B和88/89为例8B C1是mov ax,cx89 C1是mov cx,ax。C1本身是11 000 001mod11、reg000、r/m001。在8B里reg是目的所以 AX 是目的、CX 是源在89里reg是源所以 CX 是源、AX 是目的。只看C1方向是看不出来的必须结合操作码。ADD 也有类似的方向位。03 C1是add ax,cx01 C1是add cx,ax。03的二进制00000011里d1、w101的二进制00000001里d0、w1。d1时reg是目的d0时reg是源mod和r/m指向的那个操作数是另一端。这个规则在 MOV 和 ADD 上大体相通但 MOV 还有8A/8B、88/89、A0-A3、B0-BF、C6/C7等多种形式不能拿一个d位解释所有 MOV。方向位是线索不是万能钥匙。w位也一样。8B C1是 16 位传送8A C1是 8 位传送。8A和8B只差最低位但寄存器编号的解释跟着变8B C1里的001是 CX8A C1里的001是 CL。看机器码时先定w再查寄存器表顺序不能反。4. 把MOV拆成字节九类传送与短格式、长格式MOV 是 8086 里最常被拿来练解码的指令因为它的形式多覆盖了寄存器、内存、立即数、段寄存器、特定地址等几种典型路径。把 MOV 拆明白再去看 PUSH、POP、XCHG、ADD很多结构是相通的。下面按“操作数从哪到哪”来分不按助记符背。4.1 寄存器互传与内存互传8A/8B和88/89寄存器到寄存器的传送最直观。8B C1是mov ax,cx8A C1是mov al,cl。8B和8A的reg字段是目的寄存器r/m字段是源寄存器mod11表示两端都在寄存器里。把C1拆开mod11、reg000、r/m001。16 位语境下000是 AX、001是 CX8 位语境下000是 AL、001是 CL。字节序列短信息密度高错一位就换寄存器。内存与通用寄存器互传时8B表示“内存到寄存器”89表示“寄存器到内存”。8B 80 34 12是mov ax,[bxsi0x1234]89 80 34 12是mov [bxsi0x1234],ax。两条指令的 ModR/M 和位移完全一样只有操作码从8B换成89方向就反了。这里也能看出d位的作用8B的reg是目的89的reg是源。但不要把它们和8A/88混在一起8A/88是 8 位版本寄存器编号要按 AL、CL 那套表查。4.2 常数到寄存器与内存B0-BF短格式与C6/C7长格式常数传送到寄存器有两条常见路径。短格式是B0rb或B8rwB0到B7对应 8 位寄存器B8到BF对应 16 位寄存器后面直接跟立即数。B8 34 12是mov ax,0x1234B3 34是mov bl,0x34。短格式只适合“立即数到寄存器”字节数少解码也直接操作码最低 3 位就是寄存器编号。长格式是C6 /0和C7 /0。C6用于 8 位C7用于 16 位/0写在 ModR/M 的reg字段里。C7 C0 34 12是mov ax,0x1234C6 C0 34是mov al,0x34。它和短格式的结果一样但长格式能通过 ModR/M 指向内存所以C7 80 CD 1B 34 12可以表示mov word [bxsi0x1BCD],0x1234。短格式的B8-BF只面向寄存器不能直接写内存这是两条编码路径的分工不是谁替代谁。这里有个血泪经验不同汇编器对二进制常量的写法支持不一样带分组下划线的写法在老版本里可能不认。写验证代码时如果汇编器报错先别怀疑指令写错换一个支持分组二进制常量的版本再试。这个坑不影响机器码本身但会影响你对照字节序列的效率。4.3 特定地址与段寄存器A0-A3、8C/8E的边界A0-A3是一组专门面向“特定地址内存”和 AX/AL 的传送。A0 34 12是mov al,[1234h]A1 34 12是mov ax,[1234h]A2 34 12是mov [1234h],alA3 34 12是mov [1234h],ax。它们后面直接跟 16 位地址不需要 ModR/M 再描述[BXSI]这类组合。优点是写直接地址时短局限是只能和 AX/AL 打交道不能换成 CX、DX 等其他寄存器。段寄存器传送走8C和8E。8C /r是“段寄存器到通用寄存器或内存”8E /r是“通用寄存器或内存到段寄存器”。8C C0是mov ax,es8E C0是mov es,ax8E 04是mov es,[si]8C 04是mov [si],es。这里要注意编码表里能列出某个位组合不代表所有组合在真实执行时都合法。比如向 CS 写入的 MOV 在 8086 上受限汇编器可能直接拒绝。做解码器时要能识别字节做执行器或汇编器时还得再加合法性检查。这两件事不能混为一谈。4.4 用一段小程序验证从汇编写法反推字节序列下面用一段 Python 小函数拆 ModR/M不依赖具体汇编器。它只做位运算把0x80拆成mod、reg、rm再结合前面的操作码判断后面该读几个字节。实际解码器会比这复杂但这个骨架足够看清思路。def decode_modrm(byte): # 取出最高2位作为mod中间3位作为reg最低3位作为r/m mod (byte 6) 0b11 reg (byte 3) 0b111 rm byte 0b111 return mod, reg, rm # C7 80 CD 1B 34 12 中的 ModR/M 是 0x80 mod, reg, rm decode_modrm(0x80) print(mod, reg, rm) # 输出 2 0 0 # mod2 表示 disp16reg0 在 C7 里是 /0 扩展 # rm0 且 mod!0 时表示 [BXSI] # 后面依次读 2 字节位移 CD 1B 和 2 字节立即数 34 12这段代码只回答了“ModR/M 怎么拆”没有回答“操作码是什么、后面跟几个字节”。真正的解码顺序是先看前缀再读操作码根据操作码判断有没有 ModR/M有的话拆mod/reg/rm再按mod读位移按w和指令形式读立即数。参数上byte是一个 0 到 255 的整数返回值mod是 0 到 3reg和rm是 0 到 7。把小端序读取单独封装成函数能避免34 12被误读成0x3412这类低级错误。5. 容易混淆的几组概念机器码解码里的误区与边界学到这一步单个字节已经能拆了真正容易出错的是概念之间的边界。下面四组混淆在初学者里很常见每组都按“易混点、为什么混、怎么区分”拆开说。5.1 操作码与操作特征字节不是同一个“操作”易混点把 ModR/M 里的reg字段也叫操作码或者把C7后面的/0当成独立操作码。为什么混两者都在决定“这条指令到底做什么”名字里又都带“操作”。实际上操作码是指令开头的那个字节或字节组决定指令大类ModR/M 是寻址描述字节里面的reg字段在普通传送里表示寄存器在C7、FF这类指令里才被借去当扩展操作码。区分方法很简单看它是不是紧跟在操作码后面、是不是参与mod/reg/rm三块拆分。C7是操作码C0是 ModR/MC0里的000在C7语境下是/0不是 AX。写解码器时先把“操作码表”和“ModR/M 解释规则”分成两张表能少很多纠结。5.2 字节、字、双字与寄存器宽度名字像范围不同易混点看到“字”就以为一定对应 16 位寄存器看到“双字”就以为 8086 一定有 32 位寄存器操作。为什么混B/W/D 是数据宽度标记AX/AL 是寄存器名称两者经常出现在同一句话里。8086 的通用寄存器是 16 位但可以按字节访问高 8 位或低 8 位w1表示字操作w0表示字节操作。双字 D 是 32 位宽度概念在 8086 原生指令里不是常规操作数宽度更多出现在后续 x86 扩展的讨论中。区分时先问现在说的是“数据有多宽”还是“哪个寄存器”前者看 B/W/D 和w位后者查寄存器编号表。把这两个问题分开就不会把mov al,0x34的 8 位操作硬套成 16 位。5.3 固定编码指令与带操作数指令0x37不是“缺了参数”易混点看到AAA的编码只有0x37就以为漏了操作数看到XLAT的0xD7就想找 ModR/M。为什么混平时练的 MOV、ADD 大多带操作数容易形成“每条指令后面都该跟点东西”的惯性。固定编码指令的操作数是隐含的AAA对 AL/AH 做调整XLAT用[ds:bxal]取字节送到 ALLAHF把标志寄存器低字节送到 AHPUSHF把标志寄存器压栈。它们不需要在字节流里再写操作数。区分方法查操作码表时看该指令是否标注“固定编码”或“隐含操作数”如果操作码本身已经唯一确定了行为后面就不该硬读 ModR/M。反过来看到FF、C7、8B这类操作码才需要继续看后面的寻址字节。5.4 短格式与长格式不是性能优化而是编码路径不同易混点把B8 34 12和C7 C0 34 12都叫“MOV AX,0x1234”就以为它们可以随便互换或者短格式一定“更高级”。为什么混两条指令执行结果一样汇编器也都能接受差别只体现在字节数上。实际上短格式B0-BF只面向寄存器操作码里直接带寄存器编号长格式C6/C7通过 ModR/M 描述目的能指向内存也能指向寄存器。区分时看目的操作数如果目的只是寄存器短格式可能更短如果目的涉及内存或需要统一的 ModR/M 路径长格式更通用。解码器不能因为结果相同就合并处理必须按首字节分支分别读立即数和 ModR/M。把“结果等价”当成“编码等价”是手写解码器时很容易踩的坑。6. 什么时候适合翻这份笔记写解码器与读反汇编的边界这份笔记最适合放在两个场景里。一个是手写 8086 汇编器或反汇编器时需要把助记符和字节序列来回对照另一个是读老程序的机器码、看调试器里的反汇编结果时想确认某个字节到底该按操作码、ModR/M 还是立即数解释。它解决的是“字节怎么拆”的问题不是“现代处理器怎么优化”的问题。场景适合程度原因写 16 位汇编器/反汇编器适合需要逐字节对照操作码、ModR/M、位移和立即数读 8086 老程序机器码适合能判断变长指令的边界避免把数据当指令教学演示指令编码适合短指令、固定编码、双操作数编码都能找到例子直接写现代 64 位程序不适合寄存器、寻址、前缀和指令集都扩展了不能照搬做指令合法性校验只适合当起点编码表能识别字节但不等于所有组合都能执行不适合的地方也要说清楚如果目标是写现代 x86-64 程序8086 的 16 位寄存器和段寄存器模型会把人带偏如果目标是做完整的汇编器除了编码表还要处理符号、重定位、段安排和合法性检查。把这份笔记当成“字节解码的对照表”最稳别把它当成“所有 x86 问题的后悔药”。有次我对着一串C7开头的字节先把C0当成普通寄存器字段解出来发现目的寄存器不对后来强制自己先看mod是否为11再看reg是不是/0扩展才把方向理正。从那以后我每次看到陌生机器码都先标出前缀、操作码和 ModR/M再谈位移和立即数不急着写助记符。希望帮到你。本文还有配套的精品资源点击获取