Skip to content

i386 的指令结构 (for PA2)

部分内容使用了 LLM 生成

以及参考链接: this

and this

and this

先用一张图简单表示单条指令的构成:

1
2
3
4
5
6
7
8
+-----------+-----------+-----------+--------+------+------+------+------------+-----------+
|instruction| address-  | operand-  |segment |opcode|ModR/M| SIB  |displacement| immediate |
|  prefix   |size prefix|size prefix|override|      |      |      |            |           |
|-----------+-----------+-----------+--------+------+------+------+------------+-----------| 
|   0 OR 1  |   0 OR 1  |   0 OR 1  | 0 OR 1 |1 OR 2|0 OR 1|0 OR 1| 0,1,2 OR 4 |0,1,2 OR 4 |
| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -| 
|                                       number of bytes                                    | 
+------------------------------------------------------------------------------------------+

我们可以简单的把上面的指令结构拆分为三部分:一系列可选的前缀 + opcode + 一系列可选的字段

一条指令可以只有 opcode,比如 NOP 指令(操作码 0x90);所有的指令都至少有 opcode

举一个例子:66 c7 84 99 00 e0 ff ff 01 00

其组成部分的划分如下:

1
2
3
4
5
6
+-----------+-----------+-----------+--------+------+------+------+------------+-----------+
|instruction| address-  |  operand- |segment |opcode|ModR/M| SIB  |displacement| immediate |
|  prefix   |size prefix|size prefix|override|      |      |      |            |           |
|-----------+-----------+-----------+--------+------+------+------+------------+-----------|
|                            66                 c7     84     99    00 e0 ff ff    01 00   |
+------------------------------------------------------------------------------------------+

对应的汇编指令为 movw $1, -0x2000(%ecx, %ebx, 4) / mov word ptr [ecx + ebx*4 - 0x2000], 1

前缀部分

1
2
3
4
5
6
7
8
+-------------+-----------+-----------+--------+
|Lock / Repeat| address-  | operand-  |segment |
|   prefix    |size prefix|size prefix|override|
|-------------+-----------+-----------+--------+
|   0 OR 1    |  0 OR 1   |   0 OR 1  | 0 OR 1 |        // 所有前缀都是可选项
| - - - - - - - - - - - - - - - - - - - - - - -| 
|               number of bytes                | 
+----------------------------------------------+

不一样的划分

个人认为 instruction prefix 这种说法并不准确,它应该是指令前缀的一个总称;相对应的, Lock / Repeat 在原来的 PA 手册中并没有被提到,所以我主观地将 instruction prefix 换成了 Lock / Repeat

Group 1 — Lock / Repeat(同步或串行化 / 重复)

  • 0xF0 = LOCK(对内存写操作做原子化/总线锁)
  • 0xF2 = REPNE / REPNZ(在字符串指令中表示条件重复;在 SSE 指令中也可作为 opcode 扩展)
  • 0xF3 = REP / REPE / REPZ(字符串重复;在某些 SSE 指令中作为 opcode map)

Group 2 — Segment override / Branch hint(段覆盖与分支提示)

  • 段覆盖前缀:
  • 0x2ECS(代码段)覆盖前缀(也算分支提示前缀)
  • 0x36SS(堆栈段)覆盖前缀
  • 0x3EDS(数据段)覆盖前缀(也算分支提示前缀)
  • 0x26ES(附加段)覆盖前缀
  • 0x64FS(附加段)覆盖前缀
  • 0x65GS(附加段)覆盖前缀

前两个 Group 暂时不用涉及学习,事实上 PA 中 NEMU 的实现也忽略了这些前缀的实现

