反汇编之硬编码

知识点

组操作码
ModR/M 字节
ModR/M 构成,它由三个部分组成
完整硬编码由Opcode(操作码)、ModR/M (字节)、Displacement(位移量) 和 Immediate(立即数)四个部分组成
Mod 字段的 4 种模式
操作码扩展
主Opcode 80 的子操作码对照表
寄存器编码对照表
F6 /0 是一种用来表示扩展码的方式(F6是组扩展码是多个指令,想对应具体指令,还需要看MoR/M中的Reg/Opcode字段的Opcode扩展码对应的编码表值)

 

举例应用上面的硬编码,下面是一段程序的32位程序的反汇编代码

我们不用管他的正确与否,截取的这段我们只看红色的一行

复制代码
复制代码
00001130 <__do_global_dtors_aux>:
    1130:       f3 0f 1e fb             endbr32
    1134:       55                      push   ebp
    1135:       89 e5                   mov    ebp,esp
    1137:       53                      push   ebx
    1138:       e8 53 ff ff ff          call   1090 <__x86.get_pc_thunk.bx>
    113d:       81 c3 9b 2e 00 00       add    ebx,0x2e9b
    1143:       83 ec 04                sub    esp,0x4
    1146:       80 bb 30 00 00 00 00    cmp    BYTE PTR [ebx+0x30],0x0
    114d:       75 27                   jne    1176 <__do_global_dtors_aux+0x46>
 ....
 
55 push ebp 这一行
完整硬编码由Opcode(操作码)、ModR/M (字节)、Displacement(位移量) 和 Immediate(立即数)四个部分组成
但是为什么是单字节?
x86 指令集采用变长编码。为了提高效率,设计者将最常用的操作如寄存器入栈,压缩到了极短的长度。
指令格式:push 寄存器的指令格式是 01010nnn(二进制)。
寄存器编号:在 x86 中,ebp 的编号是 5(二进制 101)。
组合结果:01010 + 101 = 01010101  十六进制 55

在传统的 C 调用约定(Stack Frame 建立)中,函数头几乎总是:
  nasm示例:
55          push ebp      ; 保存调用者的基址指针
89 E5       mov ebp, esp  ; 设置当前函数的基址指针
这两行代码共同构成了所谓的 Prologue(函数序言)

 硬件是如何执行 55 的?
 当CPU 译码器看到字节 55 时,自动识别它不需要查表,电路直接识别出这是一个 push 操作,目标是 ebp。
 堆栈操作来说,将堆栈指针 esp 减去 4(在 32 位模式下)。将 ebp 寄存器里的当前值写入 [esp] 指向的内存位置。
 时钟周期来说,在现代 CPU(如 Intel 第 15 代或 Zen 5 架构)上,由于堆栈引擎(Stack Engine)的优化,这个操作几乎是零延迟的,不会占用主要的 ALU 执行单元。

  
55、50、51、53对应关系速查
 
十六进制汇编指令长度
55 push ebp 1 字节
50 push eax 1 字节
51 push ecx 1 字节
53 push ebx 1 字节

我们将重点分析:
80 bb 30 00 00 00 00 cmp BYTE PTR [ebx+0x30],0x0
 这条硬编码指令,怎么翻译为指令助记符呢? 
首先1146是放置在eip中的内存地址
1.80称为opcode(操作码)
2.bb就是ModR/M(字节)
3.30 00 00 00 称为Displacement(位移量)
4.00 称为Immediate(立即数)

 一、在 x86 指令集编码中,这一串字节 80 bb 30 00 00 00 00 是一条完整的指令。
它由 Opcode(操作码)、ModR/M (
字节)、Displacement(位移量) 和 Immediate(立即数)四个部分组成。
            80 bb 30 00 00 00 00拆解
字节(十六进制)           操作类型  含义
 80 Opcode(操作码)

表示这是一个立即数与内存/寄存器的操作(设定寻址模式,上面介绍过是4种)。

查看 80x86 Instructions by Opcode(指令操作码) - jinzi - 博客园

80 Opcode,你会看到他会对应很多操作码,如 ADD, ADC, CMP 等,他称为主Opcode ,

也就是一个Opcode对应多个助记符,但是到底是对应哪个助记符呢? 实际是由  ModR/M 字节的 Reg 字段决定

这时Reg字段代表操作码扩展即操作码的扩展,也就是最终操作码由80和reg中的111共同决定是对应哪个指令,

这里cpu最终决定是对应cmp指令

 bb ModR/M(三部分构成) 0xbb的二进制为 10 111 011。他由三个部分构成如下:
10部分:   Mod=10 (2): 32位偏移内存模式。
111部分: Reg=111 (7): 操作码扩展,对应 CMP (比较) 指令。
011部分: R/M=011 (3): 基址寄存器为 EBX
30 00 00 00     Displacement(位移量
)  
32位小端序数值,即 0x00000030 (十进制 48)。
00 Immediate(立即数) 立即数,值为 0x0

二、ModR/M 构成,它由三个部分组成
0xbb的二进制为 10 111 011
字段位 (Bit)二进制值二进制转为十进制值含义
Mod 7-6 10 0010=2

根据Mod字段的4种模式,Mod值有四种 00(0)/01(1)/10(2)/11(3) 
2代表32位偏移内存模式
由于80是group OPcode,所以80对应多个指令,到底对应哪个指令 ,进一步需要Reg字段来最终决定使用哪个指令
Reg/Opcode 5-3 111 0111=7

由于操作码是组操作码,所以不查寄存器编码表,而要查主Opcode 80 的子操作码对照表

主Opcode 80 的子操作码是 0100=7,

通过查看 下面的 主Opcode 80 的子操作码对照表 ,7代表的是cmp指令

R/M 2-0 011 011=3

指定另一个寄存器为 EBP(32位) / RBP(64位)

寄存器编码对照表中 3代表(32位寄存器ebx)

                   
注意: Reg/Opcode字段中,使用Reg还是Opcode,由操作码类型决定,如果是组操作码,则使用Opcode(扩展码)

硬编码 是否组操作码 Reg/Opcode 字段的作用 最终指令 查哪个表 说明
操作码   
ModR/M
         
89    e5 指定寄存器 (esp) mov %esp, %ebp 寄存器编码对照表 89是一条标准的双操作数指令,是有明确的指令对应。
ff    e5 指定组操作码的具体操作指令 (JMP) jmp *%ebp 主Opcode的子操作码对照表

ff操作码(Opcode)本身不足以确定具体的指令动作,
必须依靠随后的 ModR/M 字节中的 Reg/Opcode字段

(第 5-3 位)作为额外的 3 位操作码扩展,
通过查看主Opcode的子操作码对照表才能最终确定指令助记符

  89和ff可以查看  80x86 Instructions by Opcode(指令操作码、硬编码) - jinzi - 博客园

      主流硬件环境下通用的寄存器编码对照表

                 在 x86 架构中,通用寄存器通过一个 3 位的二进制数值来代表不同的寄存器。这个编码广泛应用于 ModR/M 字节的 Reg 字段和 R/M 字段。

编码 (十进制)3位的二进制数值8-bit16-bit32-bit (E)64-bit (R)
0 000 AL AX EAX RAX
1 001 CL CX ECX RCX
2 010 DL DX EDX RDX
3 011 BL BX EBX RBX
4 100 AH / SPL* SP ESP RSP
5 101 CH / BPL* BP EBP RBP
6 110 DH / SIL* SI ESI RSI
7 111 BH / DIL* DI EDI RDI
                     Mod 字段的 4 种模式
                     所以Mod值有四种 00/01/10/11 ,具体如下
Mod 值 (二进制)Mod 值 (十进制)模式名称含义
00 0 无偏移内存模式 直接寻址内存。例如 (%eax)
01 1 8位偏移内存模式 内存地址 + 一个字节的偏移量。例如 8(%eax)
10 2 32位偏移内存模式 内存地址 + 四个字节的偏移量。例如 1024(%eax)
11 3 寄存器模式 不访问内存。操作数直接就在寄存器里。

 
详细解释:
  1. 操作:CMP (比较)。它将内存中的值与立即数相减,但不保存结果,仅改变标志寄存器(EFLAGS)。
  2. 寻址:它访问地址为 EBX寄存器的值 + 0x30 的内存空间。
  3. 数据宽度:80 操作码指定了这是一个 8位(Byte) 操作。它只取内存中的一个字节与立即数 0 进行比较。
总结
cmp    BYTE PTR [ebx+0x30],0x0 该指令的意思是:
检查存储在 ebx + 48(0x30十进制为48) 这个内存位置的那个字节是否为 0。请务必检查你的 ebx 寄存器在执行此行前是否已经正确指向了一个合法的结构体或缓冲区。

   

三、我们如何通过汇编指令翻译为硬编码 例如 push ebp

将汇编指令(如 push %ebp)手动翻译为硬编码(机器码)需要查阅 Intel 指令集参考手册。翻译过程通常遵循以下三个步骤
 1. 查找指令的基本操作码 (Opcode)
   x86 指令集为了压缩体积,对常用指令提供了“快捷方式”。 
  通用规则:
许多单操作数指令(如 push 寄存器)的操作码是
1 字节。 查询手册:
查阅 PUSH 指令表,你会发现 PUSH reg 的操作码格式
50 + rd。 这里的 50 是基础操作码。 rd 是寄存器的编码值。 2. 确定寄存器的编码值 (Register Index) 正如我们之前提到的通用寄存器编码表: EAX = 0, ECX = 1, EDX = 2, EBP = 5, ESP = 4 ... 3. 计算最终硬编码 对于 push ebp: 基础码Opcode:0x50 寄存器编码:5 计算:0x50 + 5 = 0x55 结论:push %ebp 的硬编码就是 55

 对于复杂点的指令,例如涉及两个操作数的指令,需要组合 Opcode 和 ModR/M 字节。
  以 mov %esp, %ebp 为例: 
 查 Opcode:MOV 寄存器到寄存器的操作码是 89。
 构建 ModR/M 字节:
 Mod (7-6位):寄存器模式,取 11。
 Reg (5-3位):源寄存器是 esp (编码 4),即 100。
 R/M (2-0位):目标寄存器是 ebp (编码 5),即 101。
 组合二进制:11100101 \(\rightarrow \) 十六进制 E5。
 最终硬编码:89 E5

在 Linux 系统下,你可以使用以下工具快速验证 

echo "push %ebp" | as --32 -o test.o && objdump -d test.o

输出会直接显示:0: 55 push %ebp

   单操作数指令总结快速对照表
    

汇编指令 (32位) 翻译逻辑 查看寄存器编码 硬编码
push %eax 50 + 0 0代表eax 50
push %ebp 50 + 5 5代表ebp 50+5
pop %ebp 58 + 5 5代表ebp 5D
ret 固定码   C3
pop esi     5e
push   esp     54
push   edx     52
push   ecx     51
leave     c9
nop     90
pop    ebx     5b
push   esi     56

 

 

四、什么是操作码扩展 (Opcode Extension)? 
     
x86 的基础操作码(Opcode)通常只有 1 个字节(256 个组合)。比如cmp、mov 等指令 ,如果每条指令都占用一个独立的 Opcode,空间会迅速耗尽。 对于某些指令(特别是只有一个操作数的指令,如 INCDECPUSHPOP 以及大量的位运算指令),ModR/M 字节中的 Reg 字段(第 3-5 位) 是空闲的。

     在“操作码扩展”机制下,当主 Opcode 无法完全确定指令时,CPU 会查看ModR/M 字节中的 Reg 字段 的 3 个位。这 3 位可以提供 8 个额外的子操作码(000 到 111)
 这种一对多关系的操作码,比如80对应 ADD, ADC, CMP 等称为group指令 即Group Opcode,80 是一个特殊的“组操作码”(Group Opcode)。它本身并不代表一个具体的指令(如 ADD 或 XOR),而是作为一个前缀,告诉 CPU 查阅指令格式中的 ModR/M 字节的中间 3 个bit位 (Reg/Opcode 字段) 来决定具体执行哪种运算。这种许多指令共用一个主操作码,通过 ModR/M 字节中的 reg/opcode 字段(3位)来区分的机制,被称为 操作码扩展 (Opcode Extension).  译码硬件会提取这 3 位,将其作为偏移量来决定最终执行哪种具体的微操作。对于简单的指令,指令格式是直接固化在硬件电路中的。
 

      
五、组操作码(Group Opcode,专业术语)
     


     主Opcode 80 的子操作码对照表
           当操作码为 80 时,ModR/M 字节的第 3、4、5 位对应的具体指令如下:
十进制/二进制指令助记符示例 (Intel 语法)含义
0 (000) ADD add byte [reg], imm8 加法
1 (001) OR or byte [reg], imm8 逻辑或
2 (010) ADC adc byte [reg], imm8 带进位加法
3 (011) SBB sbb byte [reg], imm8 带借位减法
4 (100) AND and byte [reg], imm8 逻辑与
5 (101) SUB sub byte [reg], imm8 减法
6 (110) XOR xor byte [reg], imm8 逻辑异或
7 (111) CMP cmp byte [reg], imm8 比较 (不存储结果的减法)

FF 操作码扩展(Group 5)对应表

   当主操作码为 FF 时,对应的子操作码 如下:
 
十进制二进制指令助记符描述 (Intel 语法)功能
0 000 INC inc qword/dword [mem] 加 1
1 001 DEC dec qword/dword [mem] 减 1
2 010 CALL call qword/dword [mem] 近间接调用 (Near Indirect)
3 011 CALL call far [mem] 远间接调用 (Far Indirect)
4 100 JMP jmp qword/dword [mem] 近间接跳转 (Near Indirect)
5 101 JMP jmp far [mem] 远间接跳转 (Far Indirect)
6 110 PUSH push qword/dword [mem] 压入栈
7 111 (未定义) - 保留给未来或无效指令
 
       举例: Opcode 0xFF
       0xFF 是一个非常繁忙的操作码,如果不看 MoR/M的Reg 字段,无法确定它要做什么:
  • FF /0  (/0 代表Reg 字段为 000): INC (增加)
  • FF /1  (/1 代表Reg 字段为 001): DEC (减少)
  • FF /2 (/2 代表Reg 字段为 010):  CALL (近调用)
  • FF /4 (/4 代表Reg 字段为 100):  JMP (近跳转)
  • FF /6 (/6 代表Reg 字段为 110):  PUSH (压栈)
    指令解析过程:
    当 CPU 看到 0xFF 时,它会读取下一个字节(ModR/M)。
    如果 ModR/M 字节是 0xD0(二进制 11 010 000): Mod=11: 目标是寄存器。 Reg=010: 对应子操作码 2,即 CALL。 R/M=000: 目标寄存器是 EAX。 最终指令被识别为:call eax

    汇编器如何处理
       当你编写 inc eax 时,汇编器(如 NASM 或 GAS)会自动执行以下逻辑:
    1. 识别 inc 对应的基础操作码 0xFF(如果是 32 位通用寄存器,也有精简版 0x40 系列,但此处以 0xFF 为例)。
    2. 查表得知 INC 属于 Group 4/5,其 Reg 扩展位是 /0
    3. 生成 ModR/M 字节(8-Bit),将中间 3 位设为 000
     

        总结关系表

     
    指令示例主 OpcodeReg 扩展位 (5-3位)完整的操作行为
    ADD r/m, imm 0x81 /0 加法
    OR r/m, imm 0x81 /1 逻辑或
    ADC r/m, imm 0x81 /2 带进位加法
    DIV r/m 0xF7 /6 无符号除法
    IDIV r/m 0xF7 /7 有符号除法



FE 操作码扩展(Group 4)对应表
 
十进制二进制指令助记符典型示例 (NASM)功能
0 000 INC inc byte [rbx] 字节加 1
1 001 DEC dec byte [al] 字节减 1
2 010 (未定义) - 无效或保留
3 011 (未定义) - 无效或保留
4 100 (未定义) - 无效或保留
5 101 (未定义) - 无效或保留
6 110 (未定义) - 无效或保留
7 111 (未定义) -

