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

HeapDump效能社群發表於2021-11-25
本文由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位
通用暫存器組eaxrax
ecxrcx
edxrdx
ebxrbx
esprsp
ebprbp
esirsi
edirdi
-r8~r15
浮點暫存器組st0~st7st0~st7
XMM暫存器組XMM0~XMM7XMM0~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指令:

指令運算元描述
movqI/R/M,R/M從一個記憶體位置複製1個雙字(64位,8位元組)大小的資料到另外一個記憶體位置
movlI/R/M,R/M從一個記憶體位置複製1個字(32位,4位元組)大小的資料到另外一個記憶體位置
movwI/R/M, R/M從一個記憶體位置複製2個位元組(16位)大小的資料到另外一個記憶體位置
movbI/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宣告通用暫存器字尾大小(位元組)
charb1
shortw2
(unsigned) int / long / char*l4
floats4
doublel5
long doublet10/12

注意:通用暫存器使用字尾“l”同時表示4位元組整數和8位元組雙精度浮點數,這不會產生歧義,因為浮點數使用的是完全不同的指令和暫存器。

我們後面只介紹call、push等指令時,如果在研究HotSpot VM虛擬機器的彙編遇到了callq,pushq等指令時,千萬別不認識,字尾就是表示了運算元的大小。

下表為運算元的格式和定址模式。

