关于java:JVM源码解析模板解释器解释执行Java字节码指令上

29次阅读

共计 43534 个字符,预计需要花费 109 分钟才能阅读完成。

本文由 HeapDump 性能社区首席讲师鸠摩(马智)受权整顿公布

第 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 种:

  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)= (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 字节码指令(下)》将在下篇中释出

正文完
 0