无效或保留

 

 为什么表格后面大部分是“未定义”?
 与 FF 指令组(支持 CALL, JMP, PUSH)不同,FE 组只定义了 INC 和 DEC。 原因是在 x86 架构中,你无法执行一个“8位宽度的跳转”或“8位宽度的压栈”。跳转目标地址和栈操作必须符合处理器的原生字长(16/32/64位),因此 CALL, JMP, PUSH 只存在于 FF 指令组中。 在计算机体系结构中,原生字长(Native Word Size) 是指 CPU 在单个时钟周期内能够直接处理(运算、传输、存储)的数据的最大位数。它是衡量 CPU 处理能力和寻址能力的最核心标准, 原生字长决定了系统的整体性能表现和内存限制
 
假设你看到机器码:FE 03 (十六进制数)
0xFE: Opcode (Group 4, 8-bit)
0x03: ModR/M 字节 (二进制 00 000 011)
Mod (00): 寄存器间接寻址(无位移)。
Reg/Opcode (000): 对应 INC。
R/M (011): 对应 [EBX] (32位) 或 [RBX] (64位)。
结果: inc byte [rbx](将 RBX 指向的内存中的那个字节加 1)。

 与 80 操作码的区别
 你可能会发现 80 /0 也能实现加法。它们的区别在于:
 80 /0 [imm8] 执行的是 ADD byte [mem], imm8(需要提供一个立即数)。
 FE /0 执行的是 INC byte [mem](不需要立即数,指令更短,且不影响 cf进位标志位 )。

 

 
F7 操作码扩展 (Group 3) 对应表 
当主操作码为 F7 时,子操作码对照如下: 
十进制  二进制指令助记符典型示例 (NASM)功能描述
0 000 TEST test [mem], imm32

位测试:将内存  值与随后的立即数做逻辑与(不存结果)

1 001 (未定义) - 无效指令
2 010 NOT not rdx 按位取反:0 变 1,1 变 0
3 011 NEG neg rax 取负:计算补码(求 0-rax)
4 100 MUL mul rsi 无符号乘法:RAX * RSIRDX:RAX
5 101 IMUL imul rcx 有符号乘法:RAX * RCXRDX:RAX
6 110 DIV div rbx 无符号除法:RDX:RAX / RBX→RAX(商), RDX(余)
7 111 IDIV idiv ecx 有符号除法:EDX:EAX / ECX→EAX(商), EDX(余)
 F7 /0 是该组中唯一一个需要两个操作数的指令。它的结构是:F7 [ModR/M] [Immediate]
 例如:
   F7 03 01 00 00 00 →  test dword [rbx], 1
机器码 F7 03 的分析
F7表示 主操作码(Opcode)。属于 Group 3,用于 16/32/64 位操作数(取决于当前的指令模式)。
03表示 ModR/M 字节。
80x86 Instructions by Opcode(指令操作码、硬编码) - jinzi - 博客园
 将 03 转换为二进制:00 000 011
0 0 0 0 0 0 1 1
7 6 5 4 3 2 1 0
Mod
Reg/Opcode
R/M
ModR/M 字节由三部分构成
1.Mod 占据前2位
2.Reg/Opcode 占据中间的3位
3.R/M 占据后3位
    解释: Mod 第 7-6位 : 00  (十进制0) Reg/Opcode 第 5-3位: 000 (十进制 0)  R/M 第 2-0位 : 011(十进制3)
在 F7 组中,子操作码 /0 对应的是 TEST 指令。  在 32 位或 64 位寻址中,011 对应基址寄存器 EBX (或 RBX)。

下面是 ModR/M 字节的说明
字段位 (Bit)二进制值二进制转为十进制值含义
Mod 7-6 00 0

