【JVM源碼解析】模板解釋器解釋執行Java位元組碼指令(上)

本文由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

[email protected](@為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 [email protected],%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位元組碼指令(下)》將在下篇中釋出