第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等字节码指令的汇编代码逻辑也类似,这里不过多介绍。
推荐阅读:
第2篇-JVM虚拟机这样来调用Java主类的main()方法
第13篇-通过InterpreterCodelet存储机器指令片段
如果有问题可直接评论留言或加作者微信mazhimazh
关注公众号,有HotSpot VM源码剖析系列文章!