专注虚拟机与编译器研究

第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源码剖析系列文章!

 

 

 

 

 

 

  

 

posted on 2021-09-09 10:07  鸠摩(马智)  阅读(781)  评论(0编辑  收藏  举报

导航