执行引擎-CallStub栈帧
概述
简单些 中介绍了call_static()、call_virtual()等函数的作用,这些函数会调用JavaCalls::call()函数。我们看Java类中main()方法的调用,调用栈如下:
JavaCalls::call_helper() at javaCalls.cpp
os::os_exception_wrapper() at os_linux.cpp
JavaCalls::call() at javaCalls.cpp
jni_invoke_static() at jni.cpp
jni_CallStaticVoidMethod() at jni.cpp
JavaMain() at java.c
start_thread() at pthread_create.c
clone() at clone.S
这是Linux上的调用栈,通过JavaCalls::call_helper()函数来执行main()方法。栈的起始函数为clone(),这个函数会为每个进程(Linux进程对应着Java线程)创建单独的栈空间,这个栈空间如下图所示。
image.png在Linux操作系统上,栈的地址向低地址延伸,所以未使用的栈空间在已使用的栈空间之下。图中的每个蓝色小格表示对应方法的栈帧,而栈就是由一个一个的栈帧组成。native方法的栈帧、Java解释栈帧和Java编译栈帧都会在黄色区域中分配,所以说他们寄生在宿主栈中,这些不同的栈帧都紧密的挨在一起,所以并不会产生什么空间碎片这类的问题,而且这样的布局非常有利于进行栈的遍历。上面给出的调用栈就是通过遍历一个一个栈帧得到的,遍历过程也是栈展开的过程。后续对于异常的处理、运行jstack打印线程堆栈、GC查找根引用等都会对栈进行展开操作,所以栈展开是后面必须要介绍的。
下面我们继续看JavaCalls::call_helper()函数,这个函数中有个非常重要的调用,如下:
// do call
{
JavaCallWrapper link(method, receiver, result, CHECK);
{
HandleMark hm(thread); // HandleMark used by HandleMarkCleaner
StubRoutines::call_stub()(
(address)&link,
result_val_address,
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
);
result = link.result();
// Preserve oop return value across possible gc points
if (oop_result_flag) {
thread->set_vm_result((oop) result->get_jobject());
}
}
}
调用StubRoutines::call_stub()函数返回一个函数指针,然后通过函数指针来调用函数指针指向的函数。通过函数指针调用和通过函数名调用的方式一样,这里我们需要清楚的是,调用的目标函数仍然是C/C++函数,所以由C/C++函数调用另外一个C/C++函数时,要遵守调用约定。这个调用约定会规定怎么给被调用函数(Callee)传递参数,以及被调用函数的返回值将存储在什么地方。
下面我们就来简单说说Linux X86架构下的C/C++函数调用约定,在这个约定下,以下寄存器用于传递参数:
第1个参数:rdi c_rarg0
第2个参数:rsi c_rarg1
第3个参数:rdx c_rarg2
第4个参数:rcx c_rarg3
第5个参数:r8 c_rarg4
第6个参数:r9 c_rarg5
在函数调用时,6个及小于6个用如下寄存器来传递,在HotSpot中通过更易理解的别名c_rarg*来使用对应的寄存器。如果参数超过六个,那么程序将会用调用栈来传递那些额外的参数。
数一下我们通过函数指针调用时传递了几个参数?8个,那么后面的2个就需要通过调用函数(Caller)的栈来传递,这两个参数就是args->size_of_parameters()和CHECK(这是个宏,扩展后就是传递线程对象)。
所以我们的调用栈在调用函数指针指向的函数时,变为了如下状态:
image.png
右边是具体的call_helper()栈帧中的内容,我们把thread和parameter size压入了调用栈中,其实在调目标函数的过程还会开辟新的栈帧并在parameter size后压入返回地址和调用栈的栈底,下一篇我们再详细介绍。先来介绍下JavaCalls::call_helper()函数的实现,我们分3部分依次介绍。
1、检查目标方法是否"首次执行前就必须被编译”,是的话调用JIT编译器去编译目标方法;**
代码实现如下:
void JavaCalls::call_helper(
JavaValue* result,
methodHandle* m,
JavaCallArguments* args,
TRAPS
) {
methodHandle method = *m;
JavaThread* thread = (JavaThread*)THREAD;
...
assert(!thread->is_Compiler_thread(), "cannot compile from the compiler");
if (CompilationPolicy::must_be_compiled(method)) {
CompileBroker::compile_method(method, InvocationEntryBci,
CompilationPolicy::policy()->initial_compile_level(),
methodHandle(), 0, "must_be_compiled", CHECK);
}
...
}
对于main()方法来说,如果配置了-Xint选项,则是以解释模式执行的,所以并不会走上面的compile_method()函数的逻辑。后续我们要研究编译执行时,可以强制要求进行编译执行,然后查看执行过程。
2、获取目标方法的解释模式入口from_interpreted_entry,也就是entry_point的值。获取的entry_point就是为Java方法调用准备栈桢,并把代码调用指针指向method的第一个字节码的内存地址。entry_point相当于是method的封装,不同的method类型有不同的entry_point。**
接着看call_helper()函数的代码实现,如下:
address entry_point = method->from_interpreted_entry();
调用method的from_interpreted_entry()函数获取Method实例中_from_interpreted_entry属性的值,这个值到底在哪里设置的呢?我们后面会详细介绍。
3、调用call_stub()函数,需要传递8个参数。这个代码在前面给出过,这里不再给出。下面我们详细介绍一下这几个参数,如下:**
(1)link 此变量的类型为JavaCallWrapper,这个变量对于栈展开过程非常重要,后面会详细介绍;
(2)result_val_address 函数返回值地址;
(3)result_type 函数返回类型;
(4)method() 当前要执行的方法。通过此参数可以获取到Java方法所有的元数据信息,包括最重要的字节码信息,这样就可以根据字节码信息解释执行这个方法了;
(5)entry_point HotSpot每次在调用Java函数时,必然会调用CallStub函数指针,这个函数指针的值取自_call_stub_entry,HotSpot通过_call_stub_entry指向被调用函数地址。在调用函数之前,必须要先经过entry_point,HotSpot实际是通过entry_point从method()对象上拿到Java方法对应的第1个字节码命令,这也是整个函数的调用入口;
(6)args->parameters() 描述Java函数的入参信息;
(7)args->size_of_parameters() 参数需要占用的,以字为单位的内存大小
(8)CHECK 当前线程对象。
JavaCalls::call
之前多次提到接触到调用JavaCalls::call()方法来执行Java方法,如:
(1)Java主类装载时,调用JavaCalls::call()方法执行的Java方法checkAndLoadMain()方法
(2)类的初始化过程中,调用JavaCalls::call()方法执行的Java方法<clinit>方法
可以看出,JavaCalls::call()方法为虚拟机调用Java方法提供了便利,Java虚拟机有invokestatic、invokedynamic、invokestatic、invokespecial、invokevirtual几种方法调用指令,每个负责调用不同的方法,而这些方法都定义在JavaCalls类中,如下:
源代码位置:/src/share/vm/runtime/javaCalls.hpp
// All calls to Java have to go via JavaCalls. Sets up the stack frame
// and makes sure that the last_Java_frame pointers are chained correctly.
class JavaCalls: AllStatic {
static void call_helper(JavaValue* result, methodHandle* method, JavaCallArguments* args, TRAPS);
public:
// Optimized Constuctor call
static void call_default_constructor(JavaThread* thread, methodHandle method, Handle receiver, TRAPS);
// call_special
// ------------
// The receiver must be first oop in argument list
// receiver表示方法的接收者,如A.main()调用中,A就是方法的接收者
static void call_special(JavaValue* result, KlassHandle klass, Symbol* name,Symbol* signature, JavaCallArguments* args, TRAPS);
static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, TRAPS); // No args
static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
static void call_special(JavaValue* result, Handle receiver, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS);
// virtual call
// ------------
// The receiver must be first oop in argument list
static void call_virtual(JavaValue* result, KlassHandle spec_klass, Symbol* name,Symbol* signature, JavaCallArguments* args, TRAPS);
static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, TRAPS); // No args
static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
static void call_virtual(JavaValue* result, Handle receiver, KlassHandle spec_klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS);
// Static call
// -----------
static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, JavaCallArguments* args, TRAPS);
static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, TRAPS);
static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, TRAPS);
static void call_static(JavaValue* result, KlassHandle klass,Symbol* name, Symbol* signature, Handle arg1, Handle arg2, TRAPS);
// Low-level interface
static void call(JavaValue* result, methodHandle method, JavaCallArguments* args, TRAPS);
};
上面的方法是自解释的,对应各自的invoke*指令,这些call_static()、call_virtual()函数内部调用了call()函数:
void JavaCalls::call(JavaValue* result, methodHandle method, JavaCallArguments* args, TRAPS) {
// Check if we need to wrap a potential OS exception handler around thread
// This is used for e.g. Win32 structured exception handlers
assert(THREAD->is_Java_thread(), "only JavaThreads can make JavaCalls");
// Need to wrap each and everytime, since there might be native code down the
// stack that has installed its own exception handlers
// 通过传入call_helper函数指针,在call_helper上面封装了异常的处理,典型的回调函数用法
os::os_exception_wrapper(call_helper, result, &method, args, THREAD);
}
call()方法
只是简单检查了一下线程信息,以及根据平台比如windows会使用结构化异常(SEH)包裹call_helper,最终执行方法调用的还是call_helper()方法
。调用链如下:
JavaCalls::call_helper() javaCalls.cpp
os::os_exception_wrapper() os_linux.cpp
JavaCalls::call() javaCalls.cpp
InstanceKlass::call_class_initializer_impl() instanceKlass.cpp
InstanceKlass::call_class_initializer() instanceKlass.cpp
InstanceKlass::initialize_impl() instanceKlass.cpp
InstanceKlass::initialize() instanceKlass.cpp
InstanceKlass::initialize_impl() instanceKlass.cpp
InstanceKlass::initialize() instanceKlass.cpp
initialize_class() thread.cpp
Threads::create_vm() thread.cpp
JNI_CreateJavaVM() jni.cpp
InitializeJVM() java.c
JavaMain() java.c
JavaCalls::helper()函数的实现如下:
void JavaCalls::call_helper( JavaValue* result, methodHandle* m, JavaCallArguments* args, TRAPS )
{
methodHandle method = *m;
JavaThread * thread = (JavaThread *) THREAD;
assert( thread->is_Java_thread(), "must be called by a java thread" );
assert( method.not_null(), "must have a method to call" );
assert( !SafepointSynchronize::is_at_safepoint(), "call to Java code during VM operation" );
assert( !thread->handle_area()->no_handle_mark_active(), "cannot call out to Java here" );
/* Ignore call if method is empty */
if ( method->is_empty_method() )
{
assert( result->get_type() == T_VOID, "an empty method must return a void value" );
return;
}
assert( !thread->is_Compiler_thread(), "cannot compile from the compiler" );
if ( CompilationPolicy::must_be_compiled( method ) )
{
CompileBroker::compile_method( method, InvocationEntryBci,
CompilationPolicy::policy()->initial_compile_level(),
methodHandle(), 0, "must_be_compiled", CHECK );
}
/*
* 获取的entry_point就是为Java方法调用准备栈桢,并把代码调用指针指向method的第一个字节码的内存地址。
* entry_point相当于是method的封装,不同的method类型有不同的entry_point。
* Since the call stub sets up like the interpreter we call the from_interpreted_entry
* so we can go compiled via a i2c. Otherwise initial entry method will always
* run interpreted.
*/
address entry_point = method->from_interpreted_entry();
if ( JvmtiExport::can_post_interpreter_events() && thread->is_interp_only_mode() )
{
entry_point = method->interpreter_entry();
}
/*
* Figure out if the result value is an oop or not (Note: This is a different value
* than result_type. result_type will be T_INT of oops. (it is about size)
*/
BasicType result_type = runtime_type_from( result );
bool oop_result_flag = (result->get_type() == T_OBJECT || result->get_type() == T_ARRAY);
/*
* NOTE: if we move the computation of the result_val_address inside
* the call to call_stub, the optimizer produces wrong code.
*/
intptr_t* result_val_address = (intptr_t *) (result->get_value_addr() );
/* Find receiver */
Handle receiver = (!method->is_static() ) ? args->receiver() : Handle();
/*
* When we reenter Java, we need to reenable the yellow zone which
* might already be disabled when we are in VM.
*/
if ( thread->stack_yellow_zone_disabled() )
{
thread->reguard_stack();
}
/*
* Check that there are shadow pages available before changing thread state
* to Java
*/
if ( !os::stack_shadow_pages_available( THREAD, method ) )
{
/* Throw stack overflow exception with preinitialized exception. */
Exceptions::throw_stack_overflow_exception( THREAD, __FILE__, __LINE__, method );
return;
} else {
/* Touch pages checked if the OS needs them to be touched to be mapped. */
os::bang_stack_shadow_pages();
}
/* do call */
{
JavaCallWrapper link( method, receiver, result, CHECK );
{
HandleMark hm( thread ); /* HandleMark used by HandleMarkCleaner */
StubRoutines::call_stub() (
(address) & link,
/* (intptr_t*)&(result->_value), // see NOTE above (compiler problem) */
result_val_address, /* see NOTE above (compiler problem) */
result_type,
method(),
entry_point,
args->parameters(),
args->size_of_parameters(),
CHECK
);
result = link.result(); /* circumvent MS C++ 5.0 compiler bug (result is clobbered across call) */
/* Preserve oop return value across possible gc points */
if ( oop_result_flag )
{
thread->set_vm_result( (oop) result->get_jobject() );
}
}
} /* Exit JavaCallWrapper (can block - potential return oop must be preserved) */
/*
* Check if a thread stop or suspend should be executed
* The following assert was not realistic. Thread.stop can set that bit at any moment.
* assert(!thread->has_special_runtime_exit_condition(), "no async. exceptions should be installed");
* Restore possible oop return
*/
if ( oop_result_flag )
{
result->set_jobject( (jobject) thread->vm_result() );
thread->set_vm_result( NULL );
}
}
我们需要关注此函数做的如下几件事:
1、检查目标方法是否“首次执行前就必须被编译”,是的话调用JIT编译器去编译目标方法
2、获取目标方法的解释模式入口from_interpreted_entry,也就是entry_point的值。获取的entry_point就是为Java方法调用准备栈桢,并把代码调用指针指向method的第一个字节码的内存地址。entry_point相当于是method的封装,不同的method类型有不同的entry_point
3、
调用call_stub()函数。call_helper又可以分为两步,第一步判断一下方法是否为空,是否可以JIT编译,是否还有栈空间等,第二步StubRoutines::call_stub()实际调用os+cpu限定的方法。
调用CallStub函数的是/src/share/vm/runtime/javaCalls.cpp文件中的call_helper()函数,调用CallStub函数指针所指的函数时,需要传递8个参数,如下:
(1)link 此变量的类型为JavaCallWrapper,这个变量需要保存的信息很重要,后面将详细介绍。
(2)result_val_address 函数返回值地址。
(3)result_type 函数返回类型。
(4)method() 当前要执行的方法。通过此参数可以获取到Java方法所有的元数据信息,包括最重要的字节码信息,这样就可以根据字节码信息解释执行这个方法了。
(5)entry_point HotSpot每次在调用Java函数时,必然会调用CallStub函数指针,这个函数指针的值为_call_stub_entry,HotSpot通过_call_stub_entry指向被调用函数地址,最终调用函数。在调用函数之前,必须要先经过entry_point,HotSpot实际是通过entry_point从method()对象上拿到Java方法对应的第1个字节码命令,这也是整个函数的调用入口。
(6)args->parameters() 描述Java函数的入参信息。
(7)args->size_of_parameters() 描述Java函数的入参数量。
(8)CHECK 当前线程对象。
来源:/src/share/vm/runtime/stubRoutines.hpp
static CallStub call_stub() {
return CAST_TO_FN_PTR(CallStub, _call_stub_entry);
}
call_stub()函数返回一个函数指针,指向依赖于操作系统和cpu架构
的特定的方法,原因很简单,要执行native代码,得看看是什么cpu架构以便确定寄存器,看看什么os以便确定ABI。
其中CAST_TO_FN_PTR是宏,具体定义如下:
源代码位置:/src/share/vm/runtime/utilities/globalDefinitions.hpp
#define CAST_TO_FN_PTR(func_type, value) ((func_type)(castable_address(value)))
对call_stub()函数进行宏替换和展开后会变为如下的形式:
static CallStub call_stub(){
return (CallStub)( castable_address(_call_stub_entry) );
}
CallStub的定义如下:
源代码位置:/src/share/vm/runtime/stubRoutines.hpp
/* Calls to Java */
typedef void (*CallStub)(
address link, /* 连接器 */
intptr_t* result, /* 函数返回值地址 */
BasicType result_type, /* 函数返回类型 */
Method* method, /* JVM内部所表示的Java方法对象 */
/*
* JVM调用Java方法的例程入口。JVM内部的每一段例程都是在JVM启动过程中预先生成好的一段机器指令。要调用Java方法,
* 必须经过本例程,即需要先执行这段机器指令,然后才能跳转到Java方法字节码所对应的机器指令去执行
*/
address entry_point,
intptr_t* parameters,
int size_of_parameters,
TRAPS
);
如上定义了一种函数指针类型,指向的函数声明了8个形式参数。
在call_stub()函数中调用的castable_address()函数定义在globalDefinitions.hpp文件中,具体实现如下:
inline address_word castable_address(address x) {
return address_word(x) ;
}
address_word是一定自定义的类型,在globalDefinitions.hpp文件中的定义如下:
// unsigned integer which will hold a pointer
// except for some implementations of a C++
// linkage pointer to function. Should never
// need one of those to be placed in this type anyway.
typedef uintptr_t address_word;
其中uintptr_t也是一种自定义的类型,在Linux内核的操作系统下使用globalDefinitions_gcc.hpp文件中的定义,具体定义如下:
typedef unsigned int uintptr_t;
这样call_stub()函数其实等同于如下的实现形式:
static CallStub call_stub(){
return (CallStub)( unsigned int(_call_stub_entry) );
}
将_call_stub_entry强制转换为unsigned int类型,然后以强制转换为CallStub类型。CallStub是一个函数指针,所以_call_stub_entry应该也是一个函数指针,而不应该是一个普通的无符号整数。
在call_stub()函数中,_call_stub_entry的定义如下:
address StubRoutines::_call_stub_entry = NULL;
_call_stub_entry的初始化在在/src/cpu/x86/vm/stubGenerator_x86_64.cpp文件下的generate_initial()函数,调用链如下:
StubGenerator::generate_initial() stubGenerator_x86_64.cpp
StubGenerator::StubGenerator() stubGenerator_x86_64.cpp
StubGenerator_generate() stubGenerator_x86_64.cpp
StubRoutines::initialize1() stubRoutines.cpp
stubRoutines_init1() stubRoutines.cpp
init_globals() init.cpp
Threads::create_vm() thread.cpp
JNI_CreateJavaVM() jni.cpp
InitializeJVM() java.c
JavaMain() java.c</pre>
其中的StubGenerator类定义在src/cpu/x86/vm目录下的stubGenerator_x86_64.cpp文件中,这个文件中的generate_initial()方法会初始化call_stub_entry变量,如下:
StubRoutines::_call_stub_entry = generate_call_stub(StubRoutines::_call_stub_return_address);
调用的generate_call_stub()方法的实现如下:
address generate_call_stub( address & return_address )
{
assert( (int) frame::entry_frame_after_call_words == -(int) rsp_after_call_off + 1 &&
(int) frame::entry_frame_call_wrapper_offset == (int) call_wrapper_off,
"adjust this code" );
StubCodeMark mark( this, "StubRoutines", "call_stub" );
address start = __ pc();
/* same as in generate_catch_exception()! */
const Address rsp_after_call( rbp, rsp_after_call_off * wordSize );
const Address call_wrapper( rbp, call_wrapper_off * wordSize );
const Address result( rbp, result_off * wordSize );
const Address result_type( rbp, result_type_off * wordSize );
const Address method( rbp, method_off * wordSize );
const Address entry_point( rbp, entry_point_off * wordSize );
const Address parameters( rbp, parameters_off * wordSize );
const Address parameter_size( rbp, parameter_size_off * wordSize );
/* same as in generate_catch_exception()! */
const Address thread( rbp, thread_off * wordSize );
const Address r15_save( rbp, r15_off * wordSize );
const Address r14_save( rbp, r14_off * wordSize );
const Address r13_save( rbp, r13_off * wordSize );
const Address r12_save( rbp, r12_off * wordSize );
const Address rbx_save( rbp, rbx_off * wordSize );
/* stub code */
__ enter();
__ subptr( rsp, -rsp_after_call_off * wordSize );
/* save register parameters */
__ movptr( parameters, c_rarg5 ); /* parameters */
__ movptr( entry_point, c_rarg4 ); /* entry_point */
__ movptr( method, c_rarg3 ); /* method */
__ movl( result_type, c_rarg2 ); /* result type */
__ movptr( result, c_rarg1 ); /* result */
__ movptr( call_wrapper, c_rarg0 ); /* call wrapper */
/* save regs belonging to calling function */
__ movptr( rbx_save, rbx );
__ movptr( r12_save, r12 );
__ movptr( r13_save, r13 );
__ movptr( r14_save, r14 );
__ movptr( r15_save, r15 );
const Address mxcsr_save( rbp, mxcsr_off * wordSize );
{
Label skip_ldmx;
__ stmxcsr( mxcsr_save );
__ movl( rax, mxcsr_save );
__ andl( rax, MXCSR_MASK ); /* Only check control and mask bits */
ExternalAddress mxcsr_std( StubRoutines::addr_mxcsr_std() );
__ cmp32( rax, mxcsr_std );
__ jcc( Assembler::equal, skip_ldmx );
__ ldmxcsr( mxcsr_std );
__ bind( skip_ldmx );
}
/* Load up thread register */
__ movptr( r15_thread, thread );
__ reinit_heapbase();
/* pass parameters if any */
BLOCK_COMMENT( "pass parameters if any" );
Label parameters_done;
__ movl( c_rarg3, parameter_size );
__ testl( c_rarg3, c_rarg3 );
__ jcc( Assembler::zero, parameters_done );
Label loop;
__ movptr( c_rarg2, parameters ); /* parameter pointer */
__ movl( c_rarg1, c_rarg3 ); /* parameter counter is in c_rarg1 */
__ BIND( loop );
__ movptr( rax, Address( c_rarg2, 0 ) ); /* get parameter */
__ addptr( c_rarg2, wordSize ); /* advance to next parameter */
__ decrementl( c_rarg1 ); /* decrement counter */
__ push( rax ); /* pass parameter */
__ jcc( Assembler::notZero, loop );
/* call Java function */
__ BIND( parameters_done );
__ movptr( rbx, method ); /* get Method* */
__ movptr( c_rarg1, entry_point ); /* get entry_point */
__ mov( r13, rsp ); /* set sender sp */
BLOCK_COMMENT( "call Java function" );
__ call( c_rarg1 );
BLOCK_COMMENT( "call_stub_return_address:" );
return_address = __ pc();
/*
* store result depending on type (everything that is not
* T_OBJECT, T_LONG, T_FLOAT or T_DOUBLE is treated as T_INT)
*/
__ movptr( c_rarg0, result );
Label is_long, is_float, is_double, exit;
__ movl( c_rarg1, result_type );
__ cmpl( c_rarg1, T_OBJECT );
__ jcc( Assembler::equal, is_long );
__ cmpl( c_rarg1, T_LONG );
__ jcc( Assembler::equal, is_long );
__ cmpl( c_rarg1, T_FLOAT );
__ jcc( Assembler::equal, is_float );
__ cmpl( c_rarg1, T_DOUBLE );
__ jcc( Assembler::equal, is_double );
/* handle T_INT case */
__ movl( Address( c_rarg0, 0 ), rax );
__ BIND( exit );
/* pop parameters */
__ lea( rsp, rsp_after_call );
__ movptr( r15, r15_save );
__ movptr( r14, r14_save );
__ movptr( r13, r13_save );
__ movptr( r12, r12_save );
__ movptr( rbx, rbx_save );
__ ldmxcsr( mxcsr_save );
/* restore rsp */
__ addptr( rsp, -rsp_after_call_off * wordSize );
/* return */
__ pop( rbp );
__ ret( 0 );
/* handle return types different from T_INT */
__ BIND( is_long );
__ movq( Address( c_rarg0, 0 ), rax );
__ jmp( exit );
__ BIND( is_float );
__ movflt( Address( c_rarg0, 0 ), xmm0 );
__ jmp( exit );
__ BIND( is_double );
__ movdbl( Address( c_rarg0, 0 ), xmm0 );
__ jmp( exit );
return(start);
}
这个函数实现的逻辑有点多,而且最终会生成一段机器码,由于机器码很难读懂,所以我们可以通过方法的源代码和汇编代码来解读。
首先简单介绍一下address和Address类型。
address是u_char*类型的别名,定义如下:
源代码位置:globalDefinitions.hpp
typedef u_char* address;
Address类的定义如下:
源代码位置:/x86/vm/assembler_x86.hpp
// Address is an abstraction used to represent a memory location
// using any of the amd64 addressing modes with one object.
//
// Note: A register location is represented via a Register, not
// via an address for efficiency & simplicity reasons.
class Address VALUE_OBJ_CLASS_SPEC {
...
}
如果要看generate_call_stub()方法生成的汇编,可以在导入hsdis-amd64.so的情况下,输入如下命令:
-XX:+PrintStubCode -XX:+UnlockDiagnosticVMOptions com.test/CompilationDemo1
首先看generate_call_stub()方法如下两句代码:
// stub code
__ enter();
__ subptr(rsp, -rsp_after_call_off * wordSize);
调用macroAssembler_x86.cpp文件中的enter()方法,用来保存调用方栈基址,并将call_stub栈基址更新为当前栈顶地址。
实现如下:
void MacroAssembler::enter() {
push(rbp);
mov(rbp, rsp);
}
调用的push()方法如下:
void Assembler::push(Register src) {
int encode = prefix_and_encode(src->encoding());
emit_int8(0x50 | encode);
}
Assembler中定义的一些方法通常难以读懂,这是因为需要我们知道x86体系下机器码,并且要对Opcode编码规则有所掌握,这一部分后面会详细介绍,这里暂时不介绍,有兴趣的可以自行学习Intel开发者手册,里面对Intel cpu指令集有详细介绍。我们这里只简单认识一下生成机器码的相关方法即可。
调用的src->encoding()返回自身,而prefix_and_encode()方法的实现如下:
int Assembler::prefix_and_encode( int reg_enc, bool byteinst )
{
if ( reg_enc >= 8 )
{
prefix( REX_B );
reg_enc -= 8;
} else if ( byteinst && reg_enc >= 4 )
{
prefix( REX );
}
return(reg_enc);
}
enter()方法中调用的mov()方法的实现如下:
void Assembler::mov( Register dst, Register src )
{
LP64_ONLY( movq( dst, src ) ) NOT_LP64( movl( dst, src ) );
}
对于64位来说,调用movq()方法,如下:
void Assembler::movq( Register dst, Register src )
{
int encode = prefixq_and_encode( dst->encoding(), src->encoding() );
emit_int8( (unsigned char) 0x8B );
emit_int8( (unsigned char) (0xC0 | encode) );
}
调用prefixq_and_encode()方法的实现如下:
int Assembler::prefixq_and_encode( int dst_enc, int src_enc )
{
if ( dst_enc < 8 )
{
if ( src_enc < 8 )
{
prefix( REX_W );
} else {
prefix( REX_WB );
src_enc -= 8;
}
} else {
if ( src_enc < 8 )
{
prefix( REX_WR );
} else {
prefix( REX_WRB );
src_enc -= 8;
}
dst_enc -= 8;
}
return(dst_enc << 3 | src_enc);
}
dst_enc的值为5,src_enc的值为4。
generate_call_stub()方法中调用的subptr()方法的实现如下:
void MacroAssembler::subptr( Register dst, int32_t imm32 ) { LP64_ONLY( subq( dst, imm32 ) ) NOT_LP64( subl( dst, imm32 ) ); }
调用的 subq()方法的实现如下:
void Assembler::subq( Register dst, int32_t imm32 )
{
(void) prefixq_and_encode( dst->encoding() );
emit_arith( 0x81, 0xE8, dst, imm32 );
}
调用的prefixq_and_encode()方法的实现如下:
int Assembler::prefixq_and_encode( int reg_enc )
{
if ( reg_enc < 8 )
{
prefix( REX_W );
} else {
prefix( REX_WB );
reg_enc -= 8;
}
return(reg_enc);
}
subq()方法中调用的emit_arith()方法的实现如下:
void Assembler::emit_arith( int op1, int op2, Register dst, int32_t imm32 )
{
assert( isByte( op1 ) && isByte( op2 ), "wrong opcode" );
assert( (op1 & 0x01) == 1, "should be 32bit operation" );
assert( (op1 & 0x02) == 0, "sign-extension bit should not be set" );
if ( is8bit( imm32 ) )
{
emit_int8( op1 | 0x02 ); /* set sign bit */
emit_int8( op2 | encode( dst ) );
emit_int8( imm32 & 0xFF );
} else {
emit_int8( op1 );
emit_int8( op2 | encode( dst ) );
emit_int32( imm32 );
}
}
使用参数命令:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintStubCode
可以输出generate_call_stub方法生成的汇编,生成的汇编代码如下:
StubRoutines::call_stub [0x00007fdf4500071f, 0x00007fdf45000807[ (232 bytes)
0x00007fdf4500071f: push %rbp
0x00007fdf45000720: mov %rsp,%rbp
0x00007fdf45000723: sub $0x60,%rsp // 0x60 = -rsp_after_call_of * wordSize
如上汇编第1个为源操作数,第2个为目地操作数。如上3句汇编通常是开辟一个新栈固定的格式。
继续看generate_call_stub()方法的实现,如下:
// save register parameters
__ movptr(parameters, c_rarg5); // parameters
__ movptr(entry_point, c_rarg4); // entry_point
__ movptr(method, c_rarg3); // method
__ movl(result_type, c_rarg2); // result type
__ movptr(result, c_rarg1); // result
__ movptr(call_wrapper, c_rarg0); // call wrapper
// save regs belonging to calling function
__ movptr(rbx_save, rbx);
__ movptr(r12_save, r12);
__ movptr(r13_save, r13);
__ movptr(r14_save, r14);
__ movptr(r15_save, r15);
const Address mxcsr_save( rbp, mxcsr_off * wordSize );
{
Label skip_ldmx;
__ stmxcsr( mxcsr_save );
__ movl( rax, mxcsr_save );
__ andl( rax, MXCSR_MASK ); /* Only check control and mask bits */
ExternalAddress mxcsr_std( StubRoutines::addr_mxcsr_std() );
__ cmp32( rax, mxcsr_std );
__ jcc( Assembler::equal, skip_ldmx );
__ ldmxcsr( mxcsr_std );
__ bind( skip_ldmx );
}
生成的汇编代码如下:
0x00007fdf45000727: mov %r9,-0x8(%rbp)
0x00007fdf4500072b: mov %r8,-0x10(%rbp)
0x00007fdf4500072f: mov %rcx,-0x18(%rbp)
0x00007fdf45000733: mov %edx,-0x20(%rbp)
0x00007fdf45000736: mov %rsi,-0x28(%rbp)
0x00007fdf4500073a: mov %rdi,-0x30(%rbp)
0x00007fdf4500073e: mov %rbx,-0x38(%rbp)
0x00007fdf45000742: mov %r12,-0x40(%rbp)
0x00007fdf45000746: mov %r13,-0x48(%rbp)
0x00007fdf4500074a: mov %r14,-0x50(%rbp)
0x00007fdf4500074e: mov %r15,-0x58(%rbp)
// stmxcsr是将MXCSR寄存器中的值保存到-0x60(%rbp)中
0x00007fdf45000752: stmxcsr -0x60(%rbp)
0x00007fdf45000756: mov -0x60(%rbp),%eax
0x00007fdf45000759: and $0xffc0,%eax
// cmp通过第2个操作数减去第1个操作数的差,根据结果来设置eflags中的标志位。
// 本质上和sub指令相同,但是不会改变操作数的值
0x00007fdf4500075f: cmp 0x1762cb5f(%rip),%eax # 0x00007fdf5c62d2c4
// 当ZF=1时跳转到目标地址
0x00007fdf45000765: je 0x00007fdf45000772
// 将m32加载到MXCSR寄存器中
0x00007fdf4500076b: ldmxcsr 0x1762cb52(%rip) # 0x00007fdf5c62d2c4
MXCSR状态管理指令,ldmxcsr与stmxcsr,用于控制MXCSR寄存器(表示SSE指令的运算状态的寄存器)状态。ldmxcsr指令从存储器中加载MXCSR寄存器状态;stmxcsr指令将MXCSR寄存器状态保存到存储器中。
最终的栈帧状态如下图所示。
image.png由于call_helper()函数在调用CallStub()函数时,传递的参数多于6个,所以最后2个参数size_of_parameters与TRAPS(当前线程)要通过call_helper()的栈帧来传递,剩下的可以根据调用约定通过寄存器来传递。
可以看到在传递参数时会遵守调用约定,当x64中函数调用时,以下寄存器用于参数:
- 第1个参数:rdi c_rarg0
- 第2个参数:rsi c_rarg1
- 第3个参数:rdx c_rarg2
- 第4个参数:rcx c_rarg3
- 第5个参数:r8 c_rarg4
- 第6个参数:r9 c_rarg5
在函数调用时,6个及小于6个用如下寄存器来传递,在HotSpot中通过更易理解的别名c_rarg*来使用对应的寄存器。如果参数超过六个,那么程序调用栈就会被用来传递那些额外的参数。
继续看generate_call_stub()方法的实现,接来下会加载线程寄存器,代码如下:
// Load up thread register
__ movptr(r15_thread, thread);
__ reinit_heapbase();</pre>
生成的汇编代码如下:
0x00007fdf45000772: mov 0x18(%rbp),%r15
0x00007fdf45000776: mov 0x1764212b(%rip),%r12 # 0x00007fdf5c6428a
如果在调用函数时有参数的话需要传递参数,代码如下:
// pass parameters if any
Label parameters_done;
__ movl(c_rarg3, parameter_size);
__ testl(c_rarg3, c_rarg3); // 两操作数做与运算,仅修改标志位,不回送结果
__ jcc(Assembler::zero, parameters_done);
Label loop;
__ movptr(c_rarg2, parameters); // parameter pointer
__ movl(c_rarg1, c_rarg3); // parameter counter is in c_rarg1
__ BIND(loop);
__ movptr(rax, Address(c_rarg2, 0));// get parameter
__ addptr(c_rarg2, wordSize); // advance to next parameter
__ decrementl(c_rarg1); // decrement counter
__ push(rax); // pass parameter
__ jcc(Assembler::notZero, loop);</pre>
这里是个循环,用于传递参数,相当于如下代码:
while(%esi){
rax = *arg
push_arg(rax)
arg++; // ptr++
%esi--; // counter--
}
生成的汇编代码如下:
0x00007fdf4500077d: mov 0x10(%rbp),%ecx // 将栈中parameter size送到%ecx中
0x00007fdf45000780: test %ecx,%ecx // 做与运算,只有当%ecx中的值为0时才等于0
0x00007fdf45000782: je 0x00007fdf4500079a // 没有参数需要传递,直接跳转到parameters_done即可
// -- loop --
// 汇编执行到这里,说明paramter size不为0,需要传递参数
0x00007fdf45000788: mov -0x8(%rbp),%rdx
0x00007fdf4500078c: mov %ecx,%esi
0x00007fdf4500078e: mov (%rdx),%rax
0x00007fdf45000791: add $0x8,%rdx
0x00007fdf45000795: dec %esi
0x00007fdf45000797: push %rax
0x00007fdf45000798: jne 0x00007fdf4500078e // 跳转到loop
因为要调用Java方法,所以会为Java方法压入实际的参数,也就是压入parameter size个从parameters开始取的参数。压入参数后的栈如下图所示。
image.png调用Java函数,如下:
// call Java function
// -- parameters_done --
__ BIND(parameters_done);
__ movptr(rbx, method); // get Method*
__ movptr(c_rarg1, entry_point); // get entry_point
__ mov(r13, rsp); // set sender sp
__ call(c_rarg1); // 调用Java方法
生成的汇编代码如下:
0x00007fdf4500079a: mov -0x18(%rbp),%rbx // 将Method*送到%rbx中
0x00007fdf4500079e: mov -0x10(%rbp),%rsi // 将entry_point送到%rsi中
0x00007fdf450007a2: mov %rsp,%r13 // 将调用者的栈顶指针保存到%r13中
0x00007fdf450007a5: callq *%rsi // 调用Java方法</pre>
注意调用callq指令后,会将callq指令的下一条指令的地址压栈,再跳转到第1操作数指定的地址,也就是*%rsi表示的地址。压入下一条指令的地址是为了让函数能通过跳转到栈上的地址从子函数返回。
callq指令调用的是entry point。entry point在后面会详细介绍。
接下来在generate_call_stub()方法中会处理调用Java方法后的返回值与返回类型,而且还需要执行退栈操作,也就是将栈恢复到调用Java方法之前的状态。代码实现如下:
// store result depending on type (everything that is not
// T_OBJECT, T_LONG, T_FLOAT or T_DOUBLE is treated as T_INT)
// 保存方法调用结果依赖于结果类型,只要不是T_OBJECT, T_LONG, T_FLOAT or T_DOUBLE,都当做T_INT处理
// 将result地址的值拷贝到c_rarg0中,也就是将方法调用的结果保存在rdi寄存器中,注意result为函数返回值的地址
__ movptr(c_rarg0, result);
Label is_long, is_float, is_double, exit;
// 将result_type地址的值拷贝到c_rarg1中,也就是将方法调用的结果返回的类型保存在esi寄存器中
__ movl(c_rarg1, result_type);
// 根据结果类型的不同跳转到不同的处理分支
__ cmpl(c_rarg1, T_OBJECT);
__ jcc(Assembler::equal, is_long);
__ cmpl(c_rarg1, T_LONG);
__ jcc(Assembler::equal, is_long);
__ cmpl(c_rarg1, T_FLOAT);
__ jcc(Assembler::equal, is_float);
__ cmpl(c_rarg1, T_DOUBLE);
__ jcc(Assembler::equal, is_double);
// handle T_INT case
// 当执行到这里时,处理的就是T_INT类型,将rax中的值写入c_rarg0保存的地址指向的内存中
__ movl(Address(c_rarg0, 0), rax); // 调用函数后返回值根据调用约定会存储在eax中
__ BIND(exit);
// pop parameters
// 将rsp_after_call中保存的有效地址拷贝到rsp中,即将rsp往高地址方向移动了,
// 原来的方法调用参数argument1、...、argumentn相当于从栈中弹出
__ lea(rsp, rsp_after_call); // lea指令将地址加载到寄存器中
生成的汇编代码如下:
0x00007fdf450007a7: mov -0x28(%rbp),%rdi // 栈中的-0x28位置保存result
0x00007fdf450007ab: mov -0x20(%rbp),%esi // 栈中的-0x20位置保存result type
0x00007fdf450007ae: cmp $0xc,%esi // 是否为T_OBJECT类型
0x00007fdf450007b1: je 0x00007fdf450007f6
0x00007fdf450007b7: cmp $0xb,%esi // 是否为T_LONG类型
0x00007fdf450007ba: je 0x00007fdf450007f6
0x00007fdf450007c0: cmp $0x6,%esi // 是否为T_FLOAT类型
0x00007fdf450007c3: je 0x00007fdf450007fb
0x00007fdf450007c9: cmp $0x7,%esi // 是否为T_DOUBLE类型
0x00007fdf450007cc: je 0x00007fdf45000801
0x00007fdf450007d2: mov %eax,(%rdi) // 如果是T_INT类型,直接将返回结果%eax写到栈中-0x28的位置
// -- exit --
0x00007fdf450007d4: lea -0x60(%rbp),%rsp // 将rsp_after_call的有效地址拷到rsp中
恢复之前保存的caller-save寄存器:
// restore regs belonging to calling function
__ movptr(r15, r15_save);
__ movptr(r14, r14_save);
__ movptr(r13, r13_save);
__ movptr(r12, r12_save);
__ movptr(rbx, rbx_save);
__ ldmxcsr(mxcsr_save); </pre>
生成的汇编代码如下:
0x00007fdf450007d8: mov -0x58(%rbp),%r15
0x00007fdf450007dc: mov -0x50(%rbp),%r14
0x00007fdf450007e0: mov -0x48(%rbp),%r13
0x00007fdf450007e4: mov -0x40(%rbp),%r12
0x00007fdf450007e8: mov -0x38(%rbp),%rbx
0x00007fdf450007ec: ldmxcsr -0x60(%rbp)
在弹出了为调用Java方法保存的调用参数及恢复caller-save寄存器后,继续执行退栈操作,实现如下:
// restore rsp
__ addptr(rsp, -rsp_after_call_off * wordSize);
// return
__ pop(rbp);
__ ret(0);
// handle return types different from T_INT
__ BIND(is_long);
__ movq(Address(c_rarg0, 0), rax);
__ jmp(exit);
__ BIND(is_float);
__ movflt(Address(c_rarg0, 0), xmm0);
__ jmp(exit);
__ BIND(is_double);
__ movdbl(Address(c_rarg0, 0), xmm0);
__ jmp(exit); </pre>
生成的汇编代码如下:
// %rsp加上0x60,也就是执行退栈操作,也就相当于弹出了callee_save寄存器和压栈的那6个参数
0x00007fdf450007f0: add $0x60,%rsp
0x00007fdf450007f4: pop %rbp
0x00007fdf450007f5: retq // 方法返回,指令中的q表示64位操作数,就是指的栈中存储的return address是64位的
// -- is_long --
0x00007fdf450007f6: mov %rax,(%rdi)
0x00007fdf450007f9: jmp 0x00007fdf450007d4
// -- is_float --
0x00007fdf450007fb: vmovss %xmm0,(%rdi)
0x00007fdf450007ff: jmp 0x00007fdf450007d4
// -- is_double --
0x00007fdf45000801: vmovsd %xmm0,(%rdi)
0x00007fdf45000805: jmp 0x00007fdf450007d4
在执行完add指令后的栈状态如下图所示。
image.png然后恢复%rsp的值后,调用retq使用return address返回调用call_helper()函数的那个调用函数,至于paramter size与thread则由调用函数负责释放。