美文网首页
执行引擎-entry point栈帧

执行引擎-entry point栈帧

作者: 程序员札记 | 来源:发表于2022-06-19 10:17 被阅读0次

接着上一篇去讲,回到JavaCalls::call_helper()中:

address entry_point = method->from_interpreted_entry();

entry_point是从当前要执行的Java方法中获取的,定义如下:

源代码位置:/openjdk/hotspot/src/share/vm/oops/method.hpp

volatile address from_interpreted_entry() const
{
    return( (address) OrderAccess::load_ptr_acquire( &_from_interpreted_entry ) );
}

那么_from_interpreted_entry是何时赋值的?之前在介绍方法连接时简单介绍过,在method.hpp中有这样一个set方法:

void set_interpreter_entry( address entry )
{
    _i2i_entry      = entry;
    _from_interpreted_entry = entry;
}

在连接方法时通过如下的方法调用上面的方法:

/*
 * Called when the method_holder is getting linked. Setup entrypoints so the method
 * is ready to be called from interpreter, compiler, and vtables.
 */
void Method::link_method( methodHandle h_method, TRAPS )
{
/* ... */
    address entry = Interpreter::entry_for_method( h_method );
    assert( entry != NULL, "interpreter entry must be non-null" );
/* Sets both _i2i_entry and _from_interpreted_entry */
    set_interpreter_entry( entry );
/* ... */
}

根据注释都可以得知,当方法连接时,会去设置方法的entry_point,entry_point是通过调用Interpreter::entry_for_method()方法得到,这个方法的实现如下:

static address entry_for_method( methodHandle m )
{
    return(entry_for_kind( method_kind( m ) ) );
}

首先通过method_kind()拿到方法类型,然后调用entry_for_kind()方法根据方法类型获取方法入口entry point。调用的entry_for_kind()方法如下:

static address entry_for_kind( MethodKind k )
{
    return(_entry_table[k]);
}

这里直接返回了_entry_table数组中对应方法类型的entry_point地址。给数组中元素赋值专门有个方法:

void AbstractInterpreter::set_entry_for_kind( AbstractInterpreter::MethodKind kind, address entry )
{
    _entry_table[kind] = entry;
}

那么何时会调用set_entry_for_kind ()呢,答案就在TemplateInterpreterGenerator::generate_all()中,generate_all()会调用generate_method_entry()去生成每种方法的entry_point,所有Java方法的执行,都会通过对应类型的entry_point例程来辅助。下面来详细介绍一下generate_all()方法的实现逻辑。

HotSpot在启动时,会为所有字节码创建在特定目标平台上运行的机器码,并存放在CodeCache中,在解释执行字节码的过程中,就会从CodeCache中取出这些本地机器码并执行。

在启动虚拟机阶段会调用init_globals()方法初始化全局模块,在这个方法中通过调用interpreter_init()方法初始化模板解释器,调用栈如下:

TemplateInterpreter::initialize() templateInterpreter.cpp
interpreter_init() interpreter.cpp
init_globals() init.cpp
Threads::create_vm() thread.cpp
JNI_CreateJavaVM() jni.cpp
InitializeJVM() java.c
JavaMain() java.c
start_thread() pthread_create.c

interpreter_init()方法主要是通过调用TemplateInterpreter::initialize()方法来完成逻辑,initialize()方法的实现如下:

源代码位置:/src/share/vm/interpreter/templateInterpreter.cpp

void TemplateInterpreter::initialize()
{
    if ( _code != NULL )
        return;
/*
 * 抽象解释器AbstractInterpreter的初始化,AbstractInterpreter是基于汇编模型的解释器的共同基类,
 * 定义了解释器和解释器生成器的抽象接口
 */
    AbstractInterpreter::initialize();
/* 模板表TemplateTable的初始化,模板表TemplateTable保存了各个字节码的模板 */
    TemplateTable::initialize();
/* generate interpreter */
    {
        ResourceMark    rm;
        int     code_size = InterpreterCodeSize;
/* CodeCache的Stub队列StubQueue的初始化 */
        _code = new StubQueue( new InterpreterCodeletInterface, code_size, NULL, "Interpreter" );
/* 实例化模板解释器生成器对象TemplateInterpreterGenerator */
        InterpreterGenerator g( _code );
    }
/* initialize dispatch table */
    _active_table = _normal_table;
}

