1. 程式人生 > 其它 >【JVM原始碼解析】模板直譯器解釋執行Java位元組碼指令(上)

【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

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位元組碼指令(下)》將在下篇中釋出