专注虚拟机与编译器研究

第18章-x86指令集之常用指令

x86的指令集可分为以下4种:

  1. 通用指令
  2. x87 FPU指令,浮点数运算的指令
  3. SIMD指令,就是SSE指令
  4. 系统指令,写OS内核时使用的特殊指令

下面介绍一些通用的指令。指令由标识命令种类的助记符(mnemonic)和作为参数的操作数(operand)组成。例如move指令:

指令 操作数 描述
movq I/R/M,R/M 从一个内存位置复制1个双字(64位,8字节)大小的数据到另外一个内存位置
movl I/R/M,R/M 从一个内存位置复制1个字(32位,4字节)大小的数据到另外一个内存位置
movw I/R/M, R/M 从一个内存位置复制2个字节(16位)大小的数据到另外一个内存位置
movb I/R/M, R/M 从一个内存位置复制1个字节(8位)大小的数据到另外一个内存位置

movl为助记符。助记符有后缀,如movl中的后缀l表示作为操作数的对象的数据大小。l为long的缩写,表示32位的大小,除此之外,还有b、w,q分别表示8位、16位和64位的大小。

指令的操作数如果不止1个,就将每个操作数以逗号分隔。每个操作数都会指明是否可以是立即模式值(I)、寄存器(R)或内存地址(M)。

另外还要提示一下,在x86的汇编语言中,采用内存位置的操作数最多只能出现一个,例如不可能出现mov M,M指令。

通用寄存器中每个操作都可以有一个字符的后缀,表明操作数的大小,如下表所示。

C声明 通用寄存器后缀 大小(字节)
char b 1
short w 2
(unsigned) int / long / char* l 4
float s 4
double l 5
long double t 10/12

注意:通用寄存器使用后缀“l”同时表示4字节整数和8字节双精度浮点数,这不会产生歧义,因为浮点数使用的是完全不同的指令和寄存器。

我们后面只介绍call、push等指令时,如果在研究HotSpot VM虚拟机的汇编遇到了callq,pushq等指令时,千万别不认识,后缀就是表示了操作数的大小。

下表为操作数的格式和寻址模式。

格式

操作数值

名称

样例(通用寄存器 = C语言)

$Imm

Imm

立即数寻址

$1 = 1

Ea

R[Ea]

寄存器寻址

%eax = eax

Imm

M[Imm]

绝对寻址

0x104 = *0x104

(Ea)

M[R[Ea]]

间接寻址

(%eax)= *eax

Imm(Ea)

M[Imm+R[Ea]]

(基址+偏移量)寻址

4(%eax) = *(4+eax)

(Ea,Eb)

M[R[Ea]+R[Eb]]

变址

(%eax,%ebx) = *(eax+ebx)

Imm(Ea,Eb)

M[Imm+R[Ea]+R[Eb]]

寻址

9(%eax,%ebx)= *(9+eax+ebx)

(,Ea,s)

M[R[Ea]*s]

伸缩化变址寻址

(,%eax,4)= *(eax*4)

Imm(,Ea,s)

M[Imm+R[Ea]*s]

伸缩化变址寻址

0xfc(,%eax,4)= *(0xfc+eax*4)

(Ea,Eb,s)

M(R[Ea]+R[Eb]*s)

伸缩化变址寻址

(%eax,%ebx,4) = *(eax+ebx*4)

Imm(Ea,Eb,s)

M(Imm+R[Ea]+R[Eb]*s)

伸缩化变址寻址

8(%eax,%ebx,4) = *(8+eax+ebx*4)

注:M[xx]表示在存储器中xx地址的值,R[xx]表示寄存器xx的值,这种表示方法将寄存器、内存都看出一个大数组的形式。

汇编根据编译器的不同,有2种书写格式:

(1)Intel : Windows派系
(2)AT&T: Unix派系

下面简单介绍一下两者的不同。

下面就来认识一下常用的指令。

下面我们以给出的是AT&T汇编的写法,这两种写法有如下不同。 

1、数据传送指令

将数据从一个地方传送到另外一个地方。

1.1 mov指令

我们在介绍mov指令时介绍的全一些,因为mov指令是出现频率最高的指令,助记符中的后缀也比较多。

mov指令的形式有3种,如下:

mov   #普通的move指令
movs  #符号扩展的move指令,将源操作数进行符号扩展并传送到一个64位寄存器或存储单元中。movs就表示符号扩展 
movz  #零扩展的move指令,将源操作数进行零扩展后传送到一个64位寄存器或存储单元中。movz就表示零扩展