模板解释器的初始化包括如下几个方面:

(1)抽象解释器AbstractInterpreter的初始化,AbstractInterpreter是基于汇编模型的解释器的共同基类,定义了解释器和解释器生成器的抽象接口。

(2)模板表TemplateTable的初始化,模板表TemplateTable保存了各个字节码的模板(目标代码生成函数和参数);

(3)CodeCache的Stub队列StubQueue的初始化;

(4)解释器生成器InterpreterGenerator的初始化。

在执行InterpreterGenerator g(_code)代码时,调用InterpreterGenerator的构造函数,如下:

InterpreterGenerator::InterpreterGenerator( StubQueue* code ) : TemplateInterpreterGenerator( code )
{
    generate_all(); /* down here so it can be "virtual" */
}

调用的generate_all()方法将生成一系列HotSpot运行过程中所执行的一些公共代码的入口和所有字节码的InterpreterCodelet。这些入口包括:

  • error exits:出错退出处理入口
  • 字节码追踪入口(配置了-XX:+TraceBytecodes)
  • 函数返回入口
  • JVMTI的EarlyReturn入口
  • 逆优化调用返回入口
  • native调用返回值处理handlers入口
  • continuation入口
  • safepoint入口
  • 异常处理入口
  • 抛出异常入口
  • 方法入口(native方法和非native方法)
  • 字节码入口

部分重要的入口实现逻辑会在后面详细介绍,这里只看为非native方法入口(也就是普通的、没有native关键字修饰的Java方法)生成入口的逻辑。generate_all()方法中有如下调用语句:

#define method_entry( kind ) 
{
     CodeletMark cm( _masm, "method entry point (kind = " # kind ")" );
     Interpreter::_entry_table[Interpreter::kind] = generate_method_entry( Interpreter::kind );
}
method_entry( zerolocals )

其中method_entry是宏,扩展后如上的调用语句变为如下的形式:

Interpreter::_entry_table[Interpreter::zerolocals] = generate_method_entry(Interpreter::zerolocals);

_entry_table变量定义在AbstractInterpreter类中,如下:

// method entry points
static address _entry_table[number_of_method_entries]; // entry points for a given method

number_of_method_entries表示方法类型的总数,使用方法类型做为数组下标就可以获取对应的方法入口。调用generate_method_entry()方法为各个类型的方法生成对应的方法入口,实现如下:

address AbstractInterpreterGenerator::generate_method_entry( AbstractInterpreter::MethodKind kind )
{
/* determine code generation flags */
    bool            synchronized    = false;
    address         entry_point = NULL;
    InterpreterGenerator    * ig_this   = (InterpreterGenerator *) this;
    switch ( kind )                                 /* 根据方法类型kind生成不同的入口 */
    {
    case Interpreter::zerolocals:                   /* zerolocals表示普通方法类型 */
        break;
    case Interpreter::zerolocals_synchronized:      /* zerolocals表示普通的、同步方法类型 */
        synchronized = true;
        break;
/* ... */
    }
    if ( entry_point )
    {
        return(entry_point);
    }
    return(ig_this->generate_normal_entry( synchronized ) );
}

zerolocals表示正常的Java方法调用(包括Java程序的主函数),对于zerolocals来说,会调用ig_this->generate_normal_entry()方法生成入口。generate_normal_entry()方法会为执行的方法生成堆栈,而堆栈由局部变量表(用来存储传入的参数和被调用函数的局部变量)、帧数据和操作数栈这三大部分组成,所以方法会创建这3部分来辅助Java方法的执行。

之前在介绍CallStub栈帧时讲到过,如果要执行entry_point,那么栈帧的状态就如下图所示。

image.png

/src/cpu/x86/vm/templateInterpreter_x86_64.cpp文件中generate_normal_entry()方法在通过CallStub调用时,各个寄存器的状态如下:

rbx -> Method*
r13 -> sender sp
rsi -> entry point 

generate_normal_entry()方法的实现如下:

/* Generic interpreted method entry to (asm) interpreter */
address InterpreterGenerator::generate_normal_entry( bool synchronized )
{
/* determine code generation flags */
    bool inc_counter = UseCompiler || CountCompiledCalls;
/*
 * 执行如下方法前的寄存器中保存的值如下:
 * ebx: Method*
 * r13: sender sp
 */
    address entry_point = __ pc(); /* entry_point函数的代码入口地址 */
/* 当前rbx中存储的是指向Method的指针,通过Method*找到ConstMethod* */
    const Address constMethod( rbx, Method::const_offset() );
/* 通过Method*找到AccessFlags */
    const Address access_flags( rbx, Method::access_flags_offset() );
/* 通过ConstMethod*得到parameter的大小 */
    const Address size_of_parameters( rdx, ConstMethod::size_of_parameters_offset() );
/* 通过ConstMethod*得到local变量的大小 */
    const Address size_of_locals( rdx, ConstMethod::size_of_locals_offset() );
/*
 * 上面已经说明了获取各种方法元数据的计算方式,但并没有执行计算,下面会生成对应的汇编来执行计算
 * get parameter size (always needed)
 */
    __  movptr( rdx, constMethod );                     /* 计算ConstMethod*,保存在rdx里面 */
    __  load_unsigned_short( rcx, size_of_parameters ); /* 计算parameter大小,保存在rcx里面 */
/*
 * rbx:保存基址;rcx:保存循环变量;rdx:保存目标地址;rax:保存返回地址(下面用到)
 * 此时的各个寄存器中的值如下:
 * rbx: Method*
 * rcx: size of parameters
 * r13: sender_sp (could differ from sp+wordSize if we were called via c2i ) 即调用者的栈顶地址
 * 计算local变量的大小,保存到rdx
 */
    __ load_unsigned_short( rdx, size_of_locals );
/* 由于局部变量表用来存储传入的参数和被调用函数的局部变量,所以rdx减去rcx后就是被调用函数的局部变量可使用的大小 */
    __ subl( rdx, rcx );
/* see if we've got enough room on the stack for locals plus overhead. */
    generate_stack_overflow_check();
/*
 * 返回地址是在call_stub中保存的,如果不弹出堆栈到rax,那么局部变量区就如下面的样子:
 * [parameter 1]
 * [parameter 2]
 * ......
 * [parameter n]
 * [return address]
 * [local 1]
 * [local 2]
 * ...
 * [local n]
 * 显然中间有个return address使的局部变量表不是连续的,这会导致其中的局部变量计算方式不一致,所以暂时将返回地址存储到rax中
 * get return address
 */
    __ pop( rax );
/*
 * compute beginning of parameters (r14)
 * 计算第1个参数的地址:当前栈顶地址 + 变量大小 * 8 - 一个字大小。
 * 这儿注意,因为地址保存在低地址上,而堆栈是向低地址扩展的,所以只需加n-1个变量大小就可以得到第1个参数的地址。
 */
    __ lea( r14, Address( rsp, rcx, Address::times_8, -wordSize ) );
/*
 * 把函数的局部变量全置为0,也就是做初始化,防止之前遗留下的值影响
 * rdx:被调用函数的局部变量可使用的大小
 * allocate space for locals
 * explicitly initialize locals
 */
    {
        Label   exit, loop;
        __  testl( rdx, rdx );
        __  jcc( Assembler::lessEqual, exit );      /* do nothing if rdx <= 0 */
        __  bind( loop );
        __  push( (int) NULL_WORD );                /* initialize local variables */
        __  decrementl( rdx );                      /* until everything initialized */
        __  jcc( Assembler::greater, loop );
        __  bind( exit );
    }
/*
 * 生成固定桢
 * initialize fixed part of activation frame
 */
    generate_fixed_frame( false );
/*
 * 省略统计及栈溢出等逻辑,后面会详细介绍
 * check for synchronized methods
 * Must happen AFTER invocation_counter check and stack overflow check,
 * so method is not locked if overflows.
 */
    if ( synchronized )
    {
/* Allocate monitor and lock method */
        lock_method();
    } else {
/* no synchronization necessary */
    }
/* 省略统计相关逻辑,后面会详细介绍 */
    return(entry_point);
}

要对偏移的计算进行研究,如下:

// 当前rbx中存储的是指向Method的指针,通过Method*找到ConstMethod*
const Address constMethod(rbx, Method::const_offset());
// 通过Method*找到AccessFlags
const Address access_flags(rbx, Method::access_flags_offset());
// 通过ConstMethod*得到parameter的大小
const Address size_of_parameters(rdx,ConstMethod::size_of_parameters_offset());
// 通过ConstMethod*得到local变量的大小
const Address size_of_locals(rdx, ConstMethod::size_of_locals_offset());

如果要打印这个方法生成的汇编代码,可以在方法的return语句之前添加如下2句打印代码:

address end = __ pc();
Disassembler::decode(entry_point, end);

这样,在执行Disassembler::decode()方法时,会将此方法生成的机器码转换为汇编打印到控制台上。

调用generate_fixed_frame()方法之前生成的汇编代码如下:

Loaded disassembler from /home/mazhi/workspace/openjdk/build/linux-x86_64-normal-server-slowdebug/jdk/lib/amd64/server/hsdis-amd64.so

[Disassembling for mach='i386:x86-64']
0x00007fffe101e2e0: mov 0x10(%rbx),%rdx // 通过%rbx中保存的Method*找到ConstMethod并保存到%rdx
0x00007fffe101e2e4: movzwl 0x2a(%rdx),%ecx // 通过ConstMethod*找到入参数量保存在%ecx
0x00007fffe101e2e8: movzwl 0x28(%rdx),%edx // 通过ConstMethod*找到本地变量表大小保存在%edx
0x00007fffe101e2ec: sub %ecx,%edx // 计算方法局部变量可使用的本地变量空间的大小并保存在%edx
// ... 省略调用generate_stack_overflow_check()方法生成的汇编
0x00007fffe101e43d: pop %rax // 弹出返回地址
0x00007fffe101e43e: lea -0x8(%rsp,%rcx,8),%r14 // 计算第一个参数的地址
// 为局部变量slot(不包括方法入参)分配堆栈空间并初始化为0
// 循环进行本地变量表空间的开辟
// -- loop --
0x00007fffe101e443: test %edx,%edx
0x00007fffe101e445: jle 0x00007fffe101e454 // 由于%edx的大小等于0,所以不需要额外分配,直接跳转到exit
0x00007fffe101e44b: pushq $0x0
0x00007fffe101e450: dec %edx
0x00007fffe101e452: jg 0x00007fffe101e44b // 如果%edx的大小不等于0,跳转到loop

现在栈的状态如下图所示。

image.png

现在r14指向局部变量开始的位置,而argument和local variable都存储在了局部变量表,rbp指向了局部变量表结束位置。现在各个寄存器的状态如下:

rax: return address // %rax寄存器中存储的是返回地址return address
rbx: Method*
r14: pointer to locals
r13: sender sp

在InterpreterGenerator::generate_normal_entry()函数中,接下来会以这样的状态调用generate_fixed_frame()函数来创建Java方法运行时所需要的栈帧。generate_fixed_frame()函数会在下一篇详细介绍。

调用后栈帧变为如下的状态:


image.png

上图右边的栈状态随着具体方法的不同会显示不同的状态,不过大概的状态就是上图所示的样子。

调用完generate_fixed_frame()方法后一些寄存器中保存的值如下:

rbx:Method*
ecx:invocation counter
r13:bcp(byte code pointer)
rdx:ConstantPool* 常量池的地址
r14:本地变量表第1个参数的地址

执行完generate_fixed_frame()方法后会继续执行InterpreterGenerator::generate_normal_entry()函数,如果是为同步方法生成机器码,那么还需要调用lock_method()方法,这个方法会改变当前栈的状态,添加同步所需要的一些信息,在后面介绍锁的实现时会详细介绍。

InterpreterGenerator::generate_normal_entry()函数最终会返回生成机器码的入口执行地址,然后通过变量_entry_table数组来保存,这样就可以使用方法类型做为数组下标获取对应的方法入口了。

相关文章

网友评论

      本文标题:执行引擎-entry point栈帧

      本文链接:https://www.haomeiwen.com/subject/fiqrvrtx.html