第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

double0推送至棧頂

0x0f

dconst_1

double1推送至棧頂

0x10

bipush

將單字節的常量值-128~127推送至棧頂

0x11

sipush

將一個短整型常量值-32768~32767推送至棧頂

0x12

ldc

intfloatString型常量值從常量池中推送至棧頂

0x13

ldc_w

int,floatString型常量值從常量池中推送至棧頂(寬索引

0x14

ldc2_w

longdouble型常量值從常量池中推送至棧頂寬索引

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等字節碼指令的匯編代碼邏輯也類似,這里不過多介紹。

推薦閱讀:

第1篇-關於JVM運行時,開篇說的簡單些

第2篇-JVM虛擬機這樣來調用Java主類的main()方法

第3篇-CallStub新棧幀的創建

第4篇-JVM終於開始調用Java主類的main()方法啦

第5篇-調用Java方法后彈出棧幀及處理返回結果

第6篇-Java方法新棧幀的創建

第7篇-為Java方法創建棧幀

第8篇-dispatch_next()函數分派字節碼

第9篇-字節碼指令的定義

第10篇-初始化模板表

第11篇-認識Stub與StubQueue

第12篇-認識CodeletMark

第13篇-通過InterpreterCodelet存儲機器指令片段

第14篇-生成重要的例程

第15章-解釋器及解釋器生成器

第16章-虛擬機中的匯編器

第17章-x86-64寄存器

第18章-x86指令集之常用指令

如果有問題可直接評論留言或加作者微信mazhimazh

關注公眾號,有HotSpot VM源碼剖析系列文章!

 

 

 

 

 

 

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM