第17章-x86-64寄存器
不同的CPU都能够解释的机器语言的体系称为指令集架构(ISA,Instruction Set Architecture),也可以称为指令集(instruction set)。Intel将x86系列CPU之中的32位CPU指令集架构称为IA-32,IA是“Intel Architecture”的简称,也可以称为i386、x86-32。AMD等于Intell提出了x86系列的64位扩展,所以由AMD设计的x86系列的64位指令集架构称为AMD64。后来Intel在自己的CPU中加入和AMD64几乎相同的指令集,称为Intel 64的指令集。AMD64和Intel 64可以统称为x86-64。
x86-64的所有寄存器都是与机器字长(数据总线位宽)相同,即64位的,x86-64将x86的8个32位通用寄存器扩展为64位(eax、ebx、ecx、edx、eci、edi、ebp、esp),并且增加了8个新的64位寄存器(r8-r15),在命名方式上,也从”exx”变为”rxx”,但仍保留”exx”进行32位操作,下表描述了各寄存器的命名和作用。
描述 | 32位 | 64位 |
通用寄存器组 | eax | rax |
ecx | rcx | |
edx | rdx | |
ebx | rbx | |
esp | rsp | |
ebp | rbp | |
esi | rsi | |
edi | rdi | |
- | r8~r15 | |
浮点寄存器组 | st0~st7 | st0~st7 |
XMM寄存器组 | XMM0~XMM7 | XMM0~XMM15 |
其中的%esp与%ebp有特殊用途,用来保存指向程序栈中特定位置的指针。
另外还有eflags寄存器,通过位来表示特定的含义,如下图所示。
在HotSpot VM中,表示寄存器的类都继承自AbstractRegisterImpl类,这个类的定义如下:
源代码位置:hotspot/src/share/vm/asm/register.hpp class AbstractRegisterImpl; typedef AbstractRegisterImpl* AbstractRegister; class AbstractRegisterImpl { protected: int value() const { return (int)(intx)this; } };
AbstractRegisterImpl类的继承体系如下图所示。
另外还有个ConcreteRegisterImpl类也继承了AbstractRegisterImpl,这个灰与C2编译器的实现有关,这里不做过多讲解。
1、RegisterImpl类
RegisterImpl类用来表示通用寄存器,类的定义如下:
源代码位置:cpu/x86/vm/register_x86.hpp // 使用Register做为RegisterImpl的简称 class RegisterImpl; typedef RegisterImpl* Register; class RegisterImpl: public AbstractRegisterImpl { public: enum { number_of_registers = 16, number_of_byte_registers = 16 }; // ... };
对于64位来说,通用寄存器的位宽为64位,也可以将eax、ebx、ecx和edx的一部分当作8位寄存器来使用,所以可以存储字节的寄存器数量为4。
在HotSpot VM中定义寄存器,如下:
源代码位置:hotspot/src/cpu/x86/vm/register_x86.hpp CONSTANT_REGISTER_DECLARATION(Register, noreg, (-1)); // noreg_RegisterEnumValue = ((-1)) CONSTANT_REGISTER_DECLARATION(Register, rax, (0)); // rax_RegisterEnumValue = ((0)) CONSTANT_REGISTER_DECLARATION(Register, rcx, (1)); // rcx_RegisterEnumValue = ((1)) CONSTANT_REGISTER_DECLARATION(Register, rdx, (2)); // rdx_RegisterEnumValue = ((2)) CONSTANT_REGISTER_DECLARATION(Register, rbx, (3)); // rbx_RegisterEnumValue = ((3)) CONSTANT_REGISTER_DECLARATION(Register, rsp, (4)); // rsp_RegisterEnumValue = ((4)) CONSTANT_REGISTER_DECLARATION(Register, rbp, (5)); // rbp_RegisterEnumValue = ((5)) CONSTANT_REGISTER_DECLARATION(Register, rsi, (6)); // rsi_RegisterEnumValue = ((6)) CONSTANT_REGISTER_DECLARATION(Register, rdi, (7)); // rdi_RegisterEnumValue = ((7)) CONSTANT_REGISTER_DECLARATION(Register, r8, (8)); // r8_RegisterEnumValue = ((8)) CONSTANT_REGISTER_DECLARATION(Register, r9, (9)); // r9_RegisterEnumValue = ((9)) CONSTANT_REGISTER_DECLARATION(Register, r10, (10)); // r10_RegisterEnumValue = ((10)) CONSTANT_REGISTER_DECLARATION(Register, r11, (11)); // r11_RegisterEnumValue = ((11)) CONSTANT_REGISTER_DECLARATION(Register, r12, (12)); // r12_RegisterEnumValue = ((12)) CONSTANT_REGISTER_DECLARATION(Register, r13, (13)); // r13_RegisterEnumValue = ((13)) CONSTANT_REGISTER_DECLARATION(Register, r14, (14)); // r14_RegisterEnumValue = ((14)) CONSTANT_REGISTER_DECLARATION(Register, r15, (15)); // r15_RegisterEnumValue = ((15))
宏CONSTANT_REGISTER_DECLARATION定义如下:
源代码位置:hotspot/src/share/vm/asm/register.hpp #define CONSTANT_REGISTER_DECLARATION(type, name, value) \ extern const type name; \ enum { name##_##type##EnumValue = (value) }
经过宏扩展后如下:
extern const Register rax; enum { rax_RegisterEnumValue = ((0)) } extern const Register rcx; enum { rcx_RegisterEnumValue = ((1)) } extern const Register rdx; enum { rdx_RegisterEnumValue = ((2)) } extern const Register rbx; enum { rbx_RegisterEnumValue = ((3)) } extern const Register rsp; enum { rsp_RegisterEnumValue = ((4)) } extern const Register rbp; enum { rbp_RegisterEnumValue = ((5)) } extern const Register rsi; enum { rsi_RegisterEnumValue = ((6)) } extern const Register rsi; enum { rdi_RegisterEnumValue = ((7)) } extern const Register r8; enum { r8_RegisterEnumValue = ((8)) } extern const Register r9; enum { r9_RegisterEnumValue = ((9)) } extern const Register r10; enum { r10_RegisterEnumValue = ((10)) } extern const Register r11; enum { r11_RegisterEnumValue = ((11)) } extern const Register r12; enum { r12_RegisterEnumValue = ((12)) } extern const Register r13; enum { r13_RegisterEnumValue = ((13)) } extern const Register r14; enum { r14_RegisterEnumValue = ((14)) } extern const Register r15; enum { r15_RegisterEnumValue = ((15)) }
如上的枚举类给寄存器指定了一个常量值。
在cpu/x86/vm/register_definitions_x86.cpp文件中定义的寄存器如下:
const Register noreg = ((Register)noreg_RegisterEnumValue) const Register rax = ((Register)rax_RegisterEnumValue) const Register rcx = ((Register)rcx_RegisterEnumValue) const Register rdx = ((Register)rdx_RegisterEnumValue) const Register rbx = ((Register)rbx_RegisterEnumValue) const Register rsp = ((Register)rsp_RegisterEnumValue) const Register rbp = ((Register)rbp_RegisterEnumValue) const Register rsi = ((Register)rsi_RegisterEnumValue) const Register rdi = ((Register)rdi_RegisterEnumValue) const Register r8 = ((Register)r8_RegisterEnumValue) const Register r9 = ((Register)r9_RegisterEnumValue) const Register r10 = ((Register)r10_RegisterEnumValue) const Register r11 = ((Register)r11_RegisterEnumValue) const Register r12 = ((Register)r12_RegisterEnumValue) const Register r13 = ((Register)r13_RegisterEnumValue) const Register r14 = ((Register)r14_RegisterEnumValue) const Register r15 = ((Register)r15_RegisterEnumValue)
当我们需要使用通用寄存器时,通过rax、rcx等变量引用就可以了。
2、FloatRegisterImpl
在HotSpot VM中,使用FloatRegisterImpl来表示浮点寄存器,此类的定义如下:
源代码位置:hotspot/src/cpu/x86/vm/register_x86.hpp // 使用FloatRegister做为简称 class FloatRegisterImpl; typedef FloatRegisterImpl* FloatRegister; class FloatRegisterImpl: public AbstractRegisterImpl { public: enum { number_of_registers = 8 }; // ... }
浮点寄存器有8个,分别是st0~st7,这是8个80位寄存器。
这里需要注意的是,还有一种寄存器MMX,MMX并非一种新的寄存器,而是借用了80位浮点寄存器的低64位,也就是说,使用MMX指令集,会影响浮点运算!
3、MMXRegisterImpl
MMX 为一种 SIMD 技术,即可通过一条指令执行多个数据运算,共有8个64位寄存器(借用了80位浮点寄存器的低64位),分别为mm0 – mm7,他与其他普通64位寄存器的区别在于通过它的指令进行运算,可以同时计算2个32位数据,或者4个16位数据等等,可以应用为图像处理过程中图形 颜色的计算。
MMXRegisterImpl类的定义如下:
class MMXRegisterImpl; typedef MMXRegisterImpl* MMXRegister;
MMX寄存器的定义如下:
CONSTANT_REGISTER_DECLARATION(MMXRegister, mnoreg , (-1)); CONSTANT_REGISTER_DECLARATION(MMXRegister, mmx0 , ( 0)); CONSTANT_REGISTER_DECLARATION(MMXRegister, mmx1 , ( 1)); CONSTANT_REGISTER_DECLARATION(MMXRegister, mmx2 , ( 2)); CONSTANT_REGISTER_DECLARATION(MMXRegister, mmx3 , ( 3)); CONSTANT_REGISTER_DECLARATION(MMXRegister, mmx4 , ( 4)); CONSTANT_REGISTER_DECLARATION(MMXRegister, mmx5 , ( 5)); CONSTANT_REGISTER_DECLARATION(MMXRegister, mmx6 , ( 6)); CONSTANT_REGISTER_DECLARATION(MMXRegister, mmx7 , ( 7));
宏扩展后如下:
extern const MMXRegister mnoreg; enum { mnoreg_MMXRegisterEnumValue = ((-1)) } extern const MMXRegister mmx0; enum { mmx0_MMXRegisterEnumValue = (( 0)) } extern const MMXRegister mmx1; enum { mmx1_MMXRegisterEnumValue = (( 1)) } extern const MMXRegister mmx2; enum { mmx2_MMXRegisterEnumValue = (( 2)) } extern const MMXRegister mmx3; enum { mmx3_MMXRegisterEnumValue = (( 3)) } extern const MMXRegister mmx4; enum { mmx4_MMXRegisterEnumValue = (( 4)) } extern const MMXRegister mmx5; enum { mmx5_MMXRegisterEnumValue = (( 5)) } extern const MMXRegister mmx6; enum { mmx6_MMXRegisterEnumValue = (( 6)) } extern const MMXRegister mmx7; enum { mmx7_MMXRegisterEnumValue = (( 7)) }
MMX Pentium以及Pentium II之后的CPU中有从mm0到mm7共8个64位寄存器。但实际上MMX寄存器和浮点数寄存器是共用的,即无法同时使用浮点数寄存器和MMX寄存器。
cpu/x86/vm/register_definitions_x86.cpp文件中定义的寄存器变量如下:
const MMXRegister mnoreg = ((MMXRegister)mnoreg_MMXRegisterEnumValue) const MMXRegister mmx0 = ((MMXRegister)mmx0_MMXRegisterEnumValue) const MMXRegister mmx1 = ((MMXRegister)mmx1_MMXRegisterEnumValue) const MMXRegister mmx2 = ((MMXRegister)mmx2_MMXRegisterEnumValue) const MMXRegister mmx3 = ((MMXRegister)mmx3_MMXRegisterEnumValue) const MMXRegister mmx4 = ((MMXRegister)mmx4_MMXRegisterEnumValue) const MMXRegister mmx5 = ((MMXRegister)mmx5_MMXRegisterEnumValue) const MMXRegister mmx6 = ((MMXRegister)mmx6_MMXRegisterEnumValue) const MMXRegister mmx7 = ((MMXRegister)mmx7_MMXRegisterEnumValue)
当我们需要使用MMX寄存器时,通过mmx0、mmx1等变量引用就可以了。
4、XMMRegisterImpl类
XMM寄存器是SSE指令用的寄存器。Pentium iii以及之后的CPU中提供了xmm0到xmm7共8个128位宽的XMM寄存器。另外还有个mxcsr寄存器,这个寄存器用来表示SSE指令的运算状态的寄存器。在HotSpot VM中,通过XMMRegisterImpl类来表示寄存器。这个类的定义如下:
源代码位置:hotspot/src/share/x86/cpu/vm/register_x86.hpp // 使用XMMRegister寄存器做为简称 class XMMRegisterImpl; typedef XMMRegisterImpl* XMMRegister; class XMMRegisterImpl: public AbstractRegisterImpl { public: enum { number_of_registers = 16 }; ... }
XMM寄存器的定义如下:
CONSTANT_REGISTER_DECLARATION(XMMRegister, xnoreg , (-1)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm0 , ( 0)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm1 , ( 1)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm2 , ( 2)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm3 , ( 3)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm4 , ( 4)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm5 , ( 5)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm6 , ( 6)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm7 , ( 7)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm8, (8)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm9, (9)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm10, (10)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm11, (11)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm12, (12)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm13, (13)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm14, (14)); CONSTANT_REGISTER_DECLARATION(XMMRegister, xmm15, (15));
经过宏扩展后为:
extern const XMMRegister xnoreg; enum { xnoreg_XMMRegisterEnumValue = ((-1)) } extern const XMMRegister xmm0; enum { xmm0_XMMRegisterEnumValue = (( 0)) } extern const XMMRegister xmm1; enum { xmm1_XMMRegisterEnumValue = (( 1)) } extern const XMMRegister xmm2; enum { xmm2_XMMRegisterEnumValue = (( 2)) } extern const XMMRegister xmm3; enum { xmm3_XMMRegisterEnumValue = (( 3)) } extern const XMMRegister xmm4; enum { xmm4_XMMRegisterEnumValue = (( 4)) } extern const XMMRegister xmm5; enum { xmm5_XMMRegisterEnumValue = (( 5)) } extern const XMMRegister xmm6; enum { xmm6_XMMRegisterEnumValue = (( 6)) } extern const XMMRegister xmm7; enum { xmm7_XMMRegisterEnumValue = (( 7)) } extern const XMMRegister xmm8; enum { xmm8_XMMRegisterEnumValue = ((8)) } extern const XMMRegister xmm9; enum { xmm9_XMMRegisterEnumValue = ((9)) } extern const XMMRegister xmm10; enum { xmm10_XMMRegisterEnumValue = ((10)) } extern const XMMRegister xmm11; enum { xmm11_XMMRegisterEnumValue = ((11)) } extern const XMMRegister xmm12; enum { xmm12_XMMRegisterEnumValue = ((12)) } extern const XMMRegister xmm13; enum { xmm13_XMMRegisterEnumValue = ((13)) } extern const XMMRegister xmm14; enum { xmm14_XMMRegisterEnumValue = ((14)) } extern const XMMRegister xmm15; enum { xmm15_XMMRegisterEnumValue = ((15)) }
在cpu/x86/vm/register_definitions_x86.cpp文件中定义的寄存器变量如下:
const XMMRegister xnoreg = ((XMMRegister)xnoreg_XMMRegisterEnumValue) const XMMRegister xmm0 = ((XMMRegister)xmm0_XMMRegisterEnumValue) const XMMRegister xmm1 = ((XMMRegister)xmm1_XMMRegisterEnumValue) const XMMRegister xmm2 = ((XMMRegister)xmm2_XMMRegisterEnumValue) const XMMRegister xmm3 = ((XMMRegister)xmm3_XMMRegisterEnumValue) const XMMRegister xmm4 = ((XMMRegister)xmm4_XMMRegisterEnumValue) const XMMRegister xmm5 = ((XMMRegister)xmm5_XMMRegisterEnumValue) const XMMRegister xmm6 = ((XMMRegister)xmm6_XMMRegisterEnumValue) const XMMRegister xmm7 = ((XMMRegister)xmm7_XMMRegisterEnumValue) const XMMRegister xmm8 = ((XMMRegister)xmm8_XMMRegisterEnumValue) const XMMRegister xmm9 = ((XMMRegister)xmm9_XMMRegisterEnumValue) const XMMRegister xmm10 = ((XMMRegister)xmm10_XMMRegisterEnumValue) const XMMRegister xmm11 = ((XMMRegister)xmm11_XMMRegisterEnumValue) const XMMRegister xmm12 = ((XMMRegister)xmm12_XMMRegisterEnumValue) const XMMRegister xmm13 = ((XMMRegister)xmm13_XMMRegisterEnumValue) const XMMRegister xmm14 = ((XMMRegister)xmm14_XMMRegisterEnumValue) const XMMRegister xmm15 = ((XMMRegister)xmm15_XMMRegisterEnumValue)
当我们需要使用XMM寄存器时,直接通过xmm0、xmm1等变量引用就可以了。
第18章-x86指令集之常用指令
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)= (eax4) |
Imm(,Ea,s) | M[Imm+R[Ea]*s] | 伸缩化变址寻址 | 0xfc(,%eax,4)= (0xfc+eax4) |
(Ea,Eb,s) | M(R[Ea]+R[Eb]*s) | 伸缩化变址寻址 | (%eax,%ebx,4) = (eax+ebx4) |
Imm(Ea,Eb,s) | M(Imm+R[Ea]+R[Eb]*s) | 伸缩化变址寻址 | 8(%eax,%ebx,4) = (8+eax+ebx4) |
注: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 |
第19篇-加载与存储指令(1)
TemplateInterpreterGenerator::generate_all()函数会生成许多例程(也就是机器指令片段,英文叫Stub),包括调用set_entry_points_for_all_bytes()函数生成各个字节码对应的例程。
最终会调用到TemplateInterpreterGenerator::generate_and_dispatch()函数,调用堆栈如下:
TemplateTable::geneate() templateTable_x86_64.cpp TemplateInterpreterGenerator::generate_and_dispatch() templateInterpreter.cpp TemplateInterpreterGenerator::set_vtos_entry_points() templateInterpreter_x86_64.cpp TemplateInterpreterGenerator::set_short_entry_points() templateInterpreter.cpp TemplateInterpreterGenerator::set_entry_points() templateInterpreter.cpp TemplateInterpreterGenerator::set_entry_points_for_all_bytes() templateInterpreter.cpp TemplateInterpreterGenerator::generate_all() templateInterpreter.cpp InterpreterGenerator::InterpreterGenerator() templateInterpreter_x86_64.cpp TemplateInterpreter::initialize() templateInterpreter.cpp interpreter_init() interpreter.cpp init_globals() init.cpp
调用堆栈上的许多函数在之前介绍过,每个字节码都会指定一个generator函数,通过Template的_gen属性保存。在TemplateTable::generate()函数中调用。_gen会生成每个字节码对应的机器指令片段,所以非常重要。
首先看一个非常简单的nop字节码指令。这个指令的模板属性如下:
// Java spec bytecodes ubcp|disp|clvm|iswd in out generator argument def(Bytecodes::_nop , ____|____|____|____, vtos, vtos, nop , _ );
nop字节码指令的生成函数generator不会生成任何机器指令,所以nop字节码指令对应的汇编代码中只有栈顶缓存的逻辑。调用set_vtos_entry_points()函数生成的汇编代码如下:
// aep 0x00007fffe1027c00: push %rax 0x00007fffe1027c01: jmpq 0x00007fffe1027c30 // fep 0x00007fffe1027c06: sub $0x8,%rsp 0x00007fffe1027c0a: vmovss %xmm0,(%rsp) 0x00007fffe1027c0f: jmpq 0x00007fffe1027c30 // dep 0x00007fffe1027c14: sub $0x10,%rsp 0x00007fffe1027c18: vmovsd %xmm0,(%rsp) 0x00007fffe1027c1d: jmpq 0x00007fffe1027c30 // lep 0x00007fffe1027c22: sub $0x10,%rsp 0x00007fffe1027c26: mov %rax,(%rsp) 0x00007fffe1027c2a: jmpq 0x00007fffe1027c30 // bep cep sep iep 0x00007fffe1027c2f: push %rax // vep // 接下来为取指逻辑,开始的地址为0x00007fffe1027c30
可以看到,由于tos_in为vtos,所以如果是aep、bep、cep、sep与iep时,直接使用push指令将%rax中存储的栈顶缓存值压入表达式栈中。对于fep、dep与lep来说,在栈上开辟对应内存的大小,然后将寄存器中的值存储到表达式的栈顶上,与push指令的效果相同。
在set_vtos_entry_points()函数中会调用generate_and_dispatch()函数生成nop指令的机器指令片段及取下一条字节码指令的机器指令片段。nop不会生成任何机器指令,而取指的片段如下:
// movzbl 将做了零扩展的字节传送到双字,地址为0x00007fffe1027c30 0x00007fffe1027c30: movzbl 0x1(%r13),%ebx 0x00007fffe1027c35: inc %r13 0x00007fffe1027c38: movabs $0x7ffff73ba4a0,%r10 // movabs的源操作数只能是立即数或标号(本质还是立即数),目的操作数是寄存器 0x00007fffe1027c42: jmpq *(%r10,%rbx,8)
r13指向当前要取的字节码指令的地址。那么%r13+1就是跳过了当前的nop指令而指向了下一个字节码指令的地址,然后执行movzbl指令将所指向的Opcode加载到%ebx中。
通过jmpq的跳转地址为%r10+%rbx*8,关于这个跳转地址在前面详细介绍过,这里不再介绍。
我们讲解了nop指令,把栈顶缓存的逻辑和取指逻辑又回顾了一遍,对于每个字节码指令来说都会有有栈顶缓存和取指逻辑,后面在介绍字节码指令时就不会再介绍这2个逻辑。
加载与存储相关操作的字节码指令如下表所示。
字节码 | 助词符 | 指令含义 |
0x00 | nop | 什么都不做 |
0x01 | aconst_null | 将null推送至栈顶 |
0x02 | iconst_m1 | 将int型-1推送至栈顶 |
0x03 | iconst_0 | 将int型0推送至栈顶 |
0x04 | iconst_1 | 将int型1推送至栈顶 |
0x05 | iconst_2 | 将int型2推送至栈顶 |
0x06 | iconst_3 | 将int型3推送至栈顶 |
0x07 | iconst_4 | 将int型4推送至栈顶 |
0x08 | iconst_5 | 将int型5推送至栈顶 |
0x09 | lconst_0 | 将long型0推送至栈顶 |
0x0a | lconst_1 | 将long型1推送至栈顶 |
0x0b | fconst_0 | 将float型0推送至栈顶 |
0x0c | fconst_1 | 将float型1推送至栈顶 |
0x0d | fconst_2 | 将float型2推送至栈顶 |
0x0e | dconst_0 | 将double型0推送至栈顶 |
0x0f | dconst_1 | 将double型1推送至栈顶 |
0x10 | bipush | 将单字节的常量值(-128~127)推送至栈顶 |
0x11 | sipush | 将一个短整型常量值(-32768~32767)推送至栈顶 |
0x12 | ldc | 将int、float或String型常量值从常量池中推送至栈顶 |
0x13 | ldc_w | 将int,、float或String型常量值从常量池中推送至栈顶(宽索引) |
0x14 | ldc2_w | 将long或double型常量值从常量池中推送至栈顶(宽索引) |
0x15 | iload | 将指定的int型本地变量推送至栈顶 |
0x16 | lload | 将指定的long型本地变量推送至栈顶 |
0x17 | fload | 将指定的float型本地变量推送至栈顶 |
0x18 | dload | 将指定的double型本地变量推送至栈顶 |
0x19 | aload | 将指定的引用类型本地变量推送至栈顶 |
0x1a | iload_0 | 将第一个int型本地变量推送至栈顶 |
0x1b | iload_1 | 将第二个int型本地变量推送至栈顶 |
0x1c | iload_2 | 将第三个int型本地变量推送至栈顶 |
0x1d | iload_3 | 将第四个int型本地变量推送至栈顶 |
0x1e | lload_0 | 将第一个long型本地变量推送至栈顶 |
0x1f | lload_1 | 将第二个long型本地变量推送至栈顶 |
0x20 | lload_2 | 将第三个long型本地变量推送至栈顶 |
0x21 | lload_3 | 将第四个long型本地变量推送至栈顶 |
0x22 | fload_0 | 将第一个float型本地变量推送至栈顶 |
0x23 | fload_1 | 将第二个float型本地变量推送至栈顶 |
0x24 | fload_2 | 将第三个float型本地变量推送至栈顶 |
0x25 | fload_3 | 将第四个float型本地变量推送至栈顶 |
0x26 | dload_0 | 将第一个double型本地变量推送至栈顶 |
0x27 | dload_1 | 将第二个double型本地变量推送至栈顶 |
0x28 | dload_2 | 将第三个double型本地变量推送至栈顶 |
0x29 | dload_3 | 将第四个double型本地变量推送至栈顶 |
0x2a | aload_0 | 将第一个引用类型本地变量推送至栈顶 |
0x2b | aload_1 | 将第二个引用类型本地变量推送至栈顶 |
0x2c | aload_2 | 将第三个引用类型本地变量推送至栈顶 |
0x2d | aload_3 | 将第四个引用类型本地变量推送至栈顶 |
0x2e | iaload | 将int型数组指定索引的值推送至栈顶 |
0x2f | laload | 将long型数组指定索引的值推送至栈顶 |
0x30 | faload | 将float型数组指定索引的值推送至栈顶 |
0x31 | daload | 将double型数组指定索引的值推送至栈顶 |
0x32 | aaload | 将引用型数组指定索引的值推送至栈顶 |
0x33 | baload | 将boolean或byte型数组指定索引的值推送至栈顶 |
0x34 | caload | 将char型数组指定索引的值推送至栈顶 |
0x35 | saload | 将short型数组指定索引的值推送至栈顶 |
0x36 | istore | 将栈顶int型数值存入指定本地变量 |
0x37 | lstore | 将栈顶long型数值存入指定本地变量 |
0x38 | fstore | 将栈顶float型数值存入指定本地变量 |
0x39 | dstore | 将栈顶double型数值存入指定本地变量 |
0x3a | astore | 将栈顶引用型数值存入指定本地变量 |
0x3b | istore_0 | 将栈顶int型数值存入第一个本地变量 |
0x3c | istore_1 | 将栈顶int型数值存入第二个本地变量 |
0x3d | istore_2 | 将栈顶int型数值存入第三个本地变量 |
0x3e | istore_3 | 将栈顶int型数值存入第四个本地变量 |
0x3f | lstore_0 | 将栈顶long型数值存入第一个本地变量 |
0x40 | lstore_1 | 将栈顶long型数值存入第二个本地变量 |
0x41 | lstore_2 | 将栈顶long型数值存入第三个本地变量 |
0x42 | lstore_3 | 将栈顶long型数值存入第四个本地变量 |
0x43 | fstore_0 | 将栈顶float型数值存入第一个本地变量 |
0x44 | fstore_1 | 将栈顶float型数值存入第二个本地变量 |
0x45 | fstore_2 | 将栈顶float型数值存入第三个本地变量 |
0x46 | fstore_3 | 将栈顶float型数值存入第四个本地变量 |
0x47 | dstore_0 | 将栈顶double型数值存入第一个本地变量 |
0x48 | dstore_1 | 将栈顶double型数值存入第二个本地变量 |
0x49 | dstore_2 | 将栈顶double型数值存入第三个本地变量 |
0x4a | dstore_3 | 将栈顶double型数值存入第四个本地变量 |
0x4b | astore_0 | 将栈顶引用型数值存入第一个本地变量 |
0x4c | astore_1 | 将栈顶引用型数值存入第二个本地变量 |
0x4d | astore_2 | 将栈顶引用型数值存入第三个本地变量 |
0x4e | astore_3 | 将栈顶引用型数值存入第四个本地变量 |
0x4f | iastore | 将栈顶int型数值存入指定数组的指定索引位置 |
0x50 | lastore | 将栈顶long型数值存入指定数组的指定索引位置 |
0x51 | fastore | 将栈顶float型数值存入指定数组的指定索引位置 |
0x52 | dastore | 将栈顶double型数值存入指定数组的指定索引位置 |
0x53 | aastore | 将栈顶引用型数值存入指定数组的指定索引位置 |
0x54 | bastore | 将栈顶boolean或byte型数值存入指定数组的指定索引位置 |
0x55 | castore | 将栈顶char型数值存入指定数组的指定索引位置 |
0x56 | sastore | 将栈顶short型数值存入指定数组的指定索引位置 |
0xc4 | wide | 扩充局部变量表的访问索引的指令 |
我们不会对每个字节码指令都查看对应的机器指令片段的逻辑(其实是反编译机器指令片段为汇编后,通过查看汇编理解执行逻辑),有些指令的逻辑是类似的,这里只选择几个典型的介绍。
1、压栈类型的指令
(1)aconst_null指令
aconst_null表示将null送到栈顶,模板定义如下:
def(Bytecodes::_aconst_null , ____|____|____|____, vtos, atos, aconst_null , _ );
指令的汇编代码如下:
// xor 指令在两个操作数的对应位之间进行逻辑异或操作,并将结果存放在目标操作数中 // 第1个操作数和第2个操作数相同时,执行异或操作就相当于执行清零操作 xor %eax,%eax
由于tos_out为atos,所以栈顶的结果是缓存在%eax寄存器中的,只对%eax寄存器执行xor操作即可。
(2)iconst_m1指令
iconst_m1表示将-1压入栈内,模板定义如下:
def(Bytecodes::_iconst_m1 , ____|____|____|____, vtos, itos, iconst , -1 );
生成的机器指令经过反汇编后,得到的汇编代码如下:
mov $0xffffffff,%eax
其它的与iconst_m1字节码指令类似的字节码指令,如iconst_0、iconst_1等,模板定义如下:
def(Bytecodes::_iconst_m1 , ____|____|____|____, vtos, itos, iconst , -1 ); def(Bytecodes::_iconst_0 , ____|____|____|____, vtos, itos, iconst , 0 ); def(Bytecodes::_iconst_1 , ____|____|____|____, vtos, itos, iconst , 1 ); def(Bytecodes::_iconst_2 , ____|____|____|____, vtos, itos, iconst , 2 ); def(Bytecodes::_iconst_3 , ____|____|____|____, vtos, itos, iconst , 3 ); def(Bytecodes::_iconst_4 , ____|____|____|____, vtos, itos, iconst , 4 ); def(Bytecodes::_iconst_5 , ____|____|____|____, vtos, itos, iconst , 5 );
可以看到,生成函数都是同一个TemplateTable::iconst()函数。
iconst_0的汇编代码如下:
xor %eax,%eax
iconst_@(@为1、2、3、4、5)的字节码指令对应的汇编代码如下:
// aep 0x00007fffe10150a0: push %rax 0x00007fffe10150a1: jmpq 0x00007fffe10150d0 // fep 0x00007fffe10150a6: sub $0x8,%rsp 0x00007fffe10150aa: vmovss %xmm0,(%rsp) 0x00007fffe10150af: jmpq 0x00007fffe10150d0 // dep 0x00007fffe10150b4: sub $0x10,%rsp 0x00007fffe10150b8: vmovsd %xmm0,(%rsp) 0x00007fffe10150bd: jmpq 0x00007fffe10150d0 // lep 0x00007fffe10150c2: sub $0x10,%rsp 0x00007fffe10150c6: mov %rax,(%rsp) 0x00007fffe10150ca: jmpq 0x00007fffe10150d0 // bep/cep/sep/iep 0x00007fffe10150cf: push %rax // vep 0x00007fffe10150d0 mov $0x@,%eax // @代表1、2、3、4、5
如果看过我之前写的文章,那么如上的汇编代码应该能看懂,我在这里就不再做过多介绍了。
(3)bipush
bipush 将单字节的常量值推送至栈顶。模板定义如下:
def(Bytecodes::_bipush , ubcp|____|____|____, vtos, itos, bipush , _ );
指令的汇编代码如下:
// %r13指向字节码指令的地址,偏移1位 // 后取出1个字节的内容存储到%eax中 movsbl 0x1(%r13),%eax
由于tos_out为itos,所以将单字节的常量值存储到%eax中,这个寄存器是专门用来进行栈顶缓存的。
(4)sipush
sipush将一个短整型常量值推送到栈顶,模板定义如下:
def(Bytecodes::_bipush , ubcp|____|____|____, vtos, itos, bipush , _ );
生成的汇编代码如下:
// movzwl传送做了符号扩展字到双字 movzwl 0x1(%r13),%eax // bswap 以字节为单位,把32/64位寄存器的值按照低和高的字节交换 bswap %eax // (算术右移)指令将目的操作数进行算术右移 sar $0x10,%eax
Java中的短整型占用2个字节,所以需要对32位寄存器%eax进行一些操作。由于字节码采用大端存储,所以在处理时统一变换为小端存储。
2、存储类型指令
istore指令会将int类型数值存入指定索引的本地变量表,模板定义如下:
def(Bytecodes::_istore , ubcp|____|clvm|____, itos, vtos, istore , _ );
生成函数为TemplateTable::istore(),生成的汇编代码如下:
movzbl 0x1(%r13),%ebx neg %rbx mov %eax,(%r14,%rbx,8)
由于栈顶缓存tos_in为itos,所以直接将%eax中的值存储到指定索引的本地变量表中。
模板中指定ubcp,因为生成的汇编代码中会使用%r13,也就是字节码指令指针。
其它的istore、dstore等字节码指令的汇编代码逻辑也类似,这里不过多介绍。
第20篇-加载与存储指令之ldc与_fast_aldc指令(2)
ldc指令将int、float、或者一个类、方法类型或方法句柄的符号引用、还可能是String型常量值从常量池中推送至栈顶。
这一篇介绍一个虚拟机规范中定义的一个字节码指令ldc,另外还有一个虚拟机内部使用的字节码指令_fast_aldc。ldc指令可以加载String、方法类型或方法句柄的符号引用,但是如果要加载String、方法类型或方法句柄的符号引用,则会在类连接过程中重写ldc字节码指令为虚拟机内部使用的字节码指令_fast_aldc。下面我们详细介绍ldc指令如何加载int、float类型和类类型的数据,以及_fast_aldc加载String、方法类型或方法句柄,还有为什么要进行字节码重写等问题。
1、ldc字节码指令
ldc指令将int、float或String型常量值从常量池中推送至栈顶。模板的定义如下:
def(Bytecodes::_ldc , ubcp|____|clvm|____, vtos, vtos, ldc , false );
ldc字节码指令的格式如下:
// index是一个无符号的byte类型数据,指明当前类的运行时常量池的索引 ldc index
调用生成函数TemplateTable::ldc(bool wide)。函数生成的汇编代码如下:
第1部分代码:
// movzbl指令负责拷贝一个字节,并用0填充其目 // 的操作数中的其余各位,这种扩展方式叫"零扩展" // ldc指定的格式为ldc index,index为一个字节 0x00007fffe1028530: movzbl 0x1(%r13),%ebx // 加载index到%ebx // %rcx指向缓存池首地址、%rax指向类型数组_tags首地址 0x00007fffe1028535: mov -0x18(%rbp),%rcx 0x00007fffe1028539: mov 0x10(%rcx),%rcx 0x00007fffe102853d: mov 0x8(%rcx),%rcx 0x00007fffe1028541: mov 0x10(%rcx),%rax // 从_tags数组获取操作数类型并存储到%edx中 0x00007fffe1028545: movzbl 0x4(%rax,%rbx,1),%edx // $0x64代表JVM_CONSTANT_UnresolvedClass,比较,如果类还没有链接, // 则直接跳转到call_ldc 0x00007fffe102854a: cmp $0x64,%edx 0x00007fffe102854d: je 0x00007fffe102855d // call_ldc // $0x67代表JVM_CONSTANT_UnresolvedClassInError,也就是如果类在 // 链接过程中出现错误,则跳转到call_ldc 0x00007fffe102854f: cmp $0x67,%edx 0x00007fffe1028552: je 0x00007fffe102855d // call_ldc // $0x7代表JVM_CONSTANT_Class,表示如果类已经进行了连接,则 // 跳转到notClass 0x00007fffe1028554: cmp $0x7,%edx 0x00007fffe1028557: jne 0x00007fffe10287c0 // notClass // 类在没有连接或连接过程中出错,则执行如下的汇编代码 // -- call_ldc --
下面看一下调用call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::ldc), c_rarg1)函数生成的汇编代码,CAST_FROM_FN_PTR是宏,宏扩展后为( (address)((address_word)(InterpreterRuntime::ldc)) )。
在调用call_VM()函数时,传递的参数如下:
- %rax现在存储类型数组首地址,不过传入是为了接收调用函数的结果值
- adr是InterpreterRuntime::ldc()函数首地址
- c_rarg1用rdi寄存器存储wide值,这里为0,表示为没有加wide前缀的ldc指令生成汇编代码
生成的汇编代码如下:
第2部分:
// 将wide的值移到%esi寄存器,为后续 // 调用InterpreterRuntime::ldc()函数准备第2个参数 0x00007fffe102855d: mov $0x0,%esi // 调用MacroAssembler::call_VM()函数,通过此函数来调用HotSpot VM中用 // C++编写的函数,通过这个C++编写的函数来调用InterpreterRuntime::ldc()函数 0x00007fffe1017542: callq 0x00007fffe101754c 0x00007fffe1017547: jmpq 0x00007fffe10175df // 跳转到E1 // 调用MacroAssembler::call_VM_helper()函数 // 将栈顶存储的返回地址设置到%rax中,也就是将存储地址0x00007fffe1017547 // 的栈的slot地址设置到%rax中 0x00007fffe101754c: lea 0x8(%rsp),%rax // 调用InterpreterMacroAssembler::call_VM_base()函数 // 存储bcp到栈中特定位置 0x00007fffe1017551: mov %r13,-0x38(%rbp) // 调用MacroAssembler::call_VM_base()函数 // 将r15中的值移动到rdi寄存器中,也就是为函数调用准备第一个参数 0x00007fffe1017555: mov %r15,%rdi // 只有解释器才必须要设置fp // 将last_java_fp保存到JavaThread类的last_java_fp属性中 0x00007fffe1017558: mov %rbp,0x200(%r15) // 将last_java_sp保存到JavaThread类的last_java_sp属性中 0x00007fffe101755f: mov %rax,0x1f0(%r15) // ... 省略调用MacroAssembler::call_VM_leaf_base()函数 // 重置JavaThread::last_java_sp与JavaThread::last_java_fp属性的值 0x00007fffe1017589: movabs $0x0,%r10 0x00007fffe1017593: mov %r10,0x1f0(%r15) 0x00007fffe101759a: movabs $0x0,%r10 0x00007fffe10175a4: mov %r10,0x200(%r15) // check for pending exceptions (java_thread is set upon return) 0x00007fffe10175ab: cmpq $0x0,0x8(%r15) // 如果没有异常则直接跳转到ok 0x00007fffe10175b3: je 0x00007fffe10175be // 如果有异常则跳转到StubRoutines::forward_exception_entry()获取的例程入口 0x00007fffe10175b9: jmpq 0x00007fffe1000420 // -- ok -- // 将JavaThread::vm_result属性中的值存储到%rax寄存器中并清空vm_result属性的值 0x00007fffe10175be: mov 0x250(%r15),%rax 0x00007fffe10175c5: movabs $0x0,%r10 0x00007fffe10175cf: mov %r10,0x250(%r15) // 结束调用MacroAssembler::call_VM_base()函数 // 恢复bcp与locals 0x00007fffe10175d6: mov -0x38(%rbp),%r13 0x00007fffe10175da: mov -0x30(%rbp),%r14 // 结束调用MacroAssembler::call_VM_helper()函数 0x00007fffe10175de: retq // 结束调用MacroAssembler::call_VM()函数
下面详细解释如下汇编的意思。
call指令相当于如下两条指令:
push %eip jmp addr
而ret指令相当于:
pop %eip
所以如上汇编代码:
0x00007fffe1017542: callq 0x00007fffe101754c 0x00007fffe1017547: jmpq 0x00007fffe10175df // 跳转 ... 0x00007fffe10175de: retq
调用callq指令将jmpq的地址压入了表达式栈,也就是压入了返回地址x00007fffe1017547,这样当后续调用retq时,会跳转到jmpq指令执行,而jmpq又跳转到了0x00007fffe10175df地址处的指令执行。
通过调用MacroAssembler::call_VM()函数来调用HotSpot VM中用的C++编写的函数,call_VM()函数还会调用如下函数:
MacroAssembler::call_VM_helper InterpreterMacroAssembler::call_VM_base() MacroAssembler::call_VM_base() MacroAssembler::call_VM_leaf_base()
在如上几个函数中,最重要的就是在MacroAssembler::call_VM_base()函数中保存rsp、rbp的值到JavaThread::last_java_sp与JavaThread::last_java_fp属性中,然后通过MacroAssembler::call_VM_leaf_base()函数生成的汇编代码来调用C++编写的InterpreterRuntime::ldc()函数,如果调用InterpreterRuntime::ldc()函数有可能破坏rsp和rbp的值(其它的%r13、%r14等的寄存器中的值也有可能破坏,所以在必要时保存到栈中,在调用完成后再恢复,这样这些寄存器其实就算的上是调用者保存的寄存器了),所以为了保证rsp、rbp,将这两个值存储到线程中,在线程中保存的这2个值对于栈展开非常非常重要,后面我们会详细介绍。
由于如上汇编代码会解释执行,在解释执行过程中会调用C++函数,所以C/C++栈和Java栈都混在一起,这为我们查找带来了一定的复杂度。
调用的MacroAssembler::call_VM_leaf_base()函数生成的汇编代码如下:
第3部分汇编代码:
// 调用MacroAssembler::call_VM_leaf_base()函数 0x00007fffe1017566: test $0xf,%esp // 检查对齐 // %esp对齐的操作,跳转到 L 0x00007fffe101756c: je 0x00007fffe1017584 // %esp没有对齐时的操作 0x00007fffe1017572: sub $0x8,%rsp 0x00007fffe1017576: callq 0x00007ffff66a22a2 // 调用函数,也就是调用InterpreterRuntime::ldc()函数 0x00007fffe101757b: add $0x8,%rsp 0x00007fffe101757f: jmpq 0x00007fffe1017589 // 跳转到E2 // -- L -- // %esp对齐的操作 0x00007fffe1017584: callq 0x00007ffff66a22a2 // 调用函数,也就是调用InterpreterRuntime::ldc()函数 // -- E2 -- // 结束调用
MacroAssembler::call_VM_leaf_base()函数
在如上这段汇编中会真正调用C++函数InterpreterRuntime::ldc(),由于这是一个C++函数,所以在调用时,如果要传递参数,则要遵守C++调用约定,也就是前6个参数都放到固定的寄存器中。这个函数需要2个参数,分别为thread和wide,已经分别放到了%rdi和%rax寄存器中了。InterpreterRuntime::ldc()函数的实现如下:
// ldc负责将数值常量或String常量值从常量池中推送到栈顶 IRT_ENTRY(void, InterpreterRuntime::ldc(JavaThread* thread, bool wide)) ConstantPool* pool = method(thread)->constants(); int index = wide ? get_index_u2(thread, Bytecodes::_ldc_w) : get_index_u1(thread, Bytecodes::_ldc); constantTag tag = pool->tag_at(index); Klass* klass = pool->klass_at(index, CHECK); oop java_class = klass->java_mirror(); // java.lang.Class通过oop来表示 thread->set_vm_result(java_class); IRT_END
函数将查找到的、当前正在解释执行的方法所属的类存储到JavaThread类的vm_result属性中。我们可以回看第2部分汇编代码,会将vm_result属性的值设置到%rax中。
接下来继续看TemplateTable::ldc(bool wide)函数生成的汇编代码,此时已经通过调用call_VM()函数生成了调用InterpreterRuntime::ldc()这个C++的汇编,调用完成后值已经放到了%rax中。
// -- E1 -- 0x00007fffe10287ba: push %rax // 将调用的结果存储到表达式中 0x00007fffe10287bb: jmpq 0x00007fffe102885e // 跳转到Done // -- notClass -- // $0x4表示JVM_CONSTANT_Float 0x00007fffe10287c0: cmp $0x4,%edx 0x00007fffe10287c3: jne 0x00007fffe10287d9 // 跳到notFloat // 当ldc字节码指令加载的数为float时执行如下汇编代码 0x00007fffe10287c5: vmovss 0x58(%rcx,%rbx,8),%xmm0 0x00007fffe10287cb: sub $0x8,%rsp 0x00007fffe10287cf: vmovss %xmm0,(%rsp) 0x00007fffe10287d4: jmpq 0x00007fffe102885e // 跳转到Done // -- notFloat -- // 当ldc字节码指令加载的为非float,也就是int类型数据时通过push加入表达式栈 0x00007fffe1028859: mov 0x58(%rcx,%rbx,8),%eax 0x00007fffe102885d: push %rax // -- Done --
由于ldc指令除了加载String外,还可能加载int和float,如果是int,直接调用push压入表达式栈中,如果是float,则在表达式栈上开辟空间,然后移到到这个开辟的slot中存储。注意,float会使用%xmm0寄存器。
2、fast_aldc虚拟机内部字节码指令
下面介绍_fast_aldc指令,这个指令是虚拟机内部使用的指令而非虚拟机规范定义的指令。_fast_aldc指令的模板定义如下:
def(Bytecodes::_fast_aldc , ubcp|____|clvm|____, vtos, atos, fast_aldc , false );
生成函数为TemplateTable::fast_aldc(bool wide),这个函数生成的汇编代码如下:
// 调用InterpreterMacroAssembler::get_cache_index_at_bcp()函数生成 // 获取字节码指令的操作数,这个操作数已经指向了常量池缓存项的索引,在字节码重写 // 阶段已经进行了字节码重写 0x00007fffe10243d0: movzbl 0x1(%r13),%edx // 调用InterpreterMacroAssembler::load_resolved_reference_at_index()函数生成 // shl表示逻辑左移,相当于乘4,因为ConstantPoolCacheEntry的大小为4个字 0x00007fffe10243d5: shl $0x2,%edx // 获取Method* 0x00007fffe10243d8: mov -0x18(%rbp),%rax // 获取ConstMethod* 0x00007fffe10243dc: mov 0x10(%rax),%rax // 获取ConstantPool* 0x00007fffe10243e0: mov 0x8(%rax),%rax // 获取ConstantPool::_resolved_references属性的值,这个值 // 是一个指向对象数组的指针 0x00007fffe10243e4: mov 0x30(%rax),%rax // JNIHandles::resolve(obj) 0x00007fffe10243e8: mov (%rax),%rax // 从_resolved_references数组指定的下标索引处获取oop,先进行索引偏移 0x00007fffe10243eb: add %rdx,%rax // 要在%rax上加0x10,是因为数组对象的头大小为2个字,加上后 // %rax就指向了oop 0x00007fffe10243ee: mov 0x10(%rax),%eax
获取_resolved_references属性的值,涉及到的2个属性在ConstantPool类中的定义如下:
// Array of resolved objects from the constant pool and map from resolved // object index to original constant pool index jobject _resolved_references; // jobject是指针类型 Array<u2>* _reference_map;
关于_resolved_references指向的其实是Object数组。在ConstantPool::initialize_resolved_references()函数中初始化这个属性。调用链如下:
ConstantPool::initialize_resolved_references() constantPool.cpp Rewriter::make_constant_pool_cache() rewriter.cpp Rewriter::Rewriter() rewriter.cpp Rewriter::rewrite() rewriter.cpp InstanceKlass::rewrite_class() instanceKlass.cpp InstanceKlass::link_class_impl() instanceKlass.cpp
后续如果需要连接ldc等指令时,可能会调用如下函数:(我们只讨论ldc加载String类型数据的问题,所以我们只看往_resolved_references属性中放入表示String的oop的逻辑,MethodType与MethodHandle将不再介绍,有兴趣的可自行研究)
oop ConstantPool::string_at_impl( constantPoolHandle this_oop, int which, int obj_index, TRAPS ) { oop str = this_oop->resolved_references()->obj_at(obj_index); if (str != NULL) return str; Symbol* sym = this_oop->unresolved_string_at(which); str = StringTable::intern(sym, CHECK_(NULL)); this_oop->string_at_put(which, obj_index, str); return str; } void string_at_put(int which, int obj_index, oop str) { // 获取类型为jobject的_resolved_references属性的值 objArrayOop tmp = resolved_references(); tmp->obj_at_put(obj_index, str); }
在如上函数中向_resolved_references数组中设置缓存的值。
大概的思路就是:如果ldc加载的是字符串,那么尽量通过_resolved_references数组中一次性找到表示字符串的oop,否则要通过原常量池下标索引找到Symbol实例(Symbol实例是HotSpot VM内部使用的、用来表示字符串),根据Symbol实例生成对应的oop,然后通过常量池缓存下标索引设置到_resolved_references中。当下次查找时,通过这个常量池缓存下标缓存找到表示字符串的oop。
获取到_resolved_references属性的值后接着看生成的汇编代码,如下:
// ... // %eax中存储着表示字符串的oop 0x00007fffe1024479: test %eax,%eax // 如果已经获取到了oop,则跳转到resolved 0x00007fffe102447b: jne 0x00007fffe1024481 // 没有获取到oop,需要进行连接操作,0xe5是_fast_aldc的Opcode 0x00007fffe1024481: mov $0xe5,%edx
调用call_VM()函数生成的汇编代码如下:
// 调用InterpreterRuntime::resolve_ldc()函数 0x00007fffe1024486: callq 0x00007fffe1024490 0x00007fffe102448b: jmpq 0x00007fffe1024526 // 将%rdx中的ConstantPoolCacheEntry项存储到第1个参数中 // 调用MacroAssembler::call_VM_helper()函数生成 0x00007fffe1024490: mov %rdx,%rsi // 将返回地址加载到%rax中 0x00007fffe1024493: lea 0x8(%rsp),%rax // 调用call_VM_base()函数生成 // 保存bcp 0x00007fffe1024498: mov %r13,-0x38(%rbp) // 调用MacroAssembler::call_VM_base()函数生成 // 将r15中的值移动到c_rarg0(rdi)寄存器中,也就是为函数调用准备第一个参数 0x00007fffe102449c: mov %r15,%rdi // Only interpreter should have to set fp 只有解释器才必须要设置fp 0x00007fffe102449f: mov %rbp,0x200(%r15) 0x00007fffe10244a6: mov %rax,0x1f0(%r15) // 调用MacroAssembler::call_VM_leaf_base()生成 0x00007fffe10244ad: test $0xf,%esp 0x00007fffe10244b3: je 0x00007fffe10244cb 0x00007fffe10244b9: sub $0x8,%rsp 0x00007fffe10244bd: callq 0x00007ffff66b27ac 0x00007fffe10244c2: add $0x8,%rsp 0x00007fffe10244c6: jmpq 0x00007fffe10244d0 0x00007fffe10244cb: callq 0x00007ffff66b27ac 0x00007fffe10244d0: movabs $0x0,%r10 // 结束调用MacroAssembler::call_VM_leaf_base() 0x00007fffe10244da: mov %r10,0x1f0(%r15) 0x00007fffe10244e1: movabs $0x0,%r10 // 检查是否有异常发生 0x00007fffe10244eb: mov %r10,0x200(%r15) 0x00007fffe10244f2: cmpq $0x0,0x8(%r15) // 如果没有异常发生,则跳转到ok 0x00007fffe10244fa: je 0x00007fffe1024505 // 有异常发生,则跳转到StubRoutines::forward_exception_entry() 0x00007fffe1024500: jmpq 0x00007fffe1000420 // ---- ok ---- // 将JavaThread::vm_result属性中的值存储到oop_result寄存器中并清空vm_result属性的值 0x00007fffe1024505: mov 0x250(%r15),%rax 0x00007fffe102450c: movabs $0x0,%r10 0x00007fffe1024516: mov %r10,0x250(%r15) // 结果调用MacroAssembler::call_VM_base()函数 // 恢复bcp和locals 0x00007fffe102451d: mov -0x38(%rbp),%r13 0x00007fffe1024521: mov -0x30(%rbp),%r14 // 结束调用InterpreterMacroAssembler::call_VM_base()函数 // 结束调用MacroAssembler::call_VM_helper()函数 0x00007fffe1024525: retq // 结束调用MacroAssembler::call_VM()函数,回到 // TemplateTable::fast_aldc()函数继续看生成的代码,只 // 定义了resolved点 // ---- resolved ----
调用的InterpreterRuntime::resolve_ldc()函数的实现如下:
IRT_ENTRY(void, InterpreterRuntime::resolve_ldc( JavaThread* thread, Bytecodes::Code bytecode) ) { ResourceMark rm(thread); methodHandle m (thread, method(thread)); Bytecode_loadconstant ldc(m, bci(thread)); oop result = ldc.resolve_constant(CHECK); thread->set_vm_result(result); } IRT_END
这个函数会调用一系列的函数,相关调用链如下:
ConstantPool::string_at_put() constantPool.hpp ConstantPool::string_at_impl() constantPool.cpp ConstantPool::resolve_constant_at_impl() constantPool.cpp ConstantPool::resolve_cached_constant_at() constantPool.hpp Bytecode_loadconstant::resolve_constant() bytecode.cpp InterpreterRuntime::resolve_ldc() interpreterRuntime.cpp
其中ConstantPool::string_at_impl()函数在前面已经详细介绍过。
调用的resolve_constant()函数的实现如下:
oop Bytecode_loadconstant::resolve_constant(TRAPS) const { int index = raw_index(); ConstantPool* constants = _method->constants(); if (has_cache_index()) { return constants->resolve_cached_constant_at(index, THREAD); } else { return constants->resolve_constant_at(index, THREAD); } }
调用的resolve_cached_constant_at()或resolve_constant_at()函数的实现如下:
oop resolve_cached_constant_at(int cache_index, TRAPS) { constantPoolHandle h_this(THREAD, this); return resolve_constant_at_impl(h_this, _no_index_sentinel, cache_index, THREAD); } oop resolve_possibly_cached_constant_at(int pool_index, TRAPS) { constantPoolHandle h_this(THREAD, this); return resolve_constant_at_impl(h_this, pool_index, _possible_index_sentinel, THREAD); }
调用的resolve_constant_at_impl()函数的实现如下:
oop ConstantPool::resolve_constant_at_impl( constantPoolHandle this_oop, int index, int cache_index, TRAPS ) { oop result_oop = NULL; Handle throw_exception; if (cache_index == _possible_index_sentinel) { cache_index = this_oop->cp_to_object_index(index); } if (cache_index >= 0) { result_oop = this_oop->resolved_references()->obj_at(cache_index); if (result_oop != NULL) { return result_oop; } index = this_oop->object_to_cp_index(cache_index); } jvalue prim_value; // temp used only in a few cases below int tag_value = this_oop->tag_at(index).value(); switch (tag_value) { // ... case JVM_CONSTANT_String: assert(cache_index != _no_index_sentinel, "should have been set"); if (this_oop->is_pseudo_string_at(index)) { result_oop = this_oop->pseudo_string_at(index, cache_index); break; } result_oop = string_at_impl(this_oop, index, cache_index, CHECK_NULL); break; // ... } if (cache_index >= 0) { Handle result_handle(THREAD, result_oop); MonitorLockerEx ml(this_oop->lock()); oop result = this_oop->resolved_references()->obj_at(cache_index); if (result == NULL) { this_oop->resolved_references()->obj_at_put(cache_index, result_handle()); return result_handle(); } else { return result; } } else { return result_oop; } }
通过常量池的tags数组判断,如果常量池下标index处存储的是JVM_CONSTANT_String常量池项,则调用string_at_impl()函数,这个函数在之前已经介绍过,会根据表示字符串的Symbol实例创建出表示字符串的oop。在ConstantPool::resolve_constant_at_impl()函数中得到oop后就存储到ConstantPool::_resolved_references属性中,最后返回这个oop,这正是ldc需要的oop。
通过重写fast_aldc字节码指令,达到了通过少量指令就直接获取到oop的目的,而且oop是缓存的,所以字符串常量在HotSpot VM中的表示唯一,也就是只有一个oop表示。
C++函数约定返回的值会存储到%rax中,根据_fast_aldc字节码指令的模板定义可知,tos_out为atos,所以后续并不需要进一步操作。
HotSpot VM会在类的连接过程中重写某些字节码,如ldc字节码重写为fast_aldc,还有常量池的tags类型数组、常量池缓存等内容在《深入剖析Java虚拟机:源码剖析与实例详解》中详细介绍过,这里不再介绍。
第21篇-加载与存储指令之ldc与_fast_aldc指令(3)
iload会将int类型的本地变量推送至栈顶。模板定义如下:
def(Bytecodes::_iload , ubcp|____|clvm|____, vtos, itos, iload , _ );
iload指令的格式如下:
iload index
index是一个无符号byte类型整数,指向局部变量表的索引值。
生成函数为TemplateTable::iload(),反编译后的汇编代码如下:
// 将%ebx指向下一条字节码指令的首地址 0x00007fffe1028d30: movzbl 0x2(%r13),%ebx // $0x15为_iload指令的操作码值 0x00007fffe1028d35: cmp $0x15,%ebx // 当下一条指令为iload时,直接跳转到done 0x00007fffe1028d38: je 0x00007fffe1028deb // done // 0xdf为_fast_iload指令的操作码值 0x00007fffe1028d3e: cmp $0xdf,%ebx // 将_fast_iload2指令移动到%ecx 0x00007fffe1028d44: mov $0xe0,%ecx 0x00007fffe1028d49: je 0x00007fffe1028d5a // rewrite // 0x34为_caload指令的操作码 // _caload指令表示从数组中加载一个char类型数据到操作数栈 0x00007fffe1028d4b: cmp $0x34,%ebx // 将_fast_icaload移动到%ecx中 0x00007fffe1028d4e: mov $0xe1,%ecx 0x00007fffe1028d53: je 0x00007fffe1028d5a // rewrite // 将_fast_iload移动到%ecx中 0x00007fffe1028d55: mov $0xdf,%ecx // -- rewrite -- // 调用patch_bytecode()函数 // 重写为fast版本,因为%cl中存储的是字节码的fast版本,%ecx的8位叫%cl 0x00007fffe1028de7: mov %cl,0x0(%r13) // -- done -- // 获取字节码指令的操作数,这个操作数为本地变量表的索引 0x00007fffe1028deb: movzbl 0x1(%r13),%ebx 0x00007fffe1028df0: neg %rbx // 通过本地变量表索引从本地变量表中加载值到%eax中, // %eax中存储的就是栈顶缓存值,所以不需要压入栈内 0x00007fffe1028df3: mov (%r14,%rbx,8),%eax
执行的逻辑如下:
假设现在有个方法的字节码指令流为连接3个iload指令,这3个iload指令前后都为非iload指令。那么重写的过程如下:
汇编代码在第一次执行时,如果判断最后一个_iload之后是非_iload指令,则会重写最后一个_iload指令为_fast_iload;第二次执行时,当第2个字节码指令为_iload,而之后接着判断为_fast_iload时,会更新第2个_iload为_fast_iload2。
执行_fast_iload和执行_fast_iload2都可以提高程序执行的效率,_fast_icaload指令也一样,下面详细介绍一下这几个指令。
1、_fast_iload指令
_fast_iload会将int类型的本地变量推送至栈顶。模板定义如下:
def(Bytecodes::_fast_iload , ubcp|____|____|____, vtos, itos, fast_iload , _ );
生成函数为TemplateTable::fast_iload() ,汇编代码如下:
0x00007fffe1023f90: movzbl 0x1(%r13),%ebx 0x00007fffe1023f95: neg %rbx 0x00007fffe1023f98: mov (%r14,%rbx,8),%eax
汇编代码很简单,这里不再过多说。
执行_fast_iload指令与执行_iload指令相比,不用再进行之前汇编介绍的那么多判断,也没有重写的逻辑,所以会提高执行效率。
2、_fast_iload2指令
_fast_iload2会将int类型的本地变量推送至栈顶。模板定义如下:
def(Bytecodes::_fast_iload2 , ubcp|____|____|____, vtos, itos, fast_iload2 , _ );
生成函数为TemplateTable::fast_iload2() ,汇编代码如下:
0x00007fffe1024010: movzbl 0x1(%r13),%ebx 0x00007fffe1024015: neg %rbx 0x00007fffe1024018: mov (%r14,%rbx,8),%eax 0x00007fffe102401c: push %rax 0x00007fffe102401d: movzbl 0x3(%r13),%ebx 0x00007fffe1024022: neg %rbx 0x00007fffe1024025: mov (%r14,%rbx,8),%eax
可以看到,此指令就相当于连续执行了2次iload指令,省去了指令跳转,所以效率要高一些。
3、_fast_icaload指令
caload指令表示从数组中加载一个char类型数据到操作数栈。
_fast_icaload会将char类型数组指定索引的值推送至栈顶。模板定义如下:
def(Bytecodes::_fast_icaload , ubcp|____|____|____, vtos, itos, fast_icaload , _ );
生成函数为TemplateTable::fast_icaload(),生成的汇编代码如下:
0x00007fffe1024090: movzbl 0x1(%r13),%ebx 0x00007fffe1024095: neg %rbx // %eax中存储着index 0x00007fffe1024098: mov (%r14,%rbx,8),%eax // %rdx中存储着arrayref 0x00007fffe102409c: pop %rdx // 将一个双字扩展后送到一个四字中,%rax中存储着index 0x00007fffe102409d: movslq %eax,%rax // %rdx指向数组对象的首地址,偏移0xc后获取length属性的值 0x00007fffe10240a0: cmp 0xc(%rdx),%eax 0x00007fffe10240a3: mov %eax,%ebx // 如果数组索引index等于数组的长度或大于数组长度时,那么跳转 // 到_throw_ArrayIndexOutOfBoundsException_entry抛出异常 0x00007fffe10240a5: jae 0x00007fffe100ff20 // 在指定数组arrayref中加载指定索引处index的值 0x00007fffe10240ab: movzwl 0x10(%rdx,%rax,2),%eax
可以看到,此指令省去了指令跳转,所以效率要高一些。
由于字数限制,《模板解释器解释执行Java字节码指令(下)》将在下篇中释出