本文由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種:
- 通用指令
- x87 FPU指令,浮點數運算的指令
- SIMD指令,就是SSE指令
- 系統指令,寫OS核心時使用的特殊指令
下面介紹一些通用的指令。指令由標識命令種類的助記符(mnemonic)和作為引數的運算元(operand)組成。例如move指令:
指令 | 運算元 | 描述 |
movq | I/R/M,R/M | 從一個記憶體位置複製1個雙字(64位,8位元組)大小的資料到另外一個記憶體位置 |
movl | I/R/M,R/M | 從一個記憶體位置複製1個字(32位,4位元組)大小的資料到另外一個記憶體位置 |
movw | I/R/M, R/M | 從一個記憶體位置複製2個位元組(16位)大小的資料到另外一個記憶體位置 |
movb | I/R/M, R/M | 從一個記憶體位置複製1個位元組(8位)大小的資料到另外一個記憶體位置 |
movl為助記符。助記符有字尾,如movl中的字尾l表示作為運算元的物件的資料大小。l為long的縮寫,表示32位的大小,除此之外,還有b、w,q分別表示8位、16位和64位的大小。
指令的運算元如果不止1個,就將每個運算元以逗號分隔。每個運算元都會指明是否可以是立即模式值(I)、暫存器(R)或記憶體地址(M)。
另外還要提示一下,在x86的組合語言中,採用記憶體位置的運算元最多隻能出現一個,例如不可能出現mov M,M指令。
通用暫存器中每個操作都可以有一個字元的字尾,表明運算元的大小,如下表所示。
C宣告 | 通用暫存器字尾 | 大小(位元組) |
char | b | 1 |
short | w | 2 |
(unsigned) int / long / char* | l | 4 |
float | s | 4 |
double | l | 5 |
long double | t | 10/12 |
注意:通用暫存器使用字尾“l”同時表示4位元組整數和8位元組雙精度浮點數,這不會產生歧義,因為浮點數使用的是完全不同的指令和暫存器。
我們後面只介紹call、push等指令時,如果在研究HotSpot VM虛擬機器的彙編遇到了callq,pushq等指令時,千萬別不認識,字尾就是表示了運算元的大小。
下表為運算元的格式和定址模式。
格式 | 運算元值 | 名稱 | 樣例(通用暫存器 = C語言) |
$Imm | Imm | 立即數定址 | $1 = 1 |
Ea | R[Ea] | 暫存器定址 | %eax = eax |
Imm | M[Imm] | 絕對定址 | 0x104 = *0x104 |
(Ea) | M[R[Ea]] | 間接定址 | (%eax)= *eax |
Imm(Ea) | M[Imm+R[Ea]] | (基址+偏移量)定址 | 4(%eax) = *(4+eax) |
(Ea,Eb) | M[R[Ea]+R[Eb]] | 變址 | (%eax,%ebx) = *(eax+ebx) |
Imm(Ea,Eb) | M[Imm+R[Ea]+R[Eb]] | 定址 | 9(%eax,%ebx)= *(9+eax+ebx) |
(,Ea,s) | M[R[Ea]*s] | 伸縮化變址定址 | (,%eax,4)= (eax4) |
Imm(,Ea,s) | M[Imm+R[Ea]*s] | 伸縮化變址定址 | 0xfc(,%eax,4)= (0xfc+eax4) |
(Ea,Eb,s) | M(R[Ea]+R[Eb]*s) | 伸縮化變址定址 | (%eax,%ebx,4) = (eax+ebx4) |
Imm(Ea,Eb,s) | M(Imm+R[Ea]+R[Eb]*s) | 伸縮化變址定址 | 8(%eax,%ebx,4) = (8+eax+ebx4) |
注:M[xx]表示在儲存器中xx地址的值,R[xx]表示暫存器xx的值,這種表示方法將暫存器、記憶體都看出一個大陣列的形式。
彙編根據編譯器的不同,有2種書寫格式:
(1)Intel : Windows派系\
(2)AT&T: Unix派系
下面簡單介紹一下兩者的不同。
下面就來認識一下常用的指令。
下面我們以給出的是AT&T彙編的寫法,這兩種寫法有如下不同。
1、資料傳送指令
將資料從一個地方傳送到另外一個地方。
1.1 mov指令
我們在介紹mov指令時介紹的全一些,因為mov指令是出現頻率最高的指令,助記符中的字尾也比較多。
mov指令的形式有3種,如下:
mov #普通的move指令
movs #符號擴充套件的move指令,將源運算元進行符號擴充套件並傳送到一個64位暫存器或儲存單元中。movs就表示符號擴充套件
movz #零擴充套件的move指令,將源運算元進行零擴充套件後傳送到一個64位暫存器或儲存單元中。movz就表示零擴充套件
mov指令後有一個字母可表示運算元大小,形式如下:
movb #完成1個位元組的複製
movw #完成2個位元組的複製
movl #完成4個位元組的複製
movq #完成8個位元組的複製
還有一個指令,如下:
movabsq I,R
與movq有所不同,它是將一個64位的值直接存到一個64位暫存器中。
movs指令的形式如下:
movsbw #作符號擴充套件的1位元組複製到2位元組
movsbl #作符號擴充套件的1位元組複製到4位元組
movsbq #作符號擴充套件的1位元組複製到8位元組
movswl #作符號擴充套件的2位元組複製到4位元組
movswq #作符號擴充套件的2位元組複製到8位元組
movslq #作符號擴充套件的4位元組複製到8位元組
movz指令的形式如下:
movzbw #作0擴充套件的1位元組複製到2位元組
movzbl #作0擴充套件的1位元組複製到4位元組
movzbq #作0擴充套件的1位元組複製到8位元組
movzwl #作0擴充套件的2位元組複製到4位元組
movzwq #作0擴充套件的2位元組複製到8位元組
movzlq #作0擴充套件的4位元組複製到8位元組
舉個例子如下:
movl %ecx,%eax
movl (%ecx),%eax
第一條指令將暫存器ecx中的值複製到eax暫存器;第二條指令將ecx暫存器中的資料作為地址訪問記憶體,並將記憶體上的資料載入到eax暫存器中。
1.2 cmov指令
cmov指令的格式如下:
cmovxx
其中xx代表一個或者多個字母,這些字母表示將觸發傳送操作的條件。條件取決於 EFLAGS 暫存器的當前值。
eflags暫存器中各個們如下圖所示。
其中與cmove指令相關的eflags暫存器中的位有CF(數學表示式產生了進位或者借位) 、OF(整數值無窮大或者過小)、PF(暫存器包含數學操作造成的錯誤資料)、SF(結果為正不是負)和ZF(結果為零)。
下表為無符號條件傳送指令。
指令對 | 描述 | eflags狀態 |
cmova/cmovnbe | 大於/不小於或等於 | (CF或ZF)=0 |
cmovae/cmovnb | 大於或者等於/不小於 | CF=0 |
cmovnc | 無進位 | CF=0 |
cmovb/cmovnae | 大於/不小於或等於 | CF=1 |
cmovc | 進位 | CF=1 |
cmovbe/cmovna | 小於或者等於/不大於 | (CF或ZF)=1 |
cmove/cmovz | 等於/零 | ZF=1 |
cmovne/cmovnz | 不等於/不為零 | ZF=0 |
cmovp/cmovpe | 奇偶校驗/偶校驗 | PF=1 |
cmovnp/cmovpo | 非奇偶校驗/奇校驗 | PF=0 |
無符號條件傳送指令依靠進位、零和奇偶校驗標誌來確定兩個運算元之間的區別。
下表為有符號條件傳送指令。
指令對 | 描述 | eflags狀態 |
cmovge/cmovnl | 大於或者等於/不小於 | (SF異或OF)=0 |
cmovl/cmovnge | 大於/不大於或者等於 | (SF異或OF)=1 |
cmovle/cmovng | 小於或者等於/不大於 | ((SF異或OF)或ZF)=1 |
cmovo | 溢位 | OF=1 |
cmovno | 未溢位 | OF=0 |
cmovs | 帶符號(負) | SF=1 |
cmovns | 無符號(非負) | SF=0 |
舉個例子如下:
// 將vlaue數值載入到ecx暫存器中
movl value,%ecx
// 使用cmp指令比較ecx和ebx這兩個暫存器中的值,具體就是用ecx減去ebx然後設定eflags
cmp %ebx,%ecx
// 如果ecx的值大於ebx,使用cmova指令設定ebx的值為ecx中的值
cmova %ecx,%ebx
注意AT&T彙編的第1個運算元在前,第2個運算元在後。
1.3 push和pop指令
push指令的形式如下表所示。
指令 | 運算元 | 描述 |
push | I/R/M | PUSH 指令首先減少 ESP 的值,再將源運算元複製到堆疊。運算元是 16 位的,則 ESP 減 2,運算元是 32 位的,則 ESP 減 4 |
pusha | 指令按序(AX、CX、DX、BX、SP、BP、SI 和 DI)將 16 位通用暫存器壓入堆疊。 | |
pushad | 指令按照 EAX、ECX、EDX、EBX、ESP(執行 PUSHAD 之前的值)、EBP、ESI 和 EDI 的順序,將所有 32 位通用暫存器壓入堆疊。 |
pop指令的形式如下表所示。
指令 | 運算元 | 描述 |
pop | R/M | 指令首先把 ESP 指向的堆疊元素內容複製到一個 16 位或 32 位目的運算元中,再增加 ESP 的值。如果運算元是 16 位的,ESP 加 2,如果運算元是 32 位的,ESP 加 4 |
popa | 指令按照相反順序將同樣的暫存器彈出堆疊 | |
popad | 指令按照相反順序將同樣的暫存器彈出堆疊 |
1.4 xchg與xchgl
這個指令用於交換運算元的值,交換指令XCHG是兩個暫存器,暫存器和記憶體變數之間內容的交換指令,兩個運算元的資料型別要相同,可以是一個位元組,也可以是一個字,也可以是雙字。格式如下:
xchg R/M,R/M
xchgl I/R,I/R、
兩個運算元不能同時為記憶體變數。xchgl指令是一條古老的x86指令,作用是交換兩個暫存器或者記憶體地址裡的4位元組值,兩個值不能都是記憶體地址,他不會設定條件碼。
1.5 lea
lea計算源運算元的實際地址,並把結果儲存到目標運算元,而目標運算元必須為通用暫存器。格式如下:
lea M,R
lea(Load Effective Address)指令將地址載入到暫存器。
舉例如下:
movl 4(%ebx),%eax
leal 4(%ebx),%eax
第一條指令表示將ebx暫存器中儲存的值加4後得到的結果作為記憶體地址進行訪問,並將記憶體地址中儲存的資料載入到eax暫存器中。
第二條指令表示將ebx暫存器中儲存的值加4後得到的結果作為記憶體地址存放到eax暫存器中。
再舉個例子,如下:
leaq a(b, c, d), %rax
計算地址a + b + c * d,然後把最終地址載到暫存器rax中。可以看到只是簡單的計算,不引用源運算元裡的暫存器。這樣的完全可以把它當作乘法指令使用。
2、算術運算指令
下面介紹對有符號整數和無符號整數進行操作的基本運算指令。
2.1 add與adc指令
指令的格式如下:
add I/R/M,R/M
adc I/R/M,R/M
指令將兩個運算元相加,結果儲存在第2個運算元中。
對於第1條指令來說,由於暫存器和儲存器都有位寬限制,因此在進行加法運算時就有可能發生溢位。運算如果溢位的話,標誌暫存器eflags中的進位標誌(Carry Flag,CF)就會被置為1。
對於第2條指令來說,利用adc指令再加上進位標誌eflags.CF,就能在32位的機器上進行64位資料的加法運算。
常規的算術邏輯運算指令只要將原來IA-32中的指令擴充套件到64位即可。如addq就是四字相加。
2.2 sub與sbb指令
指令的格式如下:
sub I/R/M,R/M
sbb I/R/M,R/M
指令將用第2個運算元減去第1個運算元,結果儲存在第2個運算元中。
2.3 imul與mul指令
指令的格式如下:
imul I/R/M,R
mul I/R/M,R
將第1個運算元和第2個運算元相乘,並將結果寫入第2個運算元中,如果第2個運算元空缺,預設為eax暫存器,最終完整的結果將儲存到edx:eax中。
第1條指令執行有符號乘法,第2條指令執行無符號乘法。
2.4 idiv與div指令
指令的格式如下:
div R/M
idiv R/M
第1條指令執行無符號除法,第2條指令執行有符號除法。被除數由edx暫存器和eax暫存器拼接而成,除數由指令的第1個運算元指定,計算得到的商存入eax暫存器,餘數存入edx暫存器。如下圖所示。
edx:eax
------------ = eax(商)... edx(餘數)
暫存器
運算時被除數、商和除數的資料的位寬是不一樣的,如下表表示了idiv指令和div指令使用的暫存器的情況。
資料的位寬 | 被除數 | 除數 | 商 | 餘數 |
8位 | ax | 指令第1個運算元 | al | ah |
16位 | dx:ax | 指令第1個運算元 | ax | dx |
32位 | edx:eax | 指令第1個運算元 | eax | edx |
idiv指令和div指令通常是對位寬2倍於除數的被除數進行除法運算的。例如對於x86-32機器來說,通用暫存器的倍數為32位,1個暫存器無法容納64位的資料,所以 edx存放被除數的高32位,而eax暫存器存放被除數的低32位。
所以在進行除法運算時,必須將設定在eax暫存器中的32位資料擴充套件到包含edx暫存器在內的64位,即有符號進行符號擴充套件,無符號數進行零擴充套件。
對edx進行符號擴充套件時可以使用cltd(AT&T風格寫法)或cdq(Intel風格寫法)。指令的格式如下:
cltd // 將eax暫存器中的資料符號擴充套件到edx:eax
cltd將eax暫存器中的資料符號擴充套件到edx:eax。
2.5 incl與decl指令
指令的格式如下:
inc R/M
dec R/M
將指令第1個運算元指定的暫存器或記憶體位置儲存的資料加1或減1。
2.6 negl指令
指令的格式如下:
neg R/M
neg指令將第1個運算元的符號進行反轉。
3、位運算指令
3.1 andl、orl與xorl指令
指令的格式如下:
and I/R/M,R/M
or I/R/M,R/M
xor I/R/M,R/M
and指令將第2個運算元與第1個運算元進行按位與運算,並將結果寫入第2個運算元;
or指令將第2個運算元與第1個運算元進行按位或運算,並將結果寫入第2個運算元;
xor指令將第2個運算元與第1個運算元進行按位異或運算,並將結果寫入第2個運算元;
3.2 not指令
指令的格式如下:
not R/M
將運算元按位取反,並將結果寫入運算元中。
3.3 sal、sar、shr指令
指令的格式如下:
sal I/%cl,R/M #算術左移
sar I/%cl,R/M #算術右移
shl I/%cl,R/M #邏輯左移
shr I/%cl,R/M #邏輯右移
sal指令將第2個運算元按照第1個運算元指定的位數進行左移操作,並將結果寫入第2個運算元中。移位之後空出的低位補0。指令的第1個運算元只能是8位的立即數或cl暫存器,並且都是隻有低5位的資料才有意義,高於或等於6位數將導致暫存器中的所有資料被移走而變得沒有意義。
sar指令將第2個運算元按照第1個運算元指定的位數進行右移操作,並將結果寫入第2個運算元中。移位之後的空出進行符號擴充套件。和sal指令一樣,sar指令的第1個運算元也必須為8位的立即數或cl暫存器,並且都是隻有低5位的資料才有意義。
shl指令和sall指令的動作完全相同,沒有必要區分。
shr令將第2個運算元按照第1個運算元指定的位數進行右移操作,並將結果寫入第2個運算元中。移位之後的空出進行零擴充套件。和sal指令一樣,shr指令的第1個運算元也必須為8位的立即數或cl暫存器,並且都是隻有低5位的資料才有意義。
4、流程控制指令
4.1 jmp指令
指令的格式如下:
jmp I/R
jmp指令將程式無條件跳轉到運算元指定的目的地址。jmp指令可以視作設定指令指標(eip暫存器)的指令。目的地址也可以是星號後跟暫存器的棧,這種方式為間接函式呼叫。例如:
jmp *%eax
將程式跳轉至eax所含地址。
4.2 條件跳轉指令
條件跳轉指令的格式如下:
Jcc 目的地址
其中cc指跳轉條件,如果為真,則程式跳轉到目的地址;否則執行下一條指令。相關的條件跳轉指令如下表所示。
指令 | 跳轉條件 | 描述 | 指令 | 跳轉條件 | 描述 |
jz | ZF=1 | 為0時跳轉 | jbe | CF=1或ZF=1 | 大於或等於時跳轉 |
jnz | ZF=0 | 不為0時跳轉 | jnbe | CF=0且ZF=0 | 小於或等於時跳轉 |
je | ZF=1 | 相等時跳轉 | jg | ZF=0且SF=OF | 大於時跳轉 |
jne | ZF=0 | 不相等時跳轉 | jng | ZF=1或SF!=OF | 不大於時跳轉 |
ja | CF=0且ZF=0 | 大於時跳轉 | jge | SF=OF | 大於或等於時跳轉 |
jna | CF=1或ZF=1 | 不大於時跳轉 | jnge | SF!=OF | 小於或等於時跳轉 |
jae | CF=0 | 大於或等於時跳轉 | jl | SF!=OF | 小於時跳轉 |
jnae | CF=1 | 小於或等於時跳轉 | jnl | SF=OF | 不小於時跳轉 |
jb | CF=1 | 大於時跳轉 | jle | ZF=1或SF!=OF | 小於或等於時跳轉 |
jnb | CF=0 | 不大於時跳轉 | jnle | ZF=0且SF=OF | 大於或等於時跳轉 |
4.3 cmp指令
cmp指令的格式如下:
cmp I/R/M,R/M
cmp指令通過比較第2個運算元減去第1個運算元的差,根據結果設定標誌暫存器eflags中的標誌位。cmp指令和sub指令類似,不過cmp指令不會改變運算元的值。
運算元和所設定的標誌位之間的關係如表所示。
運算元的關係 | CF | ZF | OF |
第1個運算元小於第2個運算元 | 0 | 0 | SF |
第1個運算元等於第2個運算元 | 0 | 1 | 0 |
第1個運算元大於第2個運算元 | 1 | 0 | not SF |
4.4 test指令
指令的格式如下:
test I/R/M,R/M
指令通過比較第1個運算元與第2個運算元的邏輯與,根據結果設定標誌暫存器eflags中的標誌位。test指令本質上和and指令相同,只是test指令不會改變運算元的值。
test指令執行後CF與OF通常會被清零,並根據運算結果設定ZF和SF。運算結果為零時ZF被置為1,SF和最高位的值相同。
舉個例子如下:
test指令同時能夠檢查幾個位。假設想要知道 AL 暫存器的位 0 和位 3 是否置 1,可以使用如下指令:
test al,00001001b #掩碼為0000 1001,測試第0和位3位是否為1
從下面的資料集例子中,可以推斷只有當所有測試位都清 0 時,零標誌位才置 1:
0 0 1 0 0 1 0 1 <- 輸入值
0 0 0 0 1 0 0 1 <- 測試值
0 0 0 0 0 0 0 1 <- 結果:ZF=0
0 0 1 0 0 1 0 0 <- 輸入值
0 0 0 0 1 0 0 1 <- 測試值
0 0 0 0 0 0 0 0 <- 結果:ZF=1
test指令總是清除溢位和進位標誌位,其修改符號標誌位、零標誌位和奇偶標誌位的方法與 AND 指令相同。
4.5 sete指令
根據eflags中的狀態標誌(CF,SF,OF,ZF和PF)將目標運算元設定為0或1。這裡的目標運算元指向一個位元組暫存器(也就是8位暫存器,如AL,BL,CL)或記憶體中的一個位元組。狀態碼字尾(cc)指明瞭將要測試的條件。
獲取標誌位的指令的格式如下:
setcc R/M
指令根據標誌暫存器eflags的值,將運算元設定為0或1。
setcc中的cc和Jcc中的cc類似,可參考表。
4.6 call指令
指令的格式如下:
call I/R/M
call指令會呼叫由運算元指定的函式。call指令會將指令的下一條指令的地址壓棧,再跳轉到運算元指定的地址,這樣函式就能通過跳轉到棧上的地址從子函式返回了。相當於
push %eip
jmp addr
先壓入指令的下一個地址,然後跳轉到目標地址addr。
4.7 ret指令
指令的格式如下:
ret
ret指令用於從子函式中返回。X86架構的Linux中是將函式的返回值設定到eax暫存器並返回的。相當於如下指令:
popl %eip
將call指令壓棧的“call指令下一條指令的地址”彈出棧,並設定到指令指標中。這樣程式就能正確地返回子函式的地方。
從物理上來說,CALL 指令將其返回地址壓入堆疊,再把被呼叫過程的地址複製到指令指標暫存器。當過程準備返回時,它的 RET 指令從堆疊把返回地址彈回到指令指標暫存器。
4.8 enter指令
enter指令通過初始化ebp和esp暫存器來為函式建立函式引數和區域性變數所需要的棧幀。相當於
push %rbp
mov %rsp,%rbp
4.9 leave指令
leave通過恢復ebp與esp暫存器來移除使用enter指令建立的棧幀。相當於
mov %rbp, %rsp
pop %rbp
將棧指標指向幀指標,然後pop備份的原幀指標到%ebp
5.0 int指令
指令的格式如下:
int I
引起給定數字的中斷。這通常用於系統呼叫以及其他核心介面。
5、標誌操作
eflags暫存器的各個標誌位如下圖所示。
操作eflags暫存器標誌的一些指令如下表所示。
指令 | 運算元 | 描述 |
pushfd | R | PUSHFD 指令把 32 位 EFLAGS 暫存器內容壓入堆疊 |
popfd | R | POPFD 指令則把棧頂單元內容彈出到 EFLAGS 暫存器 |
cld | 將eflags.df設定為0 |
第19篇-載入與儲存指令(1)
TemplateInterpreterGenerator::generate_all()函式會生成許多例程(也就是機器指令片段,英文叫Stub),包括呼叫set_entry_points_for_all_bytes()函式生成各個位元組碼對應的例程。
最終會呼叫到TemplateInterpreterGenerator::generate_and_dispatch()函式,呼叫堆疊如下:
TemplateTable::geneate() templateTable_x86_64.cpp
TemplateInterpreterGenerator::generate_and_dispatch() templateInterpreter.cpp
TemplateInterpreterGenerator::set_vtos_entry_points() templateInterpreter_x86_64.cpp
TemplateInterpreterGenerator::set_short_entry_points() templateInterpreter.cpp
TemplateInterpreterGenerator::set_entry_points() templateInterpreter.cpp
TemplateInterpreterGenerator::set_entry_points_for_all_bytes() templateInterpreter.cpp
TemplateInterpreterGenerator::generate_all() templateInterpreter.cpp
InterpreterGenerator::InterpreterGenerator() templateInterpreter_x86_64.cpp
TemplateInterpreter::initialize() templateInterpreter.cpp
interpreter_init() interpreter.cpp
init_globals() init.cpp
呼叫堆疊上的許多函式在之前介紹過,每個位元組碼都會指定一個generator函式,通過Template的_gen屬性儲存。在TemplateTable::generate()函式中呼叫。_gen會生成每個位元組碼對應的機器指令片段,所以非常重要。
首先看一個非常簡單的nop位元組碼指令。這個指令的模板屬性如下:
// Java spec bytecodes ubcp|disp|clvm|iswd in out generator argument
def(Bytecodes::_nop , ____|____|____|____, vtos, vtos, nop , _ );
nop位元組碼指令的生成函式generator不會生成任何機器指令,所以nop位元組碼指令對應的彙編程式碼中只有棧頂快取的邏輯。呼叫set_vtos_entry_points()函式生成的彙編程式碼如下:
// aep
0x00007fffe1027c00: push %rax
0x00007fffe1027c01: jmpq 0x00007fffe1027c30
// fep
0x00007fffe1027c06: sub $0x8,%rsp
0x00007fffe1027c0a: vmovss %xmm0,(%rsp)
0x00007fffe1027c0f: jmpq 0x00007fffe1027c30
// dep
0x00007fffe1027c14: sub $0x10,%rsp
0x00007fffe1027c18: vmovsd %xmm0,(%rsp)
0x00007fffe1027c1d: jmpq 0x00007fffe1027c30
// lep
0x00007fffe1027c22: sub $0x10,%rsp
0x00007fffe1027c26: mov %rax,(%rsp)
0x00007fffe1027c2a: jmpq 0x00007fffe1027c30
// bep cep sep iep
0x00007fffe1027c2f: push %rax
// vep
// 接下來為取指邏輯,開始的地址為0x00007fffe1027c30
可以看到,由於tos_in為vtos,所以如果是aep、bep、cep、sep與iep時,直接使用push指令將%rax中儲存的棧頂快取值壓入表示式棧中。對於fep、dep與lep來說,在棧上開闢對應記憶體的大小,然後將暫存器中的值儲存到表示式的棧頂上,與push指令的效果相同。
在set_vtos_entry_points()函式中會呼叫generate_and_dispatch()函式生成nop指令的機器指令片段及取下一條位元組碼指令的機器指令片段。nop不會生成任何機器指令,而取指的片段如下:
// movzbl 將做了零擴充套件的位元組傳送到雙字,地址為0x00007fffe1027c30
0x00007fffe1027c30: movzbl 0x1(%r13),%ebx
0x00007fffe1027c35: inc %r13
0x00007fffe1027c38: movabs $0x7ffff73ba4a0,%r10
// movabs的源運算元只能是立即數或標號(本質還是立即數),目的運算元是暫存器
0x00007fffe1027c42: jmpq *(%r10,%rbx,8)
r13指向當前要取的位元組碼指令的地址。那麼%r13+1就是跳過了當前的nop指令而指向了下一個位元組碼指令的地址,然後執行movzbl指令將所指向的Opcode載入到%ebx中。
通過jmpq的跳轉地址為%r10+%rbx*8,關於這個跳轉地址在前面詳細介紹過,這裡不再介紹。
我們講解了nop指令,把棧頂快取的邏輯和取指邏輯又回顧了一遍,對於每個位元組碼指令來說都會有有棧頂快取和取指邏輯,後面在介紹位元組碼指令時就不會再介紹這2個邏輯。
載入與儲存相關操作的位元組碼指令如下表所示。
位元組碼 | 助詞符 | 指令含義 |
0x00 | nop | 什麼都不做 |
0x01 | aconst_null | 將null推送至棧頂 |
0x02 | iconst_m1 | 將int型-1推送至棧頂 |
0x03 | iconst_0 | 將int型0推送至棧頂 |
0x04 | iconst_1 | 將int型1推送至棧頂 |
0x05 | iconst_2 | 將int型2推送至棧頂 |
0x06 | iconst_3 | 將int型3推送至棧頂 |
0x07 | iconst_4 | 將int型4推送至棧頂 |
0x08 | iconst_5 | 將int型5推送至棧頂 |
0x09 | lconst_0 | 將long型0推送至棧頂 |
0x0a | lconst_1 | 將long型1推送至棧頂 |
0x0b | fconst_0 | 將float型0推送至棧頂 |
0x0c | fconst_1 | 將float型1推送至棧頂 |
0x0d | fconst_2 | 將float型2推送至棧頂 |
0x0e | dconst_0 | 將double型0推送至棧頂 |
0x0f | dconst_1 | 將double型1推送至棧頂 |
0x10 | bipush | 將單位元組的常量值(-128~127)推送至棧頂 |
0x11 | sipush | 將一個短整型常量值(-32768~32767)推送至棧頂 |
0x12 | ldc | 將int、float或String型常量值從常量池中推送至棧頂 |
0x13 | ldc_w | 將int,、float或String型常量值從常量池中推送至棧頂(寬索引) |
0x14 | ldc2_w | 將long或double型常量值從常量池中推送至棧頂(寬索引) |
0x15 | iload | 將指定的int型本地變數推送至棧頂 |
0x16 | lload | 將指定的long型本地變數推送至棧頂 |
0x17 | fload | 將指定的float型本地變數推送至棧頂 |
0x18 | dload | 將指定的double型本地變數推送至棧頂 |
0x19 | aload | 將指定的引用型別本地變數推送至棧頂 |
0x1a | iload_0 | 將第一個int型本地變數推送至棧頂 |
0x1b | iload_1 | 將第二個int型本地變數推送至棧頂 |
0x1c | iload_2 | 將第三個int型本地變數推送至棧頂 |
0x1d | iload_3 | 將第四個int型本地變數推送至棧頂 |
0x1e | lload_0 | 將第一個long型本地變數推送至棧頂 |
0x1f | lload_1 | 將第二個long型本地變數推送至棧頂 |
0x20 | lload_2 | 將第三個long型本地變數推送至棧頂 |
0x21 | lload_3 | 將第四個long型本地變數推送至棧頂 |
0x22 | fload_0 | 將第一個float型本地變數推送至棧頂 |
0x23 | fload_1 | 將第二個float型本地變數推送至棧頂 |
0x24 | fload_2 | 將第三個float型本地變數推送至棧頂 |
0x25 | fload_3 | 將第四個float型本地變數推送至棧頂 |
0x26 | dload_0 | 將第一個double型本地變數推送至棧頂 |
0x27 | dload_1 | 將第二個double型本地變數推送至棧頂 |
0x28 | dload_2 | 將第三個double型本地變數推送至棧頂 |
0x29 | dload_3 | 將第四個double型本地變數推送至棧頂 |
0x2a | aload_0 | 將第一個引用型別本地變數推送至棧頂 |
0x2b | aload_1 | 將第二個引用型別本地變數推送至棧頂 |
0x2c | aload_2 | 將第三個引用型別本地變數推送至棧頂 |
0x2d | aload_3 | 將第四個引用型別本地變數推送至棧頂 |
0x2e | iaload | 將int型陣列指定索引的值推送至棧頂 |
0x2f | laload | 將long型陣列指定索引的值推送至棧頂 |
0x30 | faload | 將float型陣列指定索引的值推送至棧頂 |
0x31 | daload | 將double型陣列指定索引的值推送至棧頂 |
0x32 | aaload | 將引用型陣列指定索引的值推送至棧頂 |
0x33 | baload | 將boolean或byte型陣列指定索引的值推送至棧頂 |
0x34 | caload | 將char型陣列指定索引的值推送至棧頂 |
0x35 | saload | 將short型陣列指定索引的值推送至棧頂 |
0x36 | istore | 將棧頂int型數值存入指定本地變數 |
0x37 | lstore | 將棧頂long型數值存入指定本地變數 |
0x38 | fstore | 將棧頂float型數值存入指定本地變數 |
0x39 | dstore | 將棧頂double型數值存入指定本地變數 |
0x3a | astore | 將棧頂引用型數值存入指定本地變數 |
0x3b | istore_0 | 將棧頂int型數值存入第一個本地變數 |
0x3c | istore_1 | 將棧頂int型數值存入第二個本地變數 |
0x3d | istore_2 | 將棧頂int型數值存入第三個本地變數 |
0x3e | istore_3 | 將棧頂int型數值存入第四個本地變數 |
0x3f | lstore_0 | 將棧頂long型數值存入第一個本地變數 |
0x40 | lstore_1 | 將棧頂long型數值存入第二個本地變數 |
0x41 | lstore_2 | 將棧頂long型數值存入第三個本地變數 |
0x42 | lstore_3 | 將棧頂long型數值存入第四個本地變數 |
0x43 | fstore_0 | 將棧頂float型數值存入第一個本地變數 |
0x44 | fstore_1 | 將棧頂float型數值存入第二個本地變數 |
0x45 | fstore_2 | 將棧頂float型數值存入第三個本地變數 |
0x46 | fstore_3 | 將棧頂float型數值存入第四個本地變數 |
0x47 | dstore_0 | 將棧頂double型數值存入第一個本地變數 |
0x48 | dstore_1 | 將棧頂double型數值存入第二個本地變數 |
0x49 | dstore_2 | 將棧頂double型數值存入第三個本地變數 |
0x4a | dstore_3 | 將棧頂double型數值存入第四個本地變數 |
0x4b | astore_0 | 將棧頂引用型數值存入第一個本地變數 |
0x4c | astore_1 | 將棧頂引用型數值存入第二個本地變數 |
0x4d | astore_2 | 將棧頂引用型數值存入第三個本地變數 |
0x4e | astore_3 | 將棧頂引用型數值存入第四個本地變數 |
0x4f | iastore | 將棧頂int型數值存入指定陣列的指定索引位置 |
0x50 | lastore | 將棧頂long型數值存入指定陣列的指定索引位置 |
0x51 | fastore | 將棧頂float型數值存入指定陣列的指定索引位置 |
0x52 | dastore | 將棧頂double型數值存入指定陣列的指定索引位置 |
0x53 | aastore | 將棧頂引用型數值存入指定陣列的指定索引位置 |
0x54 | bastore | 將棧頂boolean或byte型數值存入指定陣列的指定索引位置 |
0x55 | castore | 將棧頂char型數值存入指定陣列的指定索引位置 |
0x56 | sastore | 將棧頂short型數值存入指定陣列的指定索引位置 |
0xc4 | wide | 擴充區域性變數表的訪問索引的指令 |
我們不會對每個位元組碼指令都檢視對應的機器指令片段的邏輯(其實是反編譯機器指令片段為彙編後,通過檢視彙編理解執行邏輯),有些指令的邏輯是類似的,這裡只選擇幾個典型的介紹。
1、壓棧型別的指令
(1)aconst_null指令
aconst_null表示將null送到棧頂,模板定義如下:
def(Bytecodes::_aconst_null , ____|____|____|____, vtos, atos, aconst_null , _ );
指令的彙編程式碼如下:
// xor 指令在兩個運算元的對應位之間進行邏輯異或操作,並將結果存放在目標運算元中
// 第1個運算元和第2個運算元相同時,執行異或操作就相當於執行清零操作
xor %eax,%eax
由於tos_out為atos,所以棧頂的結果是快取在%eax暫存器中的,只對%eax暫存器執行xor操作即可。
(2)iconst_m1指令
iconst_m1表示將-1壓入棧內,模板定義如下:
def(Bytecodes::_iconst_m1 , ____|____|____|____, vtos, itos, iconst , -1 );
生成的機器指令經過反彙編後,得到的彙編程式碼如下:
mov $0xffffffff,%eax
其它的與iconst_m1位元組碼指令類似的位元組碼指令,如iconst_0、iconst_1等,模板定義如下:
def(Bytecodes::_iconst_m1 , ____|____|____|____, vtos, itos, iconst , -1 );
def(Bytecodes::_iconst_0 , ____|____|____|____, vtos, itos, iconst , 0 );
def(Bytecodes::_iconst_1 , ____|____|____|____, vtos, itos, iconst , 1 );
def(Bytecodes::_iconst_2 , ____|____|____|____, vtos, itos, iconst , 2 );
def(Bytecodes::_iconst_3 , ____|____|____|____, vtos, itos, iconst , 3 );
def(Bytecodes::_iconst_4 , ____|____|____|____, vtos, itos, iconst , 4 );
def(Bytecodes::_iconst_5 , ____|____|____|____, vtos, itos, iconst , 5 );
可以看到,生成函式都是同一個TemplateTable::iconst()函式。
iconst_0的彙編程式碼如下:
xor %eax,%eax
iconst_@(@為1、2、3、4、5)的位元組碼指令對應的彙編程式碼如下:
// aep
0x00007fffe10150a0: push %rax
0x00007fffe10150a1: jmpq 0x00007fffe10150d0
// fep
0x00007fffe10150a6: sub $0x8,%rsp
0x00007fffe10150aa: vmovss %xmm0,(%rsp)
0x00007fffe10150af: jmpq 0x00007fffe10150d0
// dep
0x00007fffe10150b4: sub $0x10,%rsp
0x00007fffe10150b8: vmovsd %xmm0,(%rsp)
0x00007fffe10150bd: jmpq 0x00007fffe10150d0
// lep
0x00007fffe10150c2: sub $0x10,%rsp
0x00007fffe10150c6: mov %rax,(%rsp)
0x00007fffe10150ca: jmpq 0x00007fffe10150d0
// bep/cep/sep/iep
0x00007fffe10150cf: push %rax
// vep
0x00007fffe10150d0 mov $0x@,%eax // @代表1、2、3、4、5
如果看過我之前寫的文章,那麼如上的彙編程式碼應該能看懂,我在這裡就不再做過多介紹了。
(3)bipush
bipush 將單位元組的常量值推送至棧頂。模板定義如下:
def(Bytecodes::_bipush , ubcp|____|____|____, vtos, itos, bipush , _ );
指令的彙編程式碼如下:
// %r13指向位元組碼指令的地址,偏移1位
// 後取出1個位元組的內容儲存到%eax中
movsbl 0x1(%r13),%eax
由於tos_out為itos,所以將單位元組的常量值儲存到%eax中,這個暫存器是專門用來進行棧頂快取的。
(4)sipush
sipush將一個短整型常量值推送到棧頂,模板定義如下:
def(Bytecodes::_bipush , ubcp|____|____|____, vtos, itos, bipush , _ );
生成的彙編程式碼如下:
// movzwl傳送做了符號擴充套件字到雙字
movzwl 0x1(%r13),%eax
// bswap 以位元組為單位,把32/64位暫存器的值按照低和高的位元組交換
bswap %eax
// (算術右移)指令將目的運算元進行算術右移
sar $0x10,%eax
Java中的短整型佔用2個位元組,所以需要對32位暫存器%eax進行一些操作。由於位元組碼採用大端儲存,所以在處理時統一變換為小端儲存。
2、儲存型別指令
istore指令會將int型別數值存入指定索引的本地變數表,模板定義如下:
def(Bytecodes::_istore , ubcp|____|clvm|____, itos, vtos, istore , _ );
生成函式為TemplateTable::istore(),生成的彙編程式碼如下:
movzbl 0x1(%r13),%ebx
neg %rbx
mov %eax,(%r14,%rbx,8)
由於棧頂快取tos_in為itos,所以直接將%eax中的值儲存到指定索引的本地變數表中。
模板中指定ubcp,因為生成的彙編程式碼中會使用%r13,也就是位元組碼指令指標。
其它的istore、dstore等位元組碼指令的彙編程式碼邏輯也類似,這裡不過多介紹。
第20篇-載入與儲存指令之ldc與_fast_aldc指令(2)
ldc指令將int、float、或者一個類、方法型別或方法控制程式碼的符號引用、還可能是String型常量值從常量池中推送至棧頂。
這一篇介紹一個虛擬機器規範中定義的一個位元組碼指令ldc,另外還有一個虛擬機器內部使用的位元組碼指令_fast_aldc。ldc指令可以載入String、方法型別或方法控制程式碼的符號引用,但是如果要載入String、方法型別或方法控制程式碼的符號引用,則會在類連線過程中重寫ldc位元組碼指令為虛擬機器內部使用的位元組碼指令_fast_aldc。下面我們詳細介紹ldc指令如何載入int、float型別和類型別的資料,以及_fast_aldc載入String、方法型別或方法控制程式碼,還有為什麼要進行位元組碼重寫等問題。
1、ldc位元組碼指令
ldc指令將int、float或String型常量值從常量池中推送至棧頂。模板的定義如下:
def(Bytecodes::_ldc , ubcp|____|clvm|____, vtos, vtos, ldc , false );
ldc位元組碼指令的格式如下:
// index是一個無符號的byte型別資料,指明當前類的執行時常量池的索引
ldc index
呼叫生成函式TemplateTable::ldc(bool wide)。函式生成的彙編程式碼如下:
第1部分程式碼:
// movzbl指令負責拷貝一個位元組,並用0填充其目
// 的運算元中的其餘各位,這種擴充套件方式叫"零擴充套件"
// ldc指定的格式為ldc index,index為一個位元組
0x00007fffe1028530: movzbl 0x1(%r13),%ebx // 載入index到%ebx
// %rcx指向快取池首地址、%rax指向型別陣列_tags首地址
0x00007fffe1028535: mov -0x18(%rbp),%rcx
0x00007fffe1028539: mov 0x10(%rcx),%rcx
0x00007fffe102853d: mov 0x8(%rcx),%rcx
0x00007fffe1028541: mov 0x10(%rcx),%rax
// 從_tags陣列獲取運算元型別並儲存到%edx中
0x00007fffe1028545: movzbl 0x4(%rax,%rbx,1),%edx
// $0x64代表JVM_CONSTANT_UnresolvedClass,比較,如果類還沒有連結,
// 則直接跳轉到call_ldc
0x00007fffe102854a: cmp $0x64,%edx
0x00007fffe102854d: je 0x00007fffe102855d // call_ldc
// $0x67代表JVM_CONSTANT_UnresolvedClassInError,也就是如果類在
// 連結過程中出現錯誤,則跳轉到call_ldc
0x00007fffe102854f: cmp $0x67,%edx
0x00007fffe1028552: je 0x00007fffe102855d // call_ldc
// $0x7代表JVM_CONSTANT_Class,表示如果類已經進行了連線,則
// 跳轉到notClass
0x00007fffe1028554: cmp $0x7,%edx
0x00007fffe1028557: jne 0x00007fffe10287c0 // notClass
// 類在沒有連線或連線過程中出錯,則執行如下的彙編程式碼
// -- call_ldc --
下面看一下呼叫call_VM(rax, CAST_FROM_FN_PTR(address, InterpreterRuntime::ldc), c_rarg1)函式生成的彙編程式碼,CAST_FROM_FN_PTR是巨集,巨集擴充套件後為( (address)((address_word)(InterpreterRuntime::ldc)) )。
在呼叫call_VM()函式時,傳遞的引數如下:
- %rax現在儲存型別陣列首地址,不過傳入是為了接收呼叫函式的結果值
- adr是InterpreterRuntime::ldc()函式首地址
- c_rarg1用rdi暫存器儲存wide值,這裡為0,表示為沒有加wide字首的ldc指令生成彙編程式碼
生成的彙編程式碼如下:
第2部分:
// 將wide的值移到%esi暫存器,為後續
// 呼叫InterpreterRuntime::ldc()函式準備第2個引數
0x00007fffe102855d: mov $0x0,%esi
// 呼叫MacroAssembler::call_VM()函式,通過此函式來呼叫HotSpot VM中用
// C++編寫的函式,通過這個C++編寫的函式來呼叫InterpreterRuntime::ldc()函式
0x00007fffe1017542: callq 0x00007fffe101754c
0x00007fffe1017547: jmpq 0x00007fffe10175df // 跳轉到E1
// 呼叫MacroAssembler::call_VM_helper()函式
// 將棧頂儲存的返回地址設定到%rax中,也就是將儲存地址0x00007fffe1017547
// 的棧的slot地址設定到%rax中
0x00007fffe101754c: lea 0x8(%rsp),%rax
// 呼叫InterpreterMacroAssembler::call_VM_base()函式
// 儲存bcp到棧中特定位置
0x00007fffe1017551: mov %r13,-0x38(%rbp)
// 呼叫MacroAssembler::call_VM_base()函式
// 將r15中的值移動到rdi暫存器中,也就是為函式呼叫準備第一個引數
0x00007fffe1017555: mov %r15,%rdi
// 只有直譯器才必須要設定fp
// 將last_java_fp儲存到JavaThread類的last_java_fp屬性中
0x00007fffe1017558: mov %rbp,0x200(%r15)
// 將last_java_sp儲存到JavaThread類的last_java_sp屬性中
0x00007fffe101755f: mov %rax,0x1f0(%r15)
// ... 省略呼叫MacroAssembler::call_VM_leaf_base()函式
// 重置JavaThread::last_java_sp與JavaThread::last_java_fp屬性的值
0x00007fffe1017589: movabs $0x0,%r10
0x00007fffe1017593: mov %r10,0x1f0(%r15)
0x00007fffe101759a: movabs $0x0,%r10
0x00007fffe10175a4: mov %r10,0x200(%r15)
// check for pending exceptions (java_thread is set upon return)
0x00007fffe10175ab: cmpq $0x0,0x8(%r15)
// 如果沒有異常則直接跳轉到ok
0x00007fffe10175b3: je 0x00007fffe10175be
// 如果有異常則跳轉到StubRoutines::forward_exception_entry()獲取的例程入口
0x00007fffe10175b9: jmpq 0x00007fffe1000420
// -- ok --
// 將JavaThread::vm_result屬性中的值儲存到%rax暫存器中並清空vm_result屬性的值
0x00007fffe10175be: mov 0x250(%r15),%rax
0x00007fffe10175c5: movabs $0x0,%r10
0x00007fffe10175cf: mov %r10,0x250(%r15)
// 結束呼叫MacroAssembler::call_VM_base()函式
// 恢復bcp與locals
0x00007fffe10175d6: mov -0x38(%rbp),%r13
0x00007fffe10175da: mov -0x30(%rbp),%r14
// 結束呼叫MacroAssembler::call_VM_helper()函式
0x00007fffe10175de: retq
// 結束呼叫MacroAssembler::call_VM()函式
下面詳細解釋如下彙編的意思。
call指令相當於如下兩條指令:
push %eip
jmp addr
而ret指令相當於:
pop %eip
所以如上彙編程式碼:
0x00007fffe1017542: callq 0x00007fffe101754c
0x00007fffe1017547: jmpq 0x00007fffe10175df // 跳轉
...
0x00007fffe10175de: retq
呼叫callq指令將jmpq的地址壓入了表示式棧,也就是壓入了返回地址x00007fffe1017547,這樣當後續呼叫retq時,會跳轉到jmpq指令執行,而jmpq又跳轉到了0x00007fffe10175df地址處的指令執行。
通過呼叫MacroAssembler::call_VM()函式來呼叫HotSpot VM中用的C++編寫的函式,call_VM()函式還會呼叫如下函式:
MacroAssembler::call_VM_helper
InterpreterMacroAssembler::call_VM_base()
MacroAssembler::call_VM_base()
MacroAssembler::call_VM_leaf_base()
在如上幾個函式中,最重要的就是在MacroAssembler::call_VM_base()函式中儲存rsp、rbp的值到JavaThread::last_java_sp與JavaThread::last_java_fp屬性中,然後通過MacroAssembler::call_VM_leaf_base()函式生成的彙編程式碼來呼叫C++編寫的InterpreterRuntime::ldc()函式,如果呼叫InterpreterRuntime::ldc()函式有可能破壞rsp和rbp的值(其它的%r13、%r14等的暫存器中的值也有可能破壞,所以在必要時儲存到棧中,在呼叫完成後再恢復,這樣這些暫存器其實就算的上是呼叫者儲存的暫存器了),所以為了保證rsp、rbp,將這兩個值儲存到執行緒中,線上程中儲存的這2個值對於棧展開非常非常重要,後面我們會詳細介紹。
由於如上彙編程式碼會解釋執行,在解釋執行過程中會呼叫C++函式,所以C/C++棧和Java棧都混在一起,這為我們查詢帶來了一定的複雜度。
呼叫的MacroAssembler::call_VM_leaf_base()函式生成的彙編程式碼如下:
第3部分彙編程式碼:
// 呼叫MacroAssembler::call_VM_leaf_base()函式
0x00007fffe1017566: test $0xf,%esp // 檢查對齊
// %esp對齊的操作,跳轉到 L
0x00007fffe101756c: je 0x00007fffe1017584
// %esp沒有對齊時的操作
0x00007fffe1017572: sub $0x8,%rsp
0x00007fffe1017576: callq 0x00007ffff66a22a2 // 呼叫函式,也就是呼叫InterpreterRuntime::ldc()函式
0x00007fffe101757b: add $0x8,%rsp
0x00007fffe101757f: jmpq 0x00007fffe1017589 // 跳轉到E2
// -- L --
// %esp對齊的操作
0x00007fffe1017584: callq 0x00007ffff66a22a2 // 呼叫函式,也就是呼叫InterpreterRuntime::ldc()函式
// -- E2 --
// 結束呼叫
MacroAssembler::call_VM_leaf_base()函式
在如上這段彙編中會真正呼叫C++函式InterpreterRuntime::ldc(),由於這是一個C++函式,所以在呼叫時,如果要傳遞引數,則要遵守C++呼叫約定,也就是前6個引數都放到固定的暫存器中。這個函式需要2個引數,分別為thread和wide,已經分別放到了%rdi和%rax暫存器中了。InterpreterRuntime::ldc()函式的實現如下:
// ldc負責將數值常量或String常量值從常量池中推送到棧頂
IRT_ENTRY(void, InterpreterRuntime::ldc(JavaThread* thread, bool wide))
ConstantPool* pool = method(thread)->constants();
int index = wide ? get_index_u2(thread, Bytecodes::_ldc_w) : get_index_u1(thread, Bytecodes::_ldc);
constantTag tag = pool->tag_at(index);
Klass* klass = pool->klass_at(index, CHECK);
oop java_class = klass->java_mirror(); // java.lang.Class通過oop來表示
thread->set_vm_result(java_class);
IRT_END
函式將查詢到的、當前正在解釋執行的方法所屬的類儲存到JavaThread類的vm_result屬性中。我們可以回看第2部分彙編程式碼,會將vm_result屬性的值設定到%rax中。
接下來繼續看TemplateTable::ldc(bool wide)函式生成的彙編程式碼,此時已經通過呼叫call_VM()函式生成了呼叫InterpreterRuntime::ldc()這個C++的彙編,呼叫完成後值已經放到了%rax中。
// -- E1 --
0x00007fffe10287ba: push %rax // 將呼叫的結果儲存到表示式中
0x00007fffe10287bb: jmpq 0x00007fffe102885e // 跳轉到Done
// -- notClass --
// $0x4表示JVM_CONSTANT_Float
0x00007fffe10287c0: cmp $0x4,%edx
0x00007fffe10287c3: jne 0x00007fffe10287d9 // 跳到notFloat
// 當ldc位元組碼指令載入的數為float時執行如下彙編程式碼
0x00007fffe10287c5: vmovss 0x58(%rcx,%rbx,8),%xmm0
0x00007fffe10287cb: sub $0x8,%rsp
0x00007fffe10287cf: vmovss %xmm0,(%rsp)
0x00007fffe10287d4: jmpq 0x00007fffe102885e // 跳轉到Done
// -- notFloat --
// 當ldc位元組碼指令載入的為非float,也就是int型別資料時通過push加入表示式棧
0x00007fffe1028859: mov 0x58(%rcx,%rbx,8),%eax
0x00007fffe102885d: push %rax
// -- Done --
由於ldc指令除了載入String外,還可能載入int和float,如果是int,直接呼叫push壓入表示式棧中,如果是float,則在表示式棧上開闢空間,然後移到到這個開闢的slot中儲存。注意,float會使用%xmm0暫存器。
2、fast_aldc虛擬機器內部位元組碼指令
下面介紹_fast_aldc指令,這個指令是虛擬機器內部使用的指令而非虛擬機器規範定義的指令。_fast_aldc指令的模板定義如下:
def(Bytecodes::_fast_aldc , ubcp|____|clvm|____, vtos, atos, fast_aldc , false );
生成函式為TemplateTable::fast_aldc(bool wide),這個函式生成的彙編程式碼如下:
// 呼叫InterpreterMacroAssembler::get_cache_index_at_bcp()函式生成
// 獲取位元組碼指令的運算元,這個運算元已經指向了常量池快取項的索引,在位元組碼重寫
// 階段已經進行了位元組碼重寫
0x00007fffe10243d0: movzbl 0x1(%r13),%edx
// 呼叫InterpreterMacroAssembler::load_resolved_reference_at_index()函式生成
// shl表示邏輯左移,相當於乘4,因為ConstantPoolCacheEntry的大小為4個字
0x00007fffe10243d5: shl $0x2,%edx
// 獲取Method*
0x00007fffe10243d8: mov -0x18(%rbp),%rax
// 獲取ConstMethod*
0x00007fffe10243dc: mov 0x10(%rax),%rax
// 獲取ConstantPool*
0x00007fffe10243e0: mov 0x8(%rax),%rax
// 獲取ConstantPool::_resolved_references屬性的值,這個值
// 是一個指向物件陣列的指標
0x00007fffe10243e4: mov 0x30(%rax),%rax
// JNIHandles::resolve(obj)
0x00007fffe10243e8: mov (%rax),%rax
// 從_resolved_references陣列指定的下標索引處獲取oop,先進行索引偏移
0x00007fffe10243eb: add %rdx,%rax
// 要在%rax上加0x10,是因為陣列物件的頭大小為2個字,加上後
// %rax就指向了oop
0x00007fffe10243ee: mov 0x10(%rax),%eax
獲取_resolved_references屬性的值,涉及到的2個屬性在ConstantPool類中的定義如下:
// Array of resolved objects from the constant pool and map from resolved
// object index to original constant pool index
jobject _resolved_references; // jobject是指標型別
Array<u2>* _reference_map;
關於_resolved_references指向的其實是Object陣列。在ConstantPool::initialize_resolved_references()函式中初始化這個屬性。呼叫鏈如下:
ConstantPool::initialize_resolved_references() constantPool.cpp
Rewriter::make_constant_pool_cache() rewriter.cpp
Rewriter::Rewriter() rewriter.cpp
Rewriter::rewrite() rewriter.cpp
InstanceKlass::rewrite_class() instanceKlass.cpp
InstanceKlass::link_class_impl() instanceKlass.cpp
後續如果需要連線ldc等指令時,可能會呼叫如下函式:(我們只討論ldc載入String型別資料的問題,所以我們只看往_resolved_references屬性中放入表示String的oop的邏輯,MethodType與MethodHandle將不再介紹,有興趣的可自行研究)
oop ConstantPool::string_at_impl(
constantPoolHandle this_oop,
int which,
int obj_index,
TRAPS
) {
oop str = this_oop->resolved_references()->obj_at(obj_index);
if (str != NULL)
return str;
Symbol* sym = this_oop->unresolved_string_at(which);
str = StringTable::intern(sym, CHECK_(NULL));
this_oop->string_at_put(which, obj_index, str);
return str;
}
void string_at_put(int which, int obj_index, oop str) {
// 獲取型別為jobject的_resolved_references屬性的值
objArrayOop tmp = resolved_references();
tmp->obj_at_put(obj_index, str);
}
在如上函式中向_resolved_references陣列中設定快取的值。
大概的思路就是:如果ldc載入的是字串,那麼儘量通過_resolved_references陣列中一次性找到表示字串的oop,否則要通過原常量池下標索引找到Symbol例項(Symbol例項是HotSpot VM內部使用的、用來表示字串),根據Symbol例項生成對應的oop,然後通過常量池快取下標索引設定到_resolved_references中。當下次查詢時,通過這個常量池快取下標快取找到表示字串的oop。
獲取到_resolved_references屬性的值後接著看生成的彙編程式碼,如下:
// ...
// %eax中儲存著表示字串的oop
0x00007fffe1024479: test %eax,%eax
// 如果已經獲取到了oop,則跳轉到resolved
0x00007fffe102447b: jne 0x00007fffe1024481
// 沒有獲取到oop,需要進行連線操作,0xe5是_fast_aldc的Opcode
0x00007fffe1024481: mov $0xe5,%edx
呼叫call_VM()函式生成的彙編程式碼如下:
// 呼叫InterpreterRuntime::resolve_ldc()函式
0x00007fffe1024486: callq 0x00007fffe1024490
0x00007fffe102448b: jmpq 0x00007fffe1024526
// 將%rdx中的ConstantPoolCacheEntry項儲存到第1個引數中
// 呼叫MacroAssembler::call_VM_helper()函式生成
0x00007fffe1024490: mov %rdx,%rsi
// 將返回地址載入到%rax中
0x00007fffe1024493: lea 0x8(%rsp),%rax
// 呼叫call_VM_base()函式生成
// 儲存bcp
0x00007fffe1024498: mov %r13,-0x38(%rbp)
// 呼叫MacroAssembler::call_VM_base()函式生成
// 將r15中的值移動到c_rarg0(rdi)暫存器中,也就是為函式呼叫準備第一個引數
0x00007fffe102449c: mov %r15,%rdi
// Only interpreter should have to set fp 只有直譯器才必須要設定fp
0x00007fffe102449f: mov %rbp,0x200(%r15)
0x00007fffe10244a6: mov %rax,0x1f0(%r15)
// 呼叫MacroAssembler::call_VM_leaf_base()生成
0x00007fffe10244ad: test $0xf,%esp
0x00007fffe10244b3: je 0x00007fffe10244cb
0x00007fffe10244b9: sub $0x8,%rsp
0x00007fffe10244bd: callq 0x00007ffff66b27ac
0x00007fffe10244c2: add $0x8,%rsp
0x00007fffe10244c6: jmpq 0x00007fffe10244d0
0x00007fffe10244cb: callq 0x00007ffff66b27ac
0x00007fffe10244d0: movabs $0x0,%r10
// 結束呼叫MacroAssembler::call_VM_leaf_base()
0x00007fffe10244da: mov %r10,0x1f0(%r15)
0x00007fffe10244e1: movabs $0x0,%r10
// 檢查是否有異常發生
0x00007fffe10244eb: mov %r10,0x200(%r15)
0x00007fffe10244f2: cmpq $0x0,0x8(%r15)
// 如果沒有異常發生,則跳轉到ok
0x00007fffe10244fa: je 0x00007fffe1024505
// 有異常發生,則跳轉到StubRoutines::forward_exception_entry()
0x00007fffe1024500: jmpq 0x00007fffe1000420
// ---- ok ----
// 將JavaThread::vm_result屬性中的值儲存到oop_result暫存器中並清空vm_result屬性的值
0x00007fffe1024505: mov 0x250(%r15),%rax
0x00007fffe102450c: movabs $0x0,%r10
0x00007fffe1024516: mov %r10,0x250(%r15)
// 結果呼叫MacroAssembler::call_VM_base()函式
// 恢復bcp和locals
0x00007fffe102451d: mov -0x38(%rbp),%r13
0x00007fffe1024521: mov -0x30(%rbp),%r14
// 結束呼叫InterpreterMacroAssembler::call_VM_base()函式
// 結束呼叫MacroAssembler::call_VM_helper()函式
0x00007fffe1024525: retq
// 結束呼叫MacroAssembler::call_VM()函式,回到
// TemplateTable::fast_aldc()函式繼續看生成的程式碼,只
// 定義了resolved點
// ---- resolved ----
呼叫的InterpreterRuntime::resolve_ldc()函式的實現如下:
IRT_ENTRY(void, InterpreterRuntime::resolve_ldc(
JavaThread* thread,
Bytecodes::Code bytecode)
) {
ResourceMark rm(thread);
methodHandle m (thread, method(thread));
Bytecode_loadconstant ldc(m, bci(thread));
oop result = ldc.resolve_constant(CHECK);
thread->set_vm_result(result);
}
IRT_END
這個函式會呼叫一系列的函式,相關呼叫鏈如下:
ConstantPool::string_at_put() constantPool.hpp
ConstantPool::string_at_impl() constantPool.cpp
ConstantPool::resolve_constant_at_impl() constantPool.cpp
ConstantPool::resolve_cached_constant_at() constantPool.hpp
Bytecode_loadconstant::resolve_constant() bytecode.cpp
InterpreterRuntime::resolve_ldc() interpreterRuntime.cpp
其中ConstantPool::string_at_impl()函式在前面已經詳細介紹過。
呼叫的resolve_constant()函式的實現如下:
oop Bytecode_loadconstant::resolve_constant(TRAPS) const {
int index = raw_index();
ConstantPool* constants = _method->constants();
if (has_cache_index()) {
return constants->resolve_cached_constant_at(index, THREAD);
} else {
return constants->resolve_constant_at(index, THREAD);
}
}
呼叫的resolve_cached_constant_at()或resolve_constant_at()函式的實現如下:
oop resolve_cached_constant_at(int cache_index, TRAPS) {
constantPoolHandle h_this(THREAD, this);
return resolve_constant_at_impl(h_this, _no_index_sentinel, cache_index, THREAD);
}
oop resolve_possibly_cached_constant_at(int pool_index, TRAPS) {
constantPoolHandle h_this(THREAD, this);
return resolve_constant_at_impl(h_this, pool_index, _possible_index_sentinel, THREAD);
}
呼叫的resolve_constant_at_impl()函式的實現如下:
oop ConstantPool::resolve_constant_at_impl(
constantPoolHandle this_oop,
int index,
int cache_index,
TRAPS
) {
oop result_oop = NULL;
Handle throw_exception;
if (cache_index == _possible_index_sentinel) {
cache_index = this_oop->cp_to_object_index(index);
}
if (cache_index >= 0) {
result_oop = this_oop->resolved_references()->obj_at(cache_index);
if (result_oop != NULL) {
return result_oop;
}
index = this_oop->object_to_cp_index(cache_index);
}
jvalue prim_value; // temp used only in a few cases below
int tag_value = this_oop->tag_at(index).value();
switch (tag_value) {
// ...
case JVM_CONSTANT_String:
assert(cache_index != _no_index_sentinel, "should have been set");
if (this_oop->is_pseudo_string_at(index)) {
result_oop = this_oop->pseudo_string_at(index, cache_index);
break;
}
result_oop = string_at_impl(this_oop, index, cache_index, CHECK_NULL);
break;
// ...
}
if (cache_index >= 0) {
Handle result_handle(THREAD, result_oop);
MonitorLockerEx ml(this_oop->lock());
oop result = this_oop->resolved_references()->obj_at(cache_index);
if (result == NULL) {
this_oop->resolved_references()->obj_at_put(cache_index, result_handle());
return result_handle();
} else {
return result;
}
} else {
return result_oop;
}
}
通過常量池的tags陣列判斷,如果常量池下標index處儲存的是JVM_CONSTANT_String常量池項,則呼叫string_at_impl()函式,這個函式在之前已經介紹過,會根據表示字串的Symbol例項建立出表示字串的oop。在ConstantPool::resolve_constant_at_impl()函式中得到oop後就儲存到ConstantPool::_resolved_references屬性中,最後返回這個oop,這正是ldc需要的oop。
通過重寫fast_aldc位元組碼指令,達到了通過少量指令就直接獲取到oop的目的,而且oop是快取的,所以字串常量在HotSpot VM中的表示唯一,也就是隻有一個oop表示。
C++函式約定返回的值會儲存到%rax中,根據_fast_aldc位元組碼指令的模板定義可知,tos_out為atos,所以後續並不需要進一步操作。
HotSpot VM會在類的連線過程中重寫某些位元組碼,如ldc位元組碼重寫為fast_aldc,還有常量池的tags型別陣列、常量池快取等內容在《深入剖析Java虛擬機器:原始碼剖析與例項詳解》中詳細介紹過,這裡不再介紹。
第21篇-載入與儲存指令之ldc與_fast_aldc指令(3)
iload會將int型別的本地變數推送至棧頂。模板定義如下:
def(Bytecodes::_iload , ubcp|____|clvm|____, vtos, itos, iload , _ );
iload指令的格式如下:
iload index
index是一個無符號byte型別整數,指向區域性變數表的索引值。
生成函式為TemplateTable::iload(),反編譯後的彙編程式碼如下:
// 將%ebx指向下一條位元組碼指令的首地址
0x00007fffe1028d30: movzbl 0x2(%r13),%ebx
// $0x15為_iload指令的操作碼值
0x00007fffe1028d35: cmp $0x15,%ebx
// 當下一條指令為iload時,直接跳轉到done
0x00007fffe1028d38: je 0x00007fffe1028deb // done
// 0xdf為_fast_iload指令的操作碼值
0x00007fffe1028d3e: cmp $0xdf,%ebx
// 將_fast_iload2指令移動到%ecx
0x00007fffe1028d44: mov $0xe0,%ecx
0x00007fffe1028d49: je 0x00007fffe1028d5a // rewrite
// 0x34為_caload指令的操作碼
// _caload指令表示從陣列中載入一個char型別資料到運算元棧
0x00007fffe1028d4b: cmp $0x34,%ebx
// 將_fast_icaload移動到%ecx中
0x00007fffe1028d4e: mov $0xe1,%ecx
0x00007fffe1028d53: je 0x00007fffe1028d5a // rewrite
// 將_fast_iload移動到%ecx中
0x00007fffe1028d55: mov $0xdf,%ecx
// -- rewrite --
// 呼叫patch_bytecode()函式
// 重寫為fast版本,因為%cl中儲存的是位元組碼的fast版本,%ecx的8位叫%cl
0x00007fffe1028de7: mov %cl,0x0(%r13)
// -- done --
// 獲取位元組碼指令的運算元,這個運算元為本地變數表的索引
0x00007fffe1028deb: movzbl 0x1(%r13),%ebx
0x00007fffe1028df0: neg %rbx
// 通過本地變數表索引從本地變數表中載入值到%eax中,
// %eax中儲存的就是棧頂快取值,所以不需要壓入棧內
0x00007fffe1028df3: mov (%r14,%rbx,8),%eax
執行的邏輯如下:
假設現在有個方法的位元組碼指令流為連線3個iload指令,這3個iload指令前後都為非iload指令。那麼重寫的過程如下:
彙編程式碼在第一次執行時,如果判斷最後一個_iload之後是非_iload指令,則會重寫最後一個_iload指令為_fast_iload;第二次執行時,當第2個位元組碼指令為_iload,而之後接著判斷為_fast_iload時,會更新第2個_iload為_fast_iload2。
執行_fast_iload和執行_fast_iload2都可以提高程式執行的效率,_fast_icaload指令也一樣,下面詳細介紹一下這幾個指令。
1、_fast_iload指令
_fast_iload會將int型別的本地變數推送至棧頂。模板定義如下:
def(Bytecodes::_fast_iload , ubcp|____|____|____, vtos, itos, fast_iload , _ );
生成函式為TemplateTable::fast_iload() ,彙編程式碼如下:
0x00007fffe1023f90: movzbl 0x1(%r13),%ebx
0x00007fffe1023f95: neg %rbx
0x00007fffe1023f98: mov (%r14,%rbx,8),%eax
彙編程式碼很簡單,這裡不再過多說。
執行_fast_iload指令與執行_iload指令相比,不用再進行之前彙編介紹的那麼多判斷,也沒有重寫的邏輯,所以會提高執行效率。
2、_fast_iload2指令
_fast_iload2會將int型別的本地變數推送至棧頂。模板定義如下:
def(Bytecodes::_fast_iload2 , ubcp|____|____|____, vtos, itos, fast_iload2 , _ );
生成函式為TemplateTable::fast_iload2() ,彙編程式碼如下:
0x00007fffe1024010: movzbl 0x1(%r13),%ebx
0x00007fffe1024015: neg %rbx
0x00007fffe1024018: mov (%r14,%rbx,8),%eax
0x00007fffe102401c: push %rax
0x00007fffe102401d: movzbl 0x3(%r13),%ebx
0x00007fffe1024022: neg %rbx
0x00007fffe1024025: mov (%r14,%rbx,8),%eax
可以看到,此指令就相當於連續執行了2次iload指令,省去了指令跳轉,所以效率要高一些。
3、_fast_icaload指令
caload指令表示從陣列中載入一個char型別資料到運算元棧。
_fast_icaload會將char型別陣列指定索引的值推送至棧頂。模板定義如下:
def(Bytecodes::_fast_icaload , ubcp|____|____|____, vtos, itos, fast_icaload , _ );
生成函式為TemplateTable::fast_icaload(),生成的彙編程式碼如下:
0x00007fffe1024090: movzbl 0x1(%r13),%ebx
0x00007fffe1024095: neg %rbx
// %eax中儲存著index
0x00007fffe1024098: mov (%r14,%rbx,8),%eax
// %rdx中儲存著arrayref
0x00007fffe102409c: pop %rdx
// 將一個雙字擴充套件後送到一個四字中,%rax中儲存著index
0x00007fffe102409d: movslq %eax,%rax
// %rdx指向陣列物件的首地址,偏移0xc後獲取length屬性的值
0x00007fffe10240a0: cmp 0xc(%rdx),%eax
0x00007fffe10240a3: mov %eax,%ebx
// 如果陣列索引index等於陣列的長度或大於陣列長度時,那麼跳轉
// 到_throw_ArrayIndexOutOfBoundsException_entry丟擲異常
0x00007fffe10240a5: jae 0x00007fffe100ff20
// 在指定陣列arrayref中載入指定索引處index的值
0x00007fffe10240ab: movzwl 0x10(%rdx,%rax,2),%eax
可以看到,此指令省去了指令跳轉,所以效率要高一些。
由於字數限制,《模板直譯器解釋執行Java位元組碼指令(下)》將在下篇中釋出