mov指令后有一个字母可表示操作数大小,形式如下:

movb #完成1个字节的复制
movw #完成2个字节的复制
movl #完成4个字节的复制
movq #完成8个字节的复制

还有一个指令,如下:

movabsq  I,R

与movq有所不同,它是将一个64位的值直接存到一个64位寄存器中。  

movs指令的形式如下:

movsbw #作符号扩展的1字节复制到2字节
movsbl #作符号扩展的1字节复制到4字节
movsbq #作符号扩展的1字节复制到8字节
movswl #作符号扩展的2字节复制到4字节
movswq #作符号扩展的2字节复制到8字节
movslq #作符号扩展的4字节复制到8字节

movz指令的形式如下:  

movzbw #作0扩展的1字节复制到2字节
movzbl #作0扩展的1字节复制到4字节
movzbq #作0扩展的1字节复制到8字节
movzwl #作0扩展的2字节复制到4字节
movzwq #作0扩展的2字节复制到8字节
movzlq #作0扩展的4字节复制到8字节

举个例子如下:

movl   %ecx,%eax
movl   (%ecx),%eax

第一条指令将寄存器ecx中的值复制到eax寄存器;第二条指令将ecx寄存器中的数据作为地址访问内存,并将内存上的数据加载到eax寄存器中。 

1.2 cmov指令

cmov指令的格式如下:

cmovxx

其中xx代表一个或者多个字母,这些字母表示将触发传送操作的条件。条件取决于 EFLAGS 寄存器的当前值。

eflags寄存器中各个们如下图所示。

其中与cmove指令相关的eflags寄存器中的位有CF(数学表达式产生了进位或者借位) 、OF(整数值无穷大或者过小)、PF(寄存器包含数学操作造成的错误数据)、SF(结果为正不是负)和ZF(结果为零)。

下表为无符号条件传送指令。

 指令对 描述  eflags状态 
cmova/cmovnbe 大于/不小于或等于  (CF或ZF)=0 
cmovae/cmovnb  大于或者等于/不小于 CF=0 
cmovnc  无进位  CF=0 
cmovb/cmovnae  大于/不小于或等于  CF=1
cmovc  进位 CF=1
cmovbe/cmovna  小于或者等于/不大于 (CF或ZF)=1
cmove/cmovz  等于/零 ZF=1
cmovne/cmovnz  不等于/不为零 ZF=0 
cmovp/cmovpe 奇偶校验/偶校验 PF=1 
cmovnp/cmovpo 非奇偶校验/奇校验  PF=0 

 无符号条件传送指令依靠进位、零和奇偶校验标志来确定两个操作数之间的区别。

下表为有符号条件传送指令。

指令对

描述

eflags状态

cmovge/cmovnl

大于或者等于/不小于

(SF异或OF)=0

cmovl/cmovnge

大于/不大于或者等于

(SF异或OF)=1

cmovle/cmovng

小于或者等于/不大于

((SF异或OF)或ZF)=1

cmovo

溢出

OF=1

cmovno

未溢出

OF=0

cmovs

带符号(负)

SF=1

cmovns

无符号(非负)

SF=0

举个例子如下:

// 将vlaue数值加载到ecx寄存器中
movl value,%ecx 
// 使用cmp指令比较ecx和ebx这两个寄存器中的值,具体就是用ecx减去ebx然后设置eflags
cmp %ebx,%ecx
// 如果ecx的值大于ebx,使用cmova指令设置ebx的值为ecx中的值
cmova %ecx,%ebx 

注意AT&T汇编的第1个操作数在前,第2个操作数在后。    

1.3 push和pop指令 

push指令的形式如下表所示。 

指令

操作数

描述

push

I/R/M

PUSH 指令首先减少 ESP 的值,再将源操作数复制到堆栈。操作数是 16 位的,

则 ESP 减 2,操作数是 32 位的,则 ESP 减 4

pusha

 

指令按序(AX、CX、DX、BX、SP、BP、SI 和 DI)将 16 位通用寄存器压入堆栈。

pushad

 

指令按照 EAX、ECX、EDX、EBX、ESP(执行 PUSHAD 之前的值)、

EBP、ESI 和 EDI 的顺序,将所有 32 位通用寄存器压入堆栈。

pop指令的形式如下表所示。 

指令

操作数

描述

pop

R/M

指令首先把 ESP 指向的堆栈元素内容复制到一个 16 位或 32 位目的操作数中,再增加 ESP 的值。

