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等字節碼指令的匯編代碼邏輯也類似,這里不過多介紹。
推薦閱讀:
第2篇-JVM虛擬機這樣來調用Java主類的main()方法
第13篇-通過InterpreterCodelet存儲機器指令片段
如果有問題可直接評論留言或加作者微信mazhimazh
關注公眾號,有HotSpot VM源碼剖析系列文章!