1. 程式人生 > >HotSpot的執行引擎-CallStub棧幀

HotSpot的執行引擎-CallStub棧幀

之前多次提到接觸到呼叫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

其中的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暫存器狀態儲存到儲存器中。

最終的棧幀狀態如下圖所示。

 

由於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();

生成的彙編程式碼如下:

0x00007fdf45000772: mov    0x18(%rbp),%r15
0x00007fdf45000776: mov    0x1764212b(%rip),%r12   # 0x00007fdf5c6428a8

如果在呼叫函式時有引數的話需要傳遞引數,程式碼如下:

// 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);

這裡是個迴圈,用於傳遞引數,相當於如下程式碼:

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開始取的引數。壓入引數後的棧如下圖所示。

呼叫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方法

注意呼叫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); 

生成的彙編程式碼如下:

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); 

生成的彙編程式碼如下:

// %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指令後的棧狀態如下圖所示。  

 

然後恢復%rsp的值後,呼叫retq使用return address返回呼叫call_helper()函式的那個呼叫函式,至於paramter size與thread則由呼叫函式負責釋放。 

相關文章的連結如下:

1、在Ubuntu 16.04上編譯OpenJDK8的原始碼 

2、除錯HotSpot原始碼

3、HotSpot專案結構 

4、HotSpot的啟動過程 

5、HotSpot二分模型(1)

6、HotSpot的類模型(2)  

7、HotSpot的類模型(3) 

8、HotSpot的類模型(4)

9、HotSpot的物件模型(5)  

10、HotSpot的物件模型(6) 

11、操作控制代碼Handle(7)

12、控制代碼Handle的釋放(8)

13、類載入器 

14、類的雙親委派機制 

15、核心類的預裝載

16、Java主類的裝載  

17、觸發類的裝載  

18、類檔案介紹 

19、檔案流 

20、解析Class檔案 

21、常量池解析(1) 

22、常量池解析(2)

23、欄位解析(1)

24、欄位解析之偽共享(2) 

25、欄位解析(3)  

26、欄位解析之OopMapBlock(4)

27、方法解析之Method與ConstMethod介紹  

28、方法解析

29、klassVtable與klassItable類的介紹  

30、計算vtable的大小 

31、計算itable的大小 

32、解析Class檔案之建立InstanceKlass物件 

33、欄位解析之欄位注入 

34、類的連線  

35、類的連線之驗證 

36、類的連線之重寫(1) 

37、類的連線之重寫(2)

38、方法的連線  

39、初始化vtable 

40、初始化itable  

41、類的初始化 

42、物件的建立  

43、Java引用型別 

44、Java引用型別之軟引用(1)

45、Java引用型別之軟引用(2)

46、Java引用型別之弱引用與幻像引用  

47、Java引用型別之最終引用

48、HotSpot的垃圾回收演算法  

49、HotSpot的垃圾回收器   

作者持續維護的個人部落格  classloading.com。

關注公眾號,有HotSpot原始碼剖析系列文章!

   

參考文章:

(1)JVM系列之 _call_stub_entry初始化

(2)[Inside HotSpot] Java的方法呼叫

 

  

&n