如果操作数是 16 位的,ESP 加 2,如果操作数是 32 位的,ESP 加 4

popa

 

指令按照相反顺序将同样的寄存器弹出堆栈

popad

 

指令按照相反顺序将同样的寄存器弹出堆栈

 

 1.4 xchg与xchgl

这个指令用于交换操作数的值,交换指令XCHG是两个寄存器,寄存器和内存变量之间内容的交换指令,两个操作数的数据类型要相同,可以是一个字节,也可以是一个字,也可以是双字。格式如下:

xchg    R/M,R/M
xchgl   I/R,I/R、  

两个操作数不能同时为内存变量。xchgl指令是一条古老的x86指令,作用是交换两个寄存器或者内存地址里的4字节值,两个值不能都是内存地址,他不会设置条件码。

1.5 lea

lea计算源操作数的实际地址,并把结果保存到目标操作数,而目标操作数必须为通用寄存器。格式如下:

lea M,R

lea(Load Effective Address)指令将地址加载到寄存器。

举例如下:

movl  4(%ebx),%eax
leal  4(%ebx),%eax  

第一条指令表示将ebx寄存器中存储的值加4后得到的结果作为内存地址进行访问,并将内存地址中存储的数据加载到eax寄存器中。

第二条指令表示将ebx寄存器中存储的值加4后得到的结果作为内存地址存放到eax寄存器中。

再举个例子,如下:

leaq a(b, c, d), %rax 

计算地址a + b + c * d,然后把最终地址载到寄存器rax中。可以看到只是简单的计算,不引用源操作数里的寄存器。这样的完全可以把它当作乘法指令使用。  

2、算术运算指令

下面介绍对有符号整数和无符号整数进行操作的基本运算指令。

2.1 add与adc指令

指令的格式如下:

add  I/R/M,R/M
adc  I/R/M,R/M

指令将两个操作数相加,结果保存在第2个操作数中。

对于第1条指令来说,由于寄存器和存储器都有位宽限制,因此在进行加法运算时就有可能发生溢出。运算如果溢出的话,标志寄存器eflags中的进位标志(Carry Flag,CF)就会被置为1。

对于第2条指令来说,利用adc指令再加上进位标志eflags.CF,就能在32位的机器上进行64位数据的加法运算。

常规的算术逻辑运算指令只要将原来IA-32中的指令扩展到64位即可。如addq就是四字相加。  

2.2 sub与sbb指令

指令的格式如下:

sub I/R/M,R/M
sbb I/R/M,R/M

指令将用第2个操作数减去第1个操作数,结果保存在第2个操作数中。

2.3 imul与mul指令

指令的格式如下:

imul I/R/M,R
mul  I/R/M,R

将第1个操作数和第2个操作数相乘,并将结果写入第2个操作数中,如果第2个操作数空缺,默认为eax寄存器,最终完整的结果将存储到edx:eax中。

第1条指令执行有符号乘法,第2条指令执行无符号乘法。

2.4 idiv与div指令

指令的格式如下:

div   R/M
idiv  R/M

第1条指令执行无符号除法,第2条指令执行有符号除法。被除数由edx寄存器和eax寄存器拼接而成,除数由指令的第1个操作数指定,计算得到的商存入eax寄存器,余数存入edx寄存器。如下图所示。

    edx:eax
------------ = eax(商)... edx(余数)
    寄存器

运算时被除数、商和除数的数据的位宽是不一样的,如下表表示了idiv指令和div指令使用的寄存器的情况。

数据的位宽 被除数 除数

余数

8位 ax 指令第1个操作数 al ah
16位 dx:ax 指令第1个操作数 ax dx
32位 edx:eax 指令第1个操作数 eax edx

idiv指令和div指令通常是对位宽2倍于除数的被除数进行除法运算的。例如对于x86-32机器来说,通用寄存器的倍数为32位,1个寄存器无法容纳64位的数据,所以 edx存放被除数的高32位,而eax寄存器存放被除数的低32位。

所以在进行除法运算时,必须将设置在eax寄存器中的32位数据扩展到包含edx寄存器在内的64位,即有符号进行符号扩展,无符号数进行零扩展。

对edx进行符号扩展时可以使用cltd(AT&T风格写法)或cdq(Intel风格写法)。指令的格式如下:

cltd  // 将eax寄存器中的数据符号扩展到edx:eax

cltd将eax寄存器中的数据符号扩展到edx:eax。 

2.5 incl与decl指令

指令的格式如下:

inc  R/M
dec  R/M 

将指令第1个操作数指定的寄存器或内存位置存储的数据加1或减1。

2.6 negl指令

指令的格式如下:

neg R/M

neg指令将第1个操作数的符号进行反转。  

3、位运算指令

3.1 andl、orl与xorl指令 

指令的格式如下:

and  I/R/M,R/M
or   I/R/M,R/M
xor  I/R/M,R/M

and指令将第2个操作数与第1个操作数进行按位与运算,并将结果写入第2个操作数;

or指令将第2个操作数与第1个操作数进行按位或运算,并将结果写入第2个操作数; 

xor指令将第2个操作数与第1个操作数进行按位异或运算,并将结果写入第2个操作数; 

3.2 not指令 

指令的格式如下:

not R/M

将操作数按位取反,并将结果写入操作数中。

3.3 sal、sar、shr指令

指令的格式如下:

sal  I/%cl,R/M  #算术左移
sar  I/%cl,R/M  #算术右移
shl  I/%cl,R/M  #逻辑左移
shr  I/%cl,R/M  #逻辑右移

sal指令将第2个操作数按照第1个操作数指定的位数进行左移操作,并将结果写入第2个操作数中。移位之后空出的低位补0。指令的第1个操作数只能是8位的立即数或cl寄存器,并且都是只有低5位的数据才有意义,高于或等于6位数将导致寄存器中的所有数据被移走而变得没有意义。

sar指令将第2个操作数按照第1个操作数指定的位数进行右移操作,并将结果写入第2个操作数中。移位之后的空出进行符号扩展。和sal指令一样,sar指令的第1个操作数也必须为8位的立即数或cl寄存器,并且都是只有低5位的数据才有意义。

shl指令和sall指令的动作完全相同,没有必要区分。

shr令将第2个操作数按照第1个操作数指定的位数进行右移操作,并将结果写入第2个操作数中。移位之后的空出进行零扩展。和sal指令一样,shr指令的第1个操作数也必须为8位的立即数或cl寄存器,并且都是只有低5位的数据才有意义。

4、流程控制指令

4.1 jmp指令

指令的格式如下:

jmp I/R

jmp指令将程序无条件跳转到操作数指定的目的地址。jmp指令可以视作设置指令指针(eip寄存器)的指令。目的地址也可以是星号后跟寄存器的栈,这种方式为间接函数调用。例如:

jmp *%eax

将程序跳转至eax所含地址。

4.2 条件跳转指令

条件跳转指令的格式如下:

Jcc  目的地址

其中cc指跳转条件,如果为真,则程序跳转到目的地址;否则执行下一条指令。相关的条件跳转指令如下表所示。

指令

跳转条件

描述

指令

跳转条件

描述

jz

ZF=1

为0时跳转

jbe

CF=1或ZF=1

大于或等于时跳转

jnz

ZF=0

不为0时跳转

jnbe

CF=0且ZF=0

小于或等于时跳转

je

ZF=1

相等时跳转

jg

ZF=0且SF=OF

大于时跳转

jne

ZF=0

不相等时跳转

jng

ZF=1或SF!=OF

不大于时跳转

ja

CF=0且ZF=0

大于时跳转

jge

SF=OF

大于或等于时跳转

jna

CF=1或ZF=1

不大于时跳转

jnge

SF!=OF

小于或等于时跳转

jae

CF=0

大于或等于时跳转

jl

SF!=OF

小于时跳转

jnae

CF=1

小于或等于时跳转

jnl

SF=OF

不小于时跳转

jb

CF=1

大于时跳转

jle

ZF=1或SF!=OF

小于或等于时跳转

jnb

CF=0

不大于时跳转

jnle

ZF=0且SF=OF

大于或等于时跳转

4.3 cmp指令

cmp指令的格式如下:

cmp I/R/M,R/M

cmp指令通过比较第2个操作数减去第1个操作数的差,根据结果设置标志寄存器eflags中的标志位。cmp指令和sub指令类似,不过cmp指令不会改变操作数的值。

操作数和所设置的标志位之间的关系如表所示。

操作数的关系 CF ZF OF
第1个操作数小于第2个操作数 0 0 SF
第1个操作数等于第2个操作数 0 1 0
第1个操作数大于第2个操作数 1 0 not SF


4.4 test指令

指令的格式如下:

test I/R/M,R/M

指令通过比较第1个操作数与第2个操作数的逻辑与,根据结果设置标志寄存器eflags中的标志位。test指令本质上和and指令相同,只是test指令不会改变操作数的值。

