美文网首页
generate_fixed_frame() 与dispatch

generate_fixed_frame() 与dispatch

作者: 程序员札记 | 来源:发表于2022-06-20 08:41 被阅读0次

    在从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: &quot;Courier New&quot; !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方法的字节码,其实就是根据字节码找到对应的入口地址来执行,而入口地址就是机器码的入口地址,这个机器码就是根据对应的字节码翻译过来的,这些都会在后面详细介绍

    相关文章

      网友评论

          本文标题:generate_fixed_frame() 与dispatch

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