i386 的指令结构 (for PA2)
先用一张图简单表示单条指令的构成:
1 2 3 4 5 6 7 8 | |
我们可以简单的把上面的指令结构拆分为三部分:一系列可选的前缀 + opcode + 一系列可选的字段
一条指令可以只有 opcode,比如 NOP 指令(操作码 0x90);所有的指令都至少有 opcode
举一个例子:66 c7 84 99 00 e0 ff ff 01 00
其组成部分的划分如下:
1 2 3 4 5 6 | |
对应的汇编指令为 movw $1, -0x2000(%ecx, %ebx, 4) / mov word ptr [ecx + ebx*4 - 0x2000], 1
前缀部分
1 2 3 4 5 6 7 8 | |
不一样的划分
个人认为 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(段覆盖与分支提示)
- 段覆盖前缀:
0x2E:CS(代码段)覆盖前缀(也算分支提示前缀)0x36:SS(堆栈段)覆盖前缀0x3E:DS(数据段)覆盖前缀(也算分支提示前缀)0x26:ES(附加段)覆盖前缀0x64:FS(附加段)覆盖前缀0x65:GS(附加段)覆盖前缀
前两个 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 地址模式。
有两个约定:
-
每组内只能有一个有效前缀(如果同一组内写了多个前缀,则只取最后一个 / 未定义)
-
不同组的前缀的出现顺序没有限制,但是约定上采用上述展示的顺序
在NEMU中,我们只考虑 Group 3 操作数长度前缀,其值为0x66。当opcode所对应的操作数长度可变时(16位或32位),若opcode前面出现0x66,则操作数长度临时改变为16位,否则为32位。
opcode
opcode 是所有指令的必选项,决定了 “这是一条什么指令”
有一点需要注意:即使是相同的指令,也会出现不同的 opcode,例如 MOV 指令:
1 2 3 | |
参考隔壁 RISC-V 也会为 ADD 和 ADDI 分别设置不同的 opcode(通常在助记符上有明显的区分),但是 i386 的这种现象更加常见与复杂(通常不会在助记符上明显区分,比如上面的例子都为 MOV)
字段部分
1 2 3 4 5 6 7 8 | |
和前缀不同的是,这里的各个字段的顺序不能调换(存在先后依赖关系)
我们一个一个讨论:
ModR/M 字段
ModR/M 最主要作用是对指令的 operands (操作数)提供寻址,另一个作用是对 Opcode 进行补充(可以先忽略这一条)。
我们先来看 ModR/M 的组成:
1 2 3 | |
-
Mod 字段用来指示操作数的寻址模式。它决定了操作数是否存储在寄存器中,或者是否需要通过内存来访问。
-
注意 Mod =
00的时候存在一个特例,这在之后的表格中会提到Mod 寻址模式 描述 00[base]提供 [base]形式的内存寻址01[base + disp8]提供 [base + disp8]形式的内存寻址10[base + disp32]提供 [base + disp32]形式的内存寻址11register提供寄存器寻址 -
Reg/Opcode 字段可能指示一个寄存器,也可能是 opcode 的扩展
-
如果指示一个寄存器,那么有下面的表格:
Reg/Opcode 32位寄存器 16位寄存器 8位寄存器 000EAXAXAL001ECXCXCL010EDXDXDL011EBXBXBL100ESPSPAH101EBPBPCH110ESISIDH111EDIDIBH -
(这里不考虑作为 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 | |
对应了 [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 的内存寻址
displacement 字段
由 ModR/M 的 Mod 字段 or 之前提到的特例决定是否使用该字段,而 displacement 这个字段决定了 disp 偏移值是什么
(这个字段可以是 2 bytes 但是不常见,因此只考虑 0, 1, 4 bytes 的情况)
immediate 字段
由 opcode 决定是否使用该字段,该字段存储立即数