test指令执行后CF与OF通常会被清零,并根据运算结果设置ZF和SF。运算结果为零时ZF被置为1,SF和最高位的值相同。

举个例子如下:

test指令同时能够检查几个位。假设想要知道 AL 寄存器的位 0 和位 3 是否置 1,可以使用如下指令:

test al,00001001b    #掩码为0000 1001,测试第0和位3位是否为1

从下面的数据集例子中,可以推断只有当所有测试位都清 0 时,零标志位才置 1:

0  0  1  0  0  1  0  1    <- 输入值
0  0  0  0  1  0  0  1    <- 测试值
0  0  0  0  0  0  0  1    <- 结果:ZF=0

0  0  1  0  0  1  0  0    <- 输入值
0  0  0  0  1  0  0  1    <- 测试值
0  0  0  0  0  0  0  0    <- 结果:ZF=1

test指令总是清除溢出和进位标志位,其修改符号标志位、零标志位和奇偶标志位的方法与 AND 指令相同。

4.5 sete指令

根据eflags中的状态标志(CF,SF,OF,ZF和PF)将目标操作数设置为0或1。这里的目标操作数指向一个字节寄存器(也就是8位寄存器,如AL,BL,CL)或内存中的一个字节。状态码后缀(cc)指明了将要测试的条件。

获取标志位的指令的格式如下:

setcc R/M

指令根据标志寄存器eflags的值,将操作数设置为0或1。

setcc中的cc和Jcc中的cc类似,可参考表。

4.6 call指令

指令的格式如下:

call I/R/M

call指令会调用由操作数指定的函数。call指令会将指令的下一条指令的地址压栈,再跳转到操作数指定的地址,这样函数就能通过跳转到栈上的地址从子函数返回了。相当于

push %eip
jmp addr

先压入指令的下一个地址,然后跳转到目标地址addr。    

4.7 ret指令

指令的格式如下:

ret

ret指令用于从子函数中返回。X86架构的Linux中是将函数的返回值设置到eax寄存器并返回的。相当于如下指令:

popl %eip

将call指令压栈的“call指令下一条指令的地址”弹出栈,并设置到指令指针中。这样程序就能正确地返回子函数的地方。

从物理上来说,CALL 指令将其返回地址压入堆栈,再把被调用过程的地址复制到指令指针寄存器。当过程准备返回时,它的 RET 指令从堆栈把返回地址弹回到指令指针寄存器。

4.8 enter指令

enter指令通过初始化ebp和esp寄存器来为函数建立函数参数和局部变量所需要的栈帧。相当于

push   %rbp
mov    %rsp,%rbp

4.9 leave指令

leave通过恢复ebp与esp寄存器来移除使用enter指令建立的栈帧。相当于

mov %rbp, %rsp
pop %rbp

将栈指针指向帧指针,然后pop备份的原帧指针到%ebp  

5.0 int指令

指令的格式如下:

int I

引起给定数字的中断。这通常用于系统调用以及其他内核界面。 

5、标志操作 

eflags寄存器的各个标志位如下图所示。

操作eflags寄存器标志的一些指令如下表所示。 

指令 操作数 描述
pushfd R PUSHFD 指令把 32 位 EFLAGS 寄存器内容压入堆栈
popfd R  POPFD 指令则把栈顶单元内容弹出到 EFLAGS 寄存器
 cld   将eflags.df设置为0 

推荐阅读:

第1篇-关于JVM运行时,开篇说的简单些

第2篇-JVM虚拟机这样来调用Java主类的main()方法

第3篇-CallStub新栈帧的创建

第4篇-JVM终于开始调用Java主类的main()方法啦

第5篇-调用Java方法后弹出栈帧及处理返回结果

第6篇-Java方法新栈帧的创建

第7篇-为Java方法创建栈帧

第8篇-dispatch_next()函数分派字节码

第9篇-字节码指令的定义

第10篇-初始化模板表

第11篇-认识Stub与StubQueue

第12篇-认识CodeletMark

第13篇-通过InterpreterCodelet存储机器指令片段

第14篇-生成重要的例程

第15章-解释器及解释器生成器

第16章-虚拟机中的汇编器

第17章-x86-64寄存器

如果有问题可直接评论留言或加作者微信mazhimazh

关注公众号,有HotSpot VM源码剖析系列文章!

 

posted on 2021-09-08 10:05  鸠摩(马智)  阅读(237)  评论(0编辑  收藏  举报

导航