表示寄存器间接寻址模式,且没有位移量(No displacement)
由于F7是group Opcode(组操作码),所以他对应多个指令,如test、div等指令,但是到底
对应哪个指令呢? 答案是由Reg/Opcode字段的值来确定。
Reg/Opcode 5-3 000 0

   由于主操作码是F7, 这里是扩展码的编码值 0,需要查F7 操作码扩展 (Group 3) 对应表

  这里要注意这个字段,是 Reg/Opcode,表示什么呢?

  如果操作码是确定的操作码例如 89 ,则查看 寄存器编码对照表,因为89是意义已经唯一确定。

 如果操作码是组操作码,则需要查看操作码扩展对应表 ,所以这个Reg/Opcode字段是2个功能的组合。
 到底是用Reg还是Opcode,由操作码来决定。

R/M 2-0 011 3

指定另一个寄存器为 EBX / RBX

寄存器编码对照表中 3代表(32位寄存器ebx)

 


 F6操作码扩展 子操作码 对应表
    在 x86 指令集中,操作码 F6 属于 Group 3 组操作码。它在功能上与 F7 完全一致,但唯一的区别是:
   F6 专门针对 8 位(Byte)操作数进行运算,而 F7 针对 16/32/64 位操作数。 
由于 8 位运算的特殊性,其使用的隐式寄存器始终是 AL 和 AH。
        F6 操作码扩展 (Group 3, 8-bit) 对应表
       要确定 F6 的具体功能,请查看随后的 ModR/M 字节 的第 3、4、5 位
 十进制  二进制 指令助记符典型示例 (NASM)功能描述
0 000 TEST test byte [mem], imm8 位测试AL & imm8(不存结果)
1 001 (未定义) - 无效指令(在某些 CPU 上可能表现为 TEST)
2 010 NOT not bl 按位取反:0 变 1,1 变 0
3 011 NEG neg byte [rax] 取负:计算补码(0 - 内存值)
4 100 MUL mul cl 无符号乘法AL * CLAX
5 101 IMUL imul dl 有符号乘法AL * DL→AX
6 110 DIV div bl 无符号除法AX / BL→AL(商), AH(余)
7 111 IDIV idiv byte [rsi] 有符号除法AX / [rsi]→AL(商), AH(余)
 
1. 隐式寄存器固定
     在 64 位 Windows 开发中,虽然我们习惯用 RAX,但执行 F6 系列指令时,硬件会强制锁定 8 位寄存器: 
  • 乘法 (F6 /4, /5):结果总是存放在 16 位的 AX 中。
  • 除法 (F6 /6, /7):被除数必须预先存放在 AX 中。执行后,商在 AL,余数在 AH 
2. TEST 指令的特殊结构 
   F6 /0 是该组中唯一需要额外字节的指令。 (F6表示 /0 表示扩展码为0 ,F6 /0 是一种用来表示扩展码的方式)
  • 机器码示例F6 00 FF → test byte [rax], 0xFF
    F6表示是一个字节长度的逻辑运算,F6 /0 表示是扩展码
  • 它常用于检查某个状态位(Flag),且不会破坏 AL 的内容。 
3. 符号扩展注意 (IDIV) 
         在编写汇编时,如果你要用 idiv bl 处理一个存储在 AL 里的有符号数,必须先将 AL 符号扩展到 AX 
  • 正确做法:先执行 CBW (Convert Byte to Word) 指令,然后再执行 F6 /7 
4. 机器码识别实例:F6 F3 
  • F6: Group 3 Opcode (8-bit)。
  • F3: 二进制 11 110 011
    • 中间三位是 110 (十进制 6)→ DIV
    • 最后三位是 011→ BL 寄存器。
  • 结果: div bl(计算 AX / BL)。 
总结对照 
  • F6 /4 (MUL): 结果在 AX
  • F6 /6 (DIV): 商在 AL,余数在 AH
  • F6 /3 (NEG): 单个字节变号。 
在现代处理器中,8 位运算 (F6) 的延迟通常极低,但仍需注意避免“部分寄存器停顿”(Partial Register Stall),即在修改了 AL 后立即读取整个 RAX 的操作。

 


 
 
 
posted @ 2026-01-02 11:15  jinzi  阅读(3)  评论(0)    收藏  举报