在从generate_normal_entry()函数调用generate_fixed_frame()函数时的栈与寄存器的状态如下:
栈的状态如下图所示。
image.png各个寄存器的状态如下所示。
rax: return address // %rax寄存器中存储的是返回地址r
rbx: Method* // 要执行的Java方法的指针
r14: pointer to locals // 本地变量表指针
r13: sender sp // 调用者的栈顶
调用的generate_fixed_frame()方法的实现如下:
源代码位置:src/cpu/x86/vm/templateInterpreter_x86_64.cpp
// Generate a fixed interpreter frame. This is identical setup for
// interpreted methods and for native methods hence the shared code.
void TemplateInterpreterGenerator::generate_fixed_frame(bool native_call) {
// initialize fixed part of activation frame
__ push(rax); // save return address 把返回地址紧接着局部变量区保存
__ enter(); // save old & set new rbp 进入固定桢
__ push(r13); // set sender sp 保存调用者的栈顶地址
__ push((int)NULL_WORD); // leave last_sp as null
__ movptr(r13, Address(rbx, Method::const_offset())); // 获取ConstMethod*并保存到r13中
__ lea(r13, Address(r13, ConstMethod::codes_offset())); // 保存字节码的地址到r13中
__ push(rbx); // 保存Method*的地址到堆栈上
// ProfileInterpreter的默认值为true,表示需要对方法进行信息编译
if (ProfileInterpreter) {
Label method_data_continue;
// MethodData结构基础是ProfileData,记录函数运行状态下的数据
// MethodData里面分为3个部分,一个是函数类型等运行相关统计数据,一个是参数类型运行相关统计数据,
// 还有一个是extra扩展区保存着deoptimization的相关信息
// 获取Method中的_method_data属性的值并保存到rdx中
__ movptr(rdx, Address(rbx, in_bytes(Method::method_data_offset())));
__ testptr(rdx, rdx);
__ jcc(Assembler::zero, method_data_continue);
// 执行到这里,说明_method_data已经进行了初始化,通过MethodData来获取_data属性的值并存储到rdx中
__ addptr(rdx, in_bytes(MethodData::data_offset()));
__ bind(method_data_continue);
__ push(rdx); // set the mdp (method data pointer)
} else {
__ push(0);
}
__ movptr(rdx, Address(rbx, Method::const_offset())); // 获取ConstMethod*存储到rdx
__ movptr(rdx, Address(rdx, ConstMethod::constants_offset())); // 获取ConstantPool*存储到rdx
// 获取ConstantPoolCache*并存储到rdx
__ movptr(rdx, Address(rdx, ConstantPool::cache_offset_in_bytes()));
__ push(rdx); // 保存ConstantPoolCache*到堆栈上
__ push(r14); // 保存第1个参数的地址到堆栈上
if (native_call) {
__ push(0); // no bcp
} else {
__ push(r13); // 保存Java方法字节码地址到堆栈上,注意上面对r13寄存器的值进行了更改
}
__ push(0); // reserve word for pointer to expression stack bottom
__ movptr(Address(rsp, 0), rsp); // set expression stack bottom //在rsp的地址保存rsp的值
}
生成的汇编代码如下:
<pre class="brush:csharp;gutter:false;" style="overflow: auto; font-family: "Courier New" !important; font-size: 12px !important; box-sizing: border-box; margin: 5px 0px; width: 890px; white-space: pre; border-radius: 3px; background-color: rgb(245, 245, 245); border: 1px solid rgb(204, 204, 204); padding: 5px; color: rgb(0, 0, 0);">0x00007fffed01b254: push %rax
0x00007fffed01b255: push %rbp
0x00007fffed01b256: mov %rsp,%rbp
0x00007fffed01b259: push %r13
0x00007fffed01b25b: pushq $0x0
0x00007fffed01b260: mov 0x10(%rbx),%r13
0x00007fffed01b264: lea 0x30(%r13),%r13 // lea指令获取内存地址本身
0x00007fffed01b268: push %rbx
0x00007fffed01b269: mov 0x18(%rbx),%rdx
0x00007fffed01b26d: test %rdx,%rdx
0x00007fffed01b270: je 0x00007fffed01b27d
0x00007fffed01b276: add $0x90,%rdx
0x00007fffed01b27d: push %rdx
0x00007fffed01b27e: mov 0x10(%rbx),%rdx
0x00007fffed01b282: mov 0x8(%rdx),%rdx
0x00007fffed01b286: mov 0x18(%rdx),%rdx
0x00007fffed01b28a: push %rdx
0x00007fffed01b28b: push %r14
0x00007fffed01b28d: push %r13
0x00007fffed01b28f: pushq $0x0
0x00007fffed01b294: mov %rsp,(%rsp)
通过源代码结合汇编的方式可以将代码的逻辑看的清清楚楚,最终的栈状态变为了如下的样子:
image.png左边的栈底与右边的栈顶是连续的,右边的栈中除local variable1、...、local variable n之外都是调用generate_fixed_frame()生成的。而argument word 1、...、argument word n加上右边的栈布局就是调用Java方法的栈布局,可以看到,2个栈帧重用了argument word 1、...、argument word n,而浅灰色与深灰色加起来就是为调用Java方法分配的本地变量表。
下面重新总结一下,如下所示。
image.png image.png注意rax中保存的值是返回到generate_call_stub()通过__ call(c_rarg1) 来调用generate_normal_entry()函数的返回地址,也就是会继续执行__ call(c_rarg1)下面的代码。
调用完generate_fixed_frame()方法后一些寄存器中保存的值如下:
rbx:Method*
ecx:invocation counter
r13:bcp(byte code pointer)
rdx:ConstantPool* 常量池的地址
r14:本地变量表第1个参数的地址</pre>
回顾
image.png局部变量存放引用,操作数栈来运算
void TemplateInterpreterGenerator::generate_fixed_frame(bool native_call) {
// initialize fixed part of activation frame
__ push(rax); // save return address
__ enter(); // save old & set new rbp,
__ push(rsi); // set sender sp
__ push((int32_t)NULL_WORD); // leave last_sp as null
__ movptr(rsi, Address(rbx,Method::const_offset())); // get ConstMethod*
__ lea(rsi, Address(rsi,ConstMethod::codes_offset())); // get codebase
__ push(rbx); // save Method*
if (ProfileInterpreter) {
Label method_data_continue;
__ movptr(rdx, Address(rbx, in_bytes(Method::method_data_offset())));
__ testptr(rdx, rdx);
__ jcc(Assembler::zero, method_data_continue);
__ addptr(rdx, in_bytes(MethodData::data_offset()));
__ bind(method_data_continue);
__ push(rdx); // set the mdp (method data pointer)
} else {
__ push(0);
}
__ movptr(rdx, Address(rbx, Method::const_offset()));
__ movptr(rdx, Address(rdx, ConstMethod::constants_offset()));
__ movptr(rdx, Address(rdx, ConstantPool::cache_offset_in_bytes()));
__ push(rdx); // set constant pool cache
__ push(rdi); // set locals pointer
if (native_call) {
__ push(0); // no bcp
} else {
__ push(rsi); // set bcp
}
__ push(0); // reserve word for pointer to expression stack bottom
__ movptr(Address(rsp, 0), rsp); // set expression stack bottom
}
先将return address取出再放入
image.png开辟新的栈帧,三部分:局部变量表,帧数据,操作数栈
调整参数位置,改变偏移量
计算java方法第一个字节码位置
___ movptr(rsi, Address(rbx,methodOopDesc::const_offset()));
___ lea(rsi, Address(rsi, constMethodOopDesc::codes_offset()));
将methodOop压栈
___ push(rbx);
再继续压几个表,将第一条字节码指令压栈
JVM可以加载java类,也可以加载c/c++等程序库,因此jvm通过native_call判断
if (native_call){
___ push(0);
} else {
___ push(rsi);
}
image.png
还要压一个操作数栈
局部变量区通常用一个一个slot来索引变量,一个slot能够容纳一个int,reference等类型
- 32位,一个slot4字节
- 64位,一个slot8字节
- 64位,long在局部变量表中占据16字节(两个slot)
变量这一块与C/C++编译后结果几乎完全相同
字节码的执行
在generate_normal_entry()函数中会调用generate_fixed_frame()函数为Java方法的执行生成对应的栈帧,接下来还会调用dispatch_next()函数执行Java方法的字节码。generate_normal_entry()函数中调用的dispatch_next()函数的实现如下:
// 从generate_fixed_frame()函数生成固定桢的时候,如果当前是第一次调用,
// 那么r13指向的是字节码的首地址,即第一个字节码,而step为0。
void InterpreterMacroAssembler::dispatch_next(TosState state, int step)
{
// load next bytecode (load before advancing r13 to prevent AGI)
load_unsigned_byte(rbx, Address(r13, step));
// 在当前字节码的位置,指针向前移动step宽度,获取地址上的值,这个值即为字节码在转发表中的index,
// 存储到rbx。step的值由字节码指令和操作数决定。转发表中的index其实就是字节码(范围1~202),
// 参考void DispatchTable::set_entry(int i, EntryPoint& entry) 方法。
// advance r13
increment(r13, step);//自增r13供下一次dispatch使用
// 返回当前栈顶状态的所有字节码入口点
dispatch_base(state, Interpreter::dispatch_table(state)); // Interpreter::dispatch_table(state)
}
r13指向字节码的首地址,当第1次调用时,参数step的值为0,那么load_unsigned_byte()方法从r13指向的内存中取一个字节的值,取出来的是字节码指令的操作码。增加r13的步长,这样下次执行时就会取出来下一个字节码指令的操作码。
调用的dispatch_table()函数的实现如下:
static address* dispatch_table(TosState state) {
return _active_table.table_for(state);
}
在_active_table中获取对应栈顶缓存状态的入口地址,_active_table变量定义在TemplateInterpreter类中,如下:
static DispatchTable _active_table; // the active dispatch table (used by the interpreter for dispatch)
DispatchTable类及table_for()等方法的定义如下:
DispatchTable TemplateInterpreter::_active_table;
class DispatchTable VALUE_OBJ_CLASS_SPEC {
public:
/* an entry point for each byte value (also for undefined bytecodes) */
enum { length = 1 << BitsPerByte }; /* BitsPerByte的值为8 */
private:
/* dispatch tables, indexed by tosca and bytecode */
address _table[number_of_states][length]; /* number_of_states=9,length=256 */
public:
/*
* Attributes
* ...
*/
address* table_for( TosState state )
{
return(_table[state]);
}
address* table_for()
{
return(table_for( (TosState) 0 ) );
}
/* ... */
};
address为u_char*类型的别名。_table是一个二维数组的表,维度为栈顶状态(共有9种)和字节码(最多有256个),存储的是每个栈顶状态对应的字节码的入口点。这里由于还没有介绍栈顶缓存,所以可能理解起来并不容易,不过后面传经详细介绍,等介绍完了再看这部分逻辑就比较容易理解了。
InterpreterMacroAssembler::dispatch_next()函数中调用的dispatch_base()
方法的实现如下:
void InterpreterMacroAssembler::dispatch_base( TosState state, /* 表示栈顶缓存状态 */
address* table,
bool verifyoop )
{
/*
* ...
* 获取当前栈顶状态字节码转发表的地址,保存到rscratch1
*/
lea( rscratch1, ExternalAddress( (address) table ) );
/*
* 跳转到字节码对应的入口执行机器码指令
* address = rscratch1 + rbx * 8
*/
jmp( Address( rscratch1, rbx, Address::times_8 ) );
}
比如取一个字节的指令,那么InterpreterMacroAssembler::dispatch_next()函数生成的汇编代码如下 :
// 在generate_fixed_frame()方法中已经让%r13存储了bcp
0x00007fffe1010643: movzbl 0x0(%r13),%ebx // %ebx中存储的是字节码的操作码
// $0x7ffff73ba4a0这个地址指向的是对应state状态下的一维数组,长度为256
0x00007fffe1010648: movabs $0x7ffff73ba4a0,%r10
// 注意%r10中存储的是常量,根据计算公式%r10+%rbx*8来获取指向存储入口地址的地址,
// 通过*(%r10+%rbx*8)获取到入口地址,然后跳转到入口地址执行
0x00007fffe1010652: jmpq *(%r10,%rbx,8)
%r10指向的是对应栈顶缓存状态state下的一维数组,长度为256,其中存储的值为opcode,如下图所示。
image.png下面的方法显示了对每个字节码的每个栈顶状态都设置入口地址。
void DispatchTable::set_entry(int i, EntryPoint& entry)
{
assert(0 <= i && i < length, "index out of bounds");
assert(number_of_states == 9, "check the code below");
_table[btos][i] = entry.entry(btos);
_table[ctos][i] = entry.entry(ctos);
_table[stos][i] = entry.entry(stos);
_table[atos][i] = entry.entry(atos);
_table[itos][i] = entry.entry(itos);
_table[ltos][i] = entry.entry(ltos);
_table[ftos][i] = entry.entry(ftos);
_table[dtos][i] = entry.entry(dtos);
_table[vtos][i] = entry.entry(vtos);
}
其中的参数i就是opcode,各个字节码及对应的opcode可参考https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-7.html
调用dispatch_next()函数执行Java方法的字节码,其实就是根据字节码找到对应的入口地址来执行,而入口地址就是机器码的入口地址,这个机器码就是根据对应的字节码翻译过来的,这些都会在后面详细介绍
网友评论