x86的指令集可分为以下4种:
- 通用指令
- x87 FPU指令,浮点数运算的指令
- SIMD指令,就是SSE指令
- 系统指令,写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源码剖析系列文章!