Group 3 — Operand-size override

  • 0x66(如果使用了这个 Group 的前缀,就一定是 0x66
  • 在 32-bit 模式下把操作数大小从默认 32-bit 切换到 16-bit(影响寄存器宽度和立即数/存取宽度);在 16-bit 模式下切换到 32-bit。

Group 4 — Address-size override

  • 0x67(如果使用了这个 Group 的前缀,就一定是 0x67
  • 在 32-bit 模式下把地址计算(ModR/M/SIB 解释)从 32-bit 切换到 16-bit(影响是否出现 SIB、disp 长度等);在 16-bit 模式下切换到 32-bit 地址模式。

有两个约定:

  1. 每组内只能有一个有效前缀(如果同一组内写了多个前缀,则只取最后一个 / 未定义)

  2. 不同组的前缀的出现顺序没有限制,但是约定上采用上述展示的顺序

在NEMU中,我们只考虑 Group 3 操作数长度前缀,其值为0x66。当opcode所对应的操作数长度可变时(16位或32位),若opcode前面出现0x66,则操作数长度临时改变为16位,否则为32位。

opcode

opcode 是所有指令的必选项,决定了 “这是一条什么指令”

有一点需要注意:即使是相同的指令,也会出现不同的 opcode,例如 MOV 指令:

1
2
3
MOV r32, imm32:0xB8(将立即数加载到寄存器中)
MOV r/m32, r32:0x89(将一个寄存器的值复制到另一个寄存器或内存中)
MOV r32, [mem]:0x8B(将内存中的值加载到寄存器中)

参考隔壁 RISC-V 也会为 ADD 和 ADDI 分别设置不同的 opcode(通常在助记符上有明显的区分),但是 i386 的这种现象更加常见与复杂(通常不会在助记符上明显区分,比如上面的例子都为 MOV)

字段部分

1
2
3
4
5
6
7
8
+------+------+------------+-----------+
|ModR/M| SIB  |displacement| immediate |
|      |      |            |           |
+------+------+------------+-----------| 
|0 OR 1|0 OR 1| 0,1,2 OR 4 |0,1,2 OR 4 |
| - - - - - - - - - - - - - - - - - - -| 
|           number of bytes            | 
+--------------------------------------+

和前缀不同的是,这里的各个字段的顺序不能调换(存在先后依赖关系)

我们一个一个讨论:

ModR/M 字段

ModR/M 最主要作用是对指令的 operands (操作数)提供寻址,另一个作用是对 Opcode 进行补充(可以先忽略这一条)。

我们先来看 ModR/M 的组成:

1
2
3
+---+---+---+---+---+---+---+---+
|  Mod  |Reg/Opcode |    R/M    |
+---+---+---+---+---+---+---+---+
  • Mod 字段用来指示操作数的寻址模式。它决定了操作数是否存储在寄存器中,或者是否需要通过内存来访问。

  • 注意 Mod = 00 的时候存在一个特例,这在之后的表格中会提到

    Mod 寻址模式 描述
    00 [base] 提供 [base] 形式的内存寻址
    01 [base + disp8] 提供 [base + disp8] 形式的内存寻址
    10 [base + disp32] 提供 [base + disp32] 形式的内存寻址
    11 register 提供寄存器寻址
  • Reg/Opcode 字段可能指示一个寄存器,也可能是 opcode 的扩展

  • 如果指示一个寄存器,那么有下面的表格:

    Reg/Opcode 32位寄存器 16位寄存器 8位寄存器
    000 EAX AX AL
    001 ECX CX CL
    010 EDX DX DL
    011 EBX BX BL
    100 ESP SP AH
    101 EBP BP CH
    110 ESI SI DH
    111 EDI DI BH
  • (这里不考虑作为 opcode 的扩展的情况)

  • R/M 字段用于指定操作数的来源/目标。它的含义随着 Mod 字段的不同而变化:

  • 从寄存器选择的角度来说,R/M 字段和 Reg 字段几乎没有区别
  • Mod 字段会规定 R/M 字段被指定的寄存器如何使用

考虑到 Mod 和 R/M 字段存在关联,这里给出一张更具体的表格:

注意 Mod = 00 R/M = 101 的特例

Mod R/M 寻址方式(以 32 位为例) Mod R/M 寻址方式(以 32 位为例)
00 000 [eax] 01 000 [eax + disp8]
001 [ecx] 001 [ecx + disp8]
010 [edx] 010 [edx + disp8]
011 [ebx] 011 [ebx + disp8]
100 [SIB] 100 [SIB + disp8]
101 [eip + disp32] 101 [ebp + disp8]
110 [esi] 110 [esi + disp8]
111 [edi] 111 [edi + disp8]
10 000 [eax + disp32] 11 000 eax
001 [ecx + disp32] 001 ecx
010 [edx + disp32] 010 edx
011 [ebx + disp32] 011 ebx
100 [SIB + disp32] 100 esp
101 [ebp + disp32] 101 ebp
110 [esi + disp32] 110 esi
111 [edi + disp32] 111 edi

ModR/M 有没有指定哪个是源操作数,哪个是目的操作数?

并没有,事实上是由 opcode 决定了 ModR/M 的 Reg 与 R/M 字段中哪个是源操作数/目的操作数

比如,同样是 MOV 指令,有不同的opcode:

MOV r/m32, r32(操作码 89)中,源操作数是寄存器 r32(在 REG 字段),目的操作数是寄存器或内存 r/m32(在 R/M 字段)。

MOV r32, r/m32(操作码 8B)中,源操作数是寄存器或内存 r/m32(在 R/M 字段),目的操作数是寄存器 r32(在 REG 字段)。

不难发现这两条指令对操作数位置的约定是相反的,(因为 r/m32 只有 R/M 字段可以实现)

SIB 字段

由 ModR/M 的描述不难发现,这个字段只在 ModR/M 的 Mod 不为 11 且 R/M 为 100 时才会使用

1
2
3
+---+---+---+---+---+---+---+---+
| Scale |   Index   |   Base    |
+---+---+---+---+---+---+---+---+

对应了 [Base + Index * Scale + Disp] 的内存寻址方式,其中 Index 与 Base 字段都表示寄存器(参照 ModR/M:Reg 的表格);Scale 字段为 00 01 10 11 时分别表示 1 2 4 8 的缩放因子;Disp 已经由 Mod 字段决定是否存在,由之后的 displacement 字段决定值

特例1:当 Index = 100 时,看作“Index = NULL”(不使用变址寄存器),而不是 “Index = ESP”

特例2:当 Base = 101 且 Mod = 00 时,看作“Base = NULL”(不使用基址寄存器),而不是 “Base = EBP”,同时启用 Disp32 的使用

两个特例可以同时存在,此时可以实现仅 Disp32 的内存寻址

参考:X86-64 Instruction Encoding - OSDev Wiki

image.png

displacement 字段

由 ModR/M 的 Mod 字段 or 之前提到的特例决定是否使用该字段,而 displacement 这个字段决定了 disp 偏移值是什么

(这个字段可以是 2 bytes 但是不常见,因此只考虑 0, 1, 4 bytes 的情况)

immediate 字段

由 opcode 决定是否使用该字段,该字段存储立即数


一些例子