Mctrain's Blog

What I learned in IT, as well as thought about life

X86指令编码的那些事儿

| Comments

好久没有写博客了,感觉时间飞逝,转眼又到了一年的最后一个月了,不管怎么说这一年自己还是挺有收获的,当然这些是留在年末总结的时候来写的。这篇文章继续之前“那些事儿”系列,这次要介绍的,是关于X86指令编码的“事儿”。

如果你之前有见过或者听说过prefixopcodeModR/Mescape 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 64IA-32架构下的指令格式,当然,这两种架构所采用的都是x86指令编码:

Instruction Format

可以看出,在x86的指令格式中,每条指令最多会有六部分组成,在这六部分中,只有opcode是必须的,其它的部分都是可选的。接下来我们一个部分一个部分进行介绍:

Instruction prefix

prefix说白了就是对本条指令进行一个修饰,主要包含了以下四组可能的prefix:

  • 第一组:lockrepeat

其中,LOCK prefix保证该条指令对共享内存的访问是独占的;而repeat prefixes 表示这条指令会重复执行多次,直到某个条件满足位置。其中第二种repeat prefix只能用在对string的操作,或者对I/O的操作上。

  • 第二组:segment overridebranch hintsbound

其中,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是整个指令的核心。

一个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 opcode3-bytes opcode。其中,后两者一般会有一个被称为escape opcode的byte进行引导,该byte的数值是0FH。所以,一般情况下,2-bytes opcode就是0FH后面再加一个byte,而3-bytes opcode就是0FH后面再加两个bytes。

除此之外,还有一种可能是,在这个escape opcode之前,还有可能会出现一个被称为mandatory prefix的byte,这不属于我们之前提到的任何一种prefix,我感觉它一般是在某个新的硬件特性出来的时候,为该硬件特性新增加指令的一种做法。这个byte可能的取值为66HF2H或者F3H。很有趣的一点是,66H也是lock prefix的取值,而F2HF3H也是repeat prefix的取值。所以说,当遇到这些prefix的时候,还得需要根据之后的opcode来判断这是属于哪种prefix。

这里举一个例子,比如一个指令叫PHADDW,是在XMM特性中的一条指令,它的编码为66 0F 38 01,因此,它是一个3-bytes opcode66mandatory prefix(而不是lock prefix),0F是escape opcode,而最后两个bytes38 01就是另外两个opcode bytes。

ModR/M 和 SIB

ModR/M主要是在对指令中的操作数进行寻址的时候需要用到的域,它由一个byte组成,如之前的图所示,ModR/M这一个byte又被分成了三部分:mod(由6~7两个bits组成),reg/opcode(由3~5三个bits组成),r/m(由0~2三个bits组成)。其中,如前所说,reg/opcode可以表示某个寄存器,或者作为opcode的三个额外bits进行使用。而modr/m结合,可以产生32种可能的值,包括了8个寄存器和24中寻址模式。

另外,在ModR/M三个部分可能的组合中,还有可能会涉及到另外一个寻址模式,被称为SIB,SIB也是由三部分组成:Scale,Index和Base。一般如果涉及到SIB,则相关的值就可以通过base + index * scale计算出来。

接下来,我们来详细解释下如何利用ModR/MSIB进行寻址。这里主要有三张非常关键的表,可以说,利用ModR/MSIB进行寻址都可以通过查这三张表完成。

16-bit ModR/M

32-bit ModR/M

32-bit SIB

其中,可以利用第一张表对16位的地址寻址进行查询,利用第二张表对32位的地址寻找进行查询,而第三张对某些需要用到SIB的地址寻址进行查询。对于ModR/M表(即前两张表),我们看到中间一部分列出了00~FF所有的数字,这些数字是即为一个byte的所有可能值,该byte的组成之前也提到了,如下图所示:

ModR/M 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

Displacement 和 Immediate

某些指令会在最后要求有一个用于计算内存地址的值,或者一个立即数。这部分很直接,就不解释了。


IA-32e mode

上面提到的都是16位或者32位的寻址,而我们现在主要用的系统都是64位的,那么,在64位系统下的寻址又是怎么样的呢?

这里又要引入一个新的prefix:REX prefix,如下图所示:

REX prefix

这里需要注意的是,每条指令最多只能有一个表示REX prefix的byte,而且这个byte必须紧紧贴着opcode,不能放在其他的prefix之前。另外,REX prefix的格式如下图所示:

REX prefix format

其中,最高的4位是固定的值(0100),低4位分别代表了operand size,以及是否修改ModR/MSIB的值。比如说下面四张图:

REX prefix 1

REX prefix 2

REX prefix 3

REX prefix 4

如果对应的R\X\B bit被设置上了,则会根据opcode来修改对应的ModR/Mr/m或者reg/opcode域中的值,或者SIBbase或者index域中的值。我们知道,这些域其实就是指定了某些寄存器,而基于REX prefix的修改其实就是将16或者32位的寄存器换成64位的,比如把EAX换成RAX这样。


opcode table

好了,最关键的技能来了,教你如何看懂opcode table!

其实也就三张opcode table(或者叫opcode map也行),就是前面所说的1-byte, 2-bytes3-bytes。比如我们随便截一张图:

opcode table 1

这是一个1-byte opcode table,如果我们的opcode是85,则找到第八行第五列对应的那个小格子:TEST (Ev, Gv)。那么这个是什么意思呢?首先,TEST是opcode,(Ev, Gv)是这条opcode的寻址模式。EvG这些都是缩写,在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/MSIB以及可能存在的displacement了,然后再去查之前ModR/MSIB相关的表,就能得到整条指令的意思,以及该指令所对应的寻址方式。

其实查询opcode table的整个流程特别简单。如果你想要模拟一个指令,你就在opcode table中搜索这条指令,然后找到其对应的小格子(比如前面例子中的那个TEST (Ev, Gv)),然后根据前面所的方法再去看后面的ModR/MSIB等内容,一个个往下走就行了。


虚拟化环境中的指令模拟

其实到这里,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,我们发现以下内容:

opcode table vmclear

这里需要注意的一点是,这个表示一个扩展表,是Intel为其它新添加的硬件特性重新扩展得到的opcode table,其实我们之前提到过,这里66是一个mandatory prefix,所以它的opcode是0F C7,另外,ModR/M中的reg/opcode用于补充opcode,所以这里的/6表示的是ModR/Mreg/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中缩写码的含义:

  • Codes for Addressing Method
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).
  • Codes for Operand Type
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.
qq 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.

Comments