好久没有写博客了,感觉时间飞逝,转眼又到了一年的最后一个月了,不管怎么说这一年自己还是挺有收获的,当然这些是留在年末总结的时候来写的。这篇文章继续之前“那些事儿”系列,这次要介绍的,是关于X86指令编码的“事儿”。
如果你之前有见过或者听说过prefix
,opcode
,ModR/M
,escape opcode
这些词,但是其实并不是那么清楚它们是什么意思,那么恭喜你,今天你就将得到它们!首先还是先说明下这篇博客主要参考的资料,依旧是来自Intel的文档(这里再次安利下Intel文档,写的非常详细),不过这次是第二册的几个章节(Volume 2 - Chapter 2~4, 以及Appendix A)。
好了,废话不说,直接进入正题。
我们知道,CPU在运行的时候认的是一个个二进制的数字(或者人类为了方便说明,一般会把它们先转换成一个个十六进制的数字)。那么,就需要有一套编码机制,告诉计算机,这些数字代表的意思是什么。
我们还知道,CPU一般执行的都是一条条的指令,每个指令都有其相对应的含义,以及一些它们所需的附加信息,比如数据读取的原地址,或者写入的目标地址。而CPU就是用之前所谓的编码机制,来识别并且运行这一条条指令的。
因此,将这两个合起来,就是我们今天所要讨论的指令编码
,如果用一个英文词的话,可以用encoding
,如果用两个英文词的话,那就是instruction encoding
,如果还要加第三个词的话,就变成了x86 instructino encoding
。好了,我不无聊了,继续往下聊吧。
你之前可能听说过,x86是一种CISC指令集,CISC的全称是“Complex Instruction Set Computer”,表示的是一种复杂的指令集,其中一个最重要的复杂性在于在这个指令集中,指令是不定长的,要使得CPU在这种不定长的指令集里面确定每一条指令的含义,就需要一种特定的指令格式,下图显示了Intel 64
和IA-32
架构下的指令格式,当然,这两种架构所采用的都是x86指令编码:
可以看出,在x86的指令格式中,每条指令最多会有六部分组成,在这六部分中,只有opcode
是必须的,其它的部分都是可选的。接下来我们一个部分一个部分进行介绍:
prefix
说白了就是对本条指令进行一个修饰,主要包含了以下四组可能的prefix:
lock
和 repeat
其中,LOCK prefix保证该条指令对共享内存的访问是独占的;而repeat prefixes 表示这条指令会重复执行多次,直到某个条件满足位置。其中第二种repeat prefix只能用在对string的操作,或者对I/O的操作上。
segment override
,branch hints
和 bound
其中,segment override prefix会在执行这条指令的时候将默认的段寄存器给换掉;branch hints prefix主要应用在条件跳转指令(Jcc)中,可以协助CPU进行指令的prefetch;而bound prefix主要是用intel MPX硬件特性上。
operand-size override
这个prefix主要是在解析指令的操作数的时候,可以在十六位或者三十二位的操作数大小间进行切换。
address-size override
这个prefix主要是在进行指令寻址的时候,可以在十六位或者三十二位的地址大小中进行切换。
接下来是最重要的opcode,基本上整个指令编码都是围绕opcode来进行的,所以opcode是整个指令的核心。
一个opcode可能的长度为1、2或3,除此之外,在之后会提到的ModR/M
所占用的那个byte中,还有可能会有3个bits来表示opcode的一部分内容,当然,这3个bits主要用来定义一些额外的信息,包括:direction of operation, size of displacements, register encoding, condition codes, or sign extension,至于这三个bits是否属于opcode,或者它们表示什么信息,是由不同的opcode来决定的。
一个opcode可以由一个byte组成,我们称之为1-byte opcode
,当然,与之对应的,就有2-bytes opcode
和3-bytes opcode
。其中,后两者一般会有一个被称为escape opcode
的byte进行引导,该byte的数值是0FH
。所以,一般情况下,2-bytes opcode
就是0FH
后面再加一个byte,而3-bytes opcode
就是0FH
后面再加两个bytes。
除此之外,还有一种可能是,在这个escape opcode
之前,还有可能会出现一个被称为mandatory prefix
的byte,这不属于我们之前提到的任何一种prefix,我感觉它一般是在某个新的硬件特性出来的时候,为该硬件特性新增加指令的一种做法。这个byte可能的取值为66H
,F2H
或者F3H
。很有趣的一点是,66H
也是lock prefix
的取值,而F2H
和F3H
也是repeat prefix
的取值。所以说,当遇到这些prefix的时候,还得需要根据之后的opcode来判断这是属于哪种prefix。
这里举一个例子,比如一个指令叫PHADDW
,是在XMM
特性中的一条指令,它的编码为66 0F 38 01
,因此,它是一个3-bytes opcode
,66
是mandatory prefix
(而不是lock prefix
),0F
是escape opcode,而最后两个bytes38 01
就是另外两个opcode bytes。
ModR/M
主要是在对指令中的操作数进行寻址的时候需要用到的域,它由一个byte组成,如之前的图所示,ModR/M
这一个byte又被分成了三部分:mod
(由6~7两个bits组成),reg/opcode
(由3~5三个bits组成),r/m
(由0~2三个bits组成)。其中,如前所说,reg/opcode
可以表示某个寄存器,或者作为opcode的三个额外bits进行使用。而mod
和r/m
结合,可以产生32种可能的值,包括了8个寄存器和24中寻址模式。
另外,在ModR/M
三个部分可能的组合中,还有可能会涉及到另外一个寻址模式,被称为SIB,SIB也是由三部分组成:Scale,Index和Base。一般如果涉及到SIB,则相关的值就可以通过base + index * scale
计算出来。
接下来,我们来详细解释下如何利用ModR/M
和SIB
进行寻址。这里主要有三张非常关键的表,可以说,利用ModR/M
和SIB
进行寻址都可以通过查这三张表完成。
其中,可以利用第一张表对16位的地址寻址进行查询,利用第二张表对32位的地址寻找进行查询,而第三张对某些需要用到SIB的地址寻址进行查询。对于ModR/M
表(即前两张表),我们看到中间一部分列出了00~FF所有的数字,这些数字是即为一个byte的所有可能值,该byte的组成之前也提到了,如下图所示:
所以,当得到一个ModR/M
的值,就可以查询这张表,这里举个例子,比如ModR/M
byte的值为CC
,那么我们找到CC
对应的行和列,可以发现,它对应的行为ESP/SP/AHMM4/XMM4
,列为CL/CX/ECX/MM1/XMM1/1/001
。那么就将范围限定在了这几个寄存器上,当然,至于它最后要选哪个寄存器,则是由opcode来决定的。
同样的,对于需要用到SIB
表(即最后一张表)的指令,同样的,我们获得SIB
的数值(比如CC
),发现它对应的行为[ECX*8]
,列为ESP
,即表示SIB最终的值的计算方法为[ESP]+ECX*8
。
某些指令会在最后要求有一个用于计算内存地址的值,或者一个立即数。这部分很直接,就不解释了。
上面提到的都是16位或者32位的寻址,而我们现在主要用的系统都是64位的,那么,在64位系统下的寻址又是怎么样的呢?
这里又要引入一个新的prefix:REX prefix
,如下图所示:
这里需要注意的是,每条指令最多只能有一个表示REX prefix
的byte,而且这个byte必须紧紧贴着opcode,不能放在其他的prefix之前。另外,REX prefix
的格式如下图所示:
其中,最高的4位是固定的值(0100
),低4位分别代表了operand size,以及是否修改ModR/M
和SIB
的值。比如说下面四张图:
如果对应的R\X\B bit被设置上了,则会根据opcode来修改对应的ModR/M
中r/m
或者reg/opcode
域中的值,或者SIB
中base
或者index
域中的值。我们知道,这些域其实就是指定了某些寄存器,而基于REX prefix
的修改其实就是将16或者32位的寄存器换成64位的,比如把EAX
换成RAX
这样。
好了,最关键的技能来了,教你如何看懂opcode table!
其实也就三张opcode table(或者叫opcode map也行),就是前面所说的1-byte
, 2-bytes
和3-bytes
。比如我们随便截一张图:
这是一个1-byte
opcode table,如果我们的opcode是85
,则找到第八行第五列对应的那个小格子:TEST (Ev, Gv)
。那么这个是什么意思呢?首先,TEST
是opcode,(Ev, Gv)
是这条opcode的寻址模式。E
,v
,G
这些都是缩写,在opcode map中有很多缩写,这篇博文最后的附件中显示了它们的含义。可以看到,E
表示opcode之后会跟一个ModR/M
byte,用来表示操作数,该操作数可以是一个寄存器或者一个内存地址,如果是内存地址的话,该地址可以通过之后的SIB和displacement算出来;v
表示这个操作数可能是一个16位的word,32为的doubleword,或者64位的quadword,具体情况要根据operand-size的属性决定(比如是否有operand-size override prefix
,或者REX.W bit是否被置上等);G
代表ModR/M
中的reg
域会选择一个通用寄存器。
因此,有了这个opcode table之后,就可以接着往下去看对应的ModR/M
、SIB
以及可能存在的displacement了,然后再去查之前ModR/M
和SIB
相关的表,就能得到整条指令的意思,以及该指令所对应的寻址方式。
其实查询opcode table的整个流程特别简单。如果你想要模拟一个指令,你就在opcode table中搜索这条指令,然后找到其对应的小格子(比如前面例子中的那个TEST (Ev, Gv)
),然后根据前面所的方法再去看后面的ModR/M
和SIB
等内容,一个个往下走就行了。
其实到这里,x86的指令编码部分基本上都讲完了。之所以还要加上这一小节,是想举个例子,练习练习。
我们知道,在虚拟化环境中,如果非特权级环境中的客户虚拟机执行了一条特权级指令,则会引发下陷,进入特权级别中的虚拟机监控器,由虚拟机监控器对该指令进行模拟。在虚拟化环境中有14条指令是会无条件引发虚拟机下陷(VMExit)的,它们是CPUID
, GETSEC
, INVD
, XSETBV
, INVEPT
, INVVPID
, VMCALL
, VMCLEAR
, VMLAUNCH
, VMPTRLD
, VMPTRST
, VMRESUME
, VMXOFF
, VMXON
。我们就以VMCLEAR
这条指令为例,看看虚拟机监控器里面要如何对其进行模拟。
当然,最简单的办法就是在发生VMExit的时候直接去读取相应RIP
的值,然后获得指令的opcode和其它内容,通过查表我们可以知道VMCLEAR
指令的opcode是66 0F C7 /6
,然后通过查opcode table,我们发现以下内容:
这里需要注意的一点是,这个表示一个扩展表,是Intel为其它新添加的硬件特性重新扩展得到的opcode table,其实我们之前提到过,这里66
是一个mandatory prefix
,所以它的opcode是0F C7
,另外,ModR/M
中的reg/opcode
用于补充opcode,所以这里的/6
表示的是ModR/M
中reg/opcode
的值。
不管怎么样,VMCLEAR
有3个bytes的opcode,然后从opcode table查询出来的值为VMCLEAR (Mq)
,我们查询缩写表可以知道,M
表示ModR/M
用于表示内存寻址,而q
表示一个64位的quadword(不管operand-size是什么)。因此,我们知道在这个opcode之后一定至少有一个表示ModR/M
的byte,所以我们就可以继续读这个byte,然后通过ModR/M
的寻址表来确定操作数的内存地址是什么了。具体的这里就不阐述了。
不过这里还需要提一点的是,其实最后我们发现,VMCLEAR
采用的是SIB寻址的模式,而且对于它的寻址,可以不需要通过opcode table来进行,因为在虚拟化环境中,虚拟机下陷会将需要的信息填入VMCS的某些数据结构中,比如,对于特权指令产生的下陷,会将和这个指令相关的信息(特别是操作数寻址的信息)存入一个叫VM-exit instruction information
的域中,这个域对于每种指令都会提供不同的存储信息的格式,因此我们在虚拟机监控器中对这些指令进行模拟的时候,其实是可以直接从VM-exit instruction information
域中获取所需的信息的。
附:opcode table中缩写码的含义:
Code | Description |
---|---|
A | Direct address: the instruction has no ModR/M byte; the address of the operand is encoded in the instruction. No base register, index register, or scaling factor can be applied (for example, far JMP (EA)). |
B | The VEX.vvvv field of the VEX prefix selects a general purpose register. |
C | The reg field of the ModR/M byte selects a control register (for example, MOV (0F20, 0F22)). |
D | The reg field of the ModR/M byte selects a debug register (for example, MOV (0F21,0F23)). |
E | A ModR/M byte follows the opcode and specifies the operand. The operand is either a general-purpose register or a memory address. If it is a memory address, the address is computed from a segment register and any of the following values: a base register, an index register, a scaling factor, a displacement. |
F | EFLAGS/RFLAGS Register. |
G | The reg field of the ModR/M byte selects a general register (for example, AX (000)). |
H | The VEX.vvvv field of the VEX prefix selects a 128-bit XMM register or a 256-bit YMM register, determined by operand type. For legacy SSE encodings this operand does not exist, changing the instruction to destructive form. |
I | Immediate data: the operand value is encoded in subsequent bytes of the instruction. |
J | The instruction contains a relative offset to be added to the instruction pointer register (for example, JMP (0E9), LOOP). |
L | The upper 4 bits of the 8-bit immediate selects a 128-bit XMM register or a 256-bit YMM register, deter- mined by operand type. (the MSB is ignored in 32-bit mode) |
M | The ModR/M byte may refer only to memory (for example, BOUND, LES, LDS, LSS, LFS, LGS, CMPXCHG8B). |
N | The R/M field of the ModR/M byte selects a packed-quadword, MMX technology register. |
O | The instruction has no ModR/M byte. The offset of the operand is coded as a word or double word (depending on address size attribute) in the instruction. No base register, index register, or scaling factor can be applied (for example, MOV (A0–A3)). |
P | The reg field of the ModR/M byte selects a packed quadword MMX technology register. |
Q | A ModR/M byte follows the opcode and specifies the operand. The operand is either an MMX technology register or a memory address. If it is a memory address, the address is computed from a segment register and any of the following values: a base register, an index register, a scaling factor, and a displacement. |
R | The R/M field of the ModR/M byte may refer only to a general register (for example, MOV (0F20-0F23)). |
S | The reg field of the ModR/M byte selects a segment register (for example, MOV (8C,8E)). |
U | The R/M field of the ModR/M byte selects a 128-bit XMM register or a 256-bit YMM register, determined by operand type. |
V | The reg field of the ModR/M byte selects a 128-bit XMM register or a 256-bit YMM register, determined by operand type. |
W | A ModR/M byte follows the opcode and specifies the operand. The operand is either a 128-bit XMM register, a 256-bit YMM register (determined by operand type), or a memory address. If it is a memory address, the address is computed from a segment register and any of the following values: a base register, an index register, a scaling factor, and a displacement. |
X | Memory addressed by the DS:rSI register pair (for example, MOVS, CMPS, OUTS, or LODS). |
Y | Memory addressed by the ES:rDI register pair (for example, MOVS, CMPS, INS, STOS, or SCAS). |
Code | Description |
---|---|
a | Two one-word operands in memory or two double-word operands in memory, depending on operand-size attribute (used only by the BOUND instruction). |
b | Byte, regardless of operand-size attribute. |
c | Byte or word, depending on operand-size attribute. |
d | Doubleword, regardless of operand-size attribute. |
dq | Double-quadword, regardless of operand-size attribute. |
p | 32-bit, 48-bit, or 80-bit pointer, depending on operand-size attribute. pd 128-bit or 256-bit packed double-precision floating-point data. |
pi | Quadword MMX technology register (for example: mm0). |
ps | 128-bit or 256-bit packed single-precision floating-point data. |
q | Quadword, regardless of operand-size attribute. |
Quad-Quadword (256-bits), regardless of operand-size attribute. s 6-byte or 10-byte pseudo-descriptor. | |
sd | Scalar element of a 128-bit double-precision floating data. |
ss | Scalar element of a 128-bit single-precision floating data. |
si | Doubleword integer register (for example: eax). |
v | Word, doubleword or quadword (in 64-bit mode), depending on operand-size attribute. |
w | Word, regardless of operand-size attribute. |
x | dq or qq based on the operand-size attribute. |
y | Doubleword or quadword (in 64-bit mode), depending on operand-size attribute. |
z | Word for 16-bit operand-size or doubleword for 32 or 64-bit operand-size. |