格式運算元值名稱樣例(通用暫存器 = C語言)
$ImmImm立即數定址$1 = 1
EaR[Ea]暫存器定址%eax = eax
ImmM[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指令的形式如下表所示。 

指令運算元描述
pushI/R/MPUSH 指令首先減少 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指令的形式如下表所示。 

指令運算元描述
popR/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個運算元alah
16位dx:ax指令第1個運算元axdx
32位edx:eax指令第1個運算元eaxedx

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指跳轉條件,如果為真,則程式跳轉到目的地址;否則執行下一條指令。相關的條件跳轉指令如下表所示。

指令跳轉條件描述指令跳轉條件描述
jzZF=1為0時跳轉jbeCF=1或ZF=1大於或等於時跳轉
jnzZF=0不為0時跳轉jnbeCF=0且ZF=0小於或等於時跳轉
jeZF=1相等時跳轉jgZF=0且SF=OF大於時跳轉
jneZF=0不相等時跳轉jngZF=1或SF!=OF不大於時跳轉
jaCF=0且ZF=0大於時跳轉jgeSF=OF大於或等於時跳轉
jnaCF=1或ZF=1不大於時跳轉jngeSF!=OF小於或等於時跳轉
jaeCF=0大於或等於時跳轉jlSF!=OF小於時跳轉
jnaeCF=1小於或等於時跳轉jnlSF=OF不小於時跳轉
jbCF=1大於時跳轉jleZF=1或SF!=OF小於或等於時跳轉
jnbCF=0不大於時跳轉jnleZF=0且SF=OF大於或等於時跳轉

4.3 cmp指令

cmp指令的格式如下:

cmp I/R/M,R/M

cmp指令通過比較第2個運算元減去第1個運算元的差,根據結果設定標誌暫存器eflags中的標誌位。cmp指令和sub指令類似,不過cmp指令不會改變運算元的值。

運算元和所設定的標誌位之間的關係如表所示。

運算元的關係CFZFOF
第1個運算元小於第2個運算元00SF
第1個運算元等於第2個運算元010
第1個運算元大於第2個運算元10not 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暫存器標誌的一些指令如下表所示。 

指令運算元描述
pushfdRPUSHFD 指令把 32 位 EFLAGS 暫存器內容壓入堆疊
popfdR 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個邏輯。

載入與儲存相關操作的位元組碼指令如下表所示。

位元組碼助詞符指令含義
0x00nop什麼都不做
0x01aconst_null    將null推送至棧頂
0x02iconst_m1將int型-1推送至棧頂
0x03iconst_0將int型0推送至棧頂
0x04iconst_1將int型1推送至棧頂
0x05iconst_2將int型2推送至棧頂
0x06iconst_3將int型3推送至棧頂
0x07iconst_4將int型4推送至棧頂
0x08iconst_5將int型5推送至棧頂
0x09lconst_0將long型0推送至棧頂
0x0alconst_1將long型1推送至棧頂
0x0bfconst_0將float型0推送至棧頂
0x0cfconst_1將float型1推送至棧頂
0x0dfconst_2將float型2推送至棧頂
0x0edconst_0將double型0推送至棧頂
0x0fdconst_1將double型1推送至棧頂
0x10bipush將單位元組的常量值(-128~127)推送至棧頂
0x11sipush將一個短整型常量值(-32768~32767)推送至棧頂
0x12ldc將int、float或String型常量值從常量池中推送至棧頂
0x13ldc_w將int,、float或String型常量值從常量池中推送至棧頂(寬索引)
0x14ldc2_w將long或double型常量值從常量池中推送至棧頂(寬索引)
0x15iload將指定的int型本地變數推送至棧頂
0x16lload將指定的long型本地變數推送至棧頂
0x17fload將指定的float型本地變數推送至棧頂
0x18dload將指定的double型本地變數推送至棧頂
0x19aload將指定的引用型別本地變數推送至棧頂
0x1aiload_0將第一個int型本地變數推送至棧頂
0x1biload_1將第二個int型本地變數推送至棧頂
0x1ciload_2將第三個int型本地變數推送至棧頂
0x1diload_3將第四個int型本地變數推送至棧頂
0x1elload_0將第一個long型本地變數推送至棧頂
0x1flload_1將第二個long型本地變數推送至棧頂
0x20lload_2將第三個long型本地變數推送至棧頂
0x21lload_3將第四個long型本地變數推送至棧頂
0x22fload_0將第一個float型本地變數推送至棧頂
0x23fload_1將第二個float型本地變數推送至棧頂
0x24fload_2將第三個float型本地變數推送至棧頂
0x25fload_3將第四個float型本地變數推送至棧頂
0x26dload_0將第一個double型本地變數推送至棧頂
0x27dload_1將第二個double型本地變數推送至棧頂
0x28dload_2將第三個double型本地變數推送至棧頂
0x29dload_3將第四個double型本地變數推送至棧頂
0x2aaload_0將第一個引用型別本地變數推送至棧頂
0x2baload_1將第二個引用型別本地變數推送至棧頂
0x2caload_2將第三個引用型別本地變數推送至棧頂
0x2daload_3將第四個引用型別本地變數推送至棧頂
0x2eiaload將int型陣列指定索引的值推送至棧頂
0x2flaload將long型陣列指定索引的值推送至棧頂
0x30faload將float型陣列指定索引的值推送至棧頂
0x31daload將double型陣列指定索引的值推送至棧頂
0x32aaload將引用型陣列指定索引的值推送至棧頂
0x33baload將boolean或byte型陣列指定索引的值推送至棧頂
0x34caload將char型陣列指定索引的值推送至棧頂
0x35saload將short型陣列指定索引的值推送至棧頂
0x36istore將棧頂int型數值存入指定本地變數
0x37lstore將棧頂long型數值存入指定本地變數
0x38fstore將棧頂float型數值存入指定本地變數
0x39dstore將棧頂double型數值存入指定本地變數
0x3aastore將棧頂引用型數值存入指定本地變數
0x3bistore_0將棧頂int型數值存入第一個本地變數
0x3cistore_1將棧頂int型數值存入第二個本地變數
0x3distore_2將棧頂int型數值存入第三個本地變數
0x3eistore_3將棧頂int型數值存入第四個本地變數
0x3flstore_0將棧頂long型數值存入第一個本地變數
0x40lstore_1將棧頂long型數值存入第二個本地變數
0x41lstore_2將棧頂long型數值存入第三個本地變數
0x42lstore_3將棧頂long型數值存入第四個本地變數
0x43fstore_0將棧頂float型數值存入第一個本地變數
0x44fstore_1將棧頂float型數值存入第二個本地變數
0x45fstore_2將棧頂float型數值存入第三個本地變數
0x46fstore_3將棧頂float型數值存入第四個本地變數
0x47dstore_0將棧頂double型數值存入第一個本地變數
0x48dstore_1將棧頂double型數值存入第二個本地變數
0x49dstore_2將棧頂double型數值存入第三個本地變數
0x4adstore_3將棧頂double型數值存入第四個本地變數
0x4bastore_0將棧頂引用型數值存入第一個本地變數
0x4castore_1將棧頂引用型數值存入第二個本地變數
0x4dastore_2將棧頂引用型數值存入第三個本地變數
0x4eastore_3將棧頂引用型數值存入第四個本地變數
0x4fiastore將棧頂int型數值存入指定陣列的指定索引位置
0x50lastore將棧頂long型數值存入指定陣列的指定索引位置
0x51fastore將棧頂float型數值存入指定陣列的指定索引位置
0x52dastore將棧頂double型數值存入指定陣列的指定索引位置
0x53aastore將棧頂引用型數值存入指定陣列的指定索引位置
0x54bastore將棧頂boolean或byte型數值存入指定陣列的指定索引位置
0x55castore將棧頂char型數值存入指定陣列的指定索引位置
0x56sastore將棧頂short型數值存入指定陣列的指定索引位置
0xc4wide擴充區域性變數表的訪問索引的指令

我們不會對每個位元組碼指令都檢視對應的機器指令片段的邏輯(其實是反編譯機器指令片段為彙編後,通過檢視彙編理解執行邏輯),有些指令的邏輯是類似的,這裡只選擇幾個典型的介紹。

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

相關文章