JVM方法调用——native方法到java方法

native方法到java方法

最为经典的一个JNI调用Java方法就是调用Main函数,下面顺便会介绍java的启动过程。

java的main函数在src/java.base/share/native/launcher/main.c,这个函数会处理一些有参数的内容,然后进入到libjli的JLI_Launch函数中。这个函数最主要的任务就是对libjvm.so进行了加载,同时获取了一些符号的地址,将这些地址保存在了如下一个结构体中:

/*
 * Pointers to the needed JNI invocation API, initialized by LoadJavaVM.
 */
typedef jint (JNICALL *CreateJavaVM_t)(JavaVM **pvm, void **env, void *args);
typedef jint (JNICALL *GetDefaultJavaVMInitArgs_t)(void *args);
typedef jint (JNICALL *GetCreatedJavaVMs_t)(JavaVM **vmBuf, jsize bufLen, jsize *nVMs);

typedef struct {
    CreateJavaVM_t CreateJavaVM; 						 // JNI_CreateJavaVM
    GetDefaultJavaVMInitArgs_t GetDefaultJavaVMInitArgs; // JNI_GetDefaultJavaVMInitArgs
    GetCreatedJavaVMs_t GetCreatedJavaVMs;				 // JNI_GetCreatedJavaVMs
} InvocationFunctions;

虚拟机以动态链接库的形式存在,除了享受到一般的动态链接库的好处之外,还可以将jvm嵌入到其他程序里面。

正式进入主类的main方法其实还有非常多的任务,主要就是初始化一些数据、生成一些代码,以及启动一些工作线程,这些任务主要在Threads::create_vminit_globals中完成。之所以要分为两个线程来完成,推测可能是便于处理异常?我也不太清楚这么设计的原因。在加载完主类之后就会开始执行main方法。

首先使用JNI接口JNIEnv_::CallStaticVoidMethod,这里使用的是C++的JNI接口和C的在风格上有一些不同。这个函数会调用位于src/hotspot/share/prims/jni.cpp的函数jni_CallStaticVoidMethod,下面是其定义:

JNI_ENTRY(void, jni_CallStaticVoidMethod(JNIEnv *env, jclass cls, jmethodID methodID, ...))
  JNIWrapper("CallStaticVoidMethod");
  HOTSPOT_JNI_CALLSTATICVOIDMETHOD_ENTRY(env, cls, (uintptr_t) methodID);
  DT_VOID_RETURN_MARK(CallStaticVoidMethod);

  va_list args;
  va_start(args, methodID);
  JavaValue jvalue(T_VOID);
  JNI_ArgumentPusherVaArg ap(methodID, args);
  jni_invoke_static(env, &jvalue, NULL, JNI_STATIC, methodID, &ap, CHECK);
  va_end(args);
JNI_END

其做的事件有两件,首先是将可变参数构造为一个JNI_ArgumentPusherVaArg对象,这个对象能够提供对参数的访问。另外就是进入到jni_invoke_static,正式发起调用,接下来会依次去到JavaCalls::callos::os_exception_wrapperJavaCalls::call_helper,call_helper里面必须做的就是获取了入口点和处理java方法返回值,其中真正发起调用的地方在:

// do call
{ JavaCallWrapper link(method, receiver, result, CHECK); // 无法理解,除了对JNIHandleBlock的处理之外。
 { HandleMark hm(thread);  // HandleMark used by HandleMarkCleaner
  // generate_call_stub
  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)

里面有个JavaCallWrapper,暂时没有完全明白到底做了什么,里面获取了receiver和result指针,我觉得可能是和GC有关,另外的就是设置了JNIHandleBlock这个和线程的资源管理有关。StubRoutines::call_stub会获取一个CallStub类型的例程。

下面是CallStub的类型声明:

// Calls to Java
typedef void (*CallStub)(
    address   link,
    intptr_t* result,
    BasicType result_type,
    Method* method,
    address   entry_point,
    intptr_t* parameters,
    int       size_of_parameters,
    TRAPS
);

一共有8个参数,amd64 abi下前6个参数使用寄存器进行传递所以还有两个参数需要使用栈传递,分别是size_of_parameters__thread__,进入了call_stub例程后的栈结构如下:

stackframe-enter-call_stub.drawio

这个例程由StubGenerator::generate_call_stub生成,负责栈的切换和调用的实现,栈的变化逻辑非常清晰,下面的代码已经有了注释,进入了java方法的入口之后的处理逻辑已经说过了:

// Call stub stack layout word offsets from rbp
enum call_stub_layout {
    rsp_after_call_off = -12,
    mxcsr_off          = rsp_after_call_off, // simd的寄存器。
    r15_off            = -11,
    r14_off            = -10,
    r13_off            = -9,
    r12_off            = -8,
    rbx_off            = -7,
    call_wrapper_off   = -6,
    result_off         = -5,
    result_type_off    = -4,
    method_off         = -3,
    entry_point_off    = -2,
    parameters_off     = -1,
    rbp_off            =  0,
    retaddr_off        =  1,
    parameter_size_off =  2,
    thread_off         =  3
};


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

    // 保存callee-saved寄存器。
    // 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);
    if (UseAVX > 2) {
        __ movl(rbx, 0xffff);
        __ kmovwl(k1, rbx);
    }
	
    // 不太会用simd拓展,所以不太明白发生了什么,大概还是放入了栈中对应位置吧?
    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();

    // 从JavaArguments的数组中复制对应字数的数据到栈中,这个结构就是通过JNI_ArgumentPusher把可变参数列表转化而来的。
    // 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); // 在这里进入entry_point,实际上是generate_normal_entry生成的代码。

    BLOCK_COMMENT("call_stub_return_address:");
    return_address = __ pc(); // 会成为一个异常返回点,如果发生了无法处理的异常会跳转到这里。

    // 处理返回值,返回值在java方法的栈顶,也就是rax或者xmm0中,x86实现或许有不同。
    // 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); // 将返回值保存地址赋给c_rarg0。
    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); // 返回值放在rax,然后将返回值移动到目标地址。

    __ BIND(exit);

    // pop parameters 恢复到rsp_after_call位置。
    __ 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
    __ vzeroupper();
    __ 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;
}

在正式进入被调用的java函数之前的栈布局和枚举call_stub_layout中描述的一样,除了下面还有传入的参数以外。

posted @ 2022-09-21 22:57  aana  阅读(436)  评论(0编辑  收藏  举报