• JVM作为一个C/C++编写的java运行时,它是如何调用Java函数的呢?
  • 发布于 1个月前
  • 43 热度
    0 评论
楔子

JVM作为一个C/C++编写的java运行时,它是如何调用Java函数的呢?本篇看下!


过程
一般的来说,java通过把源码编译成字节码,然后JVM加载字节码,编译成机器码,这跟CLR几乎一模一样。但是生成机器码的形式则完全不一。其中JVM的Main入口是个比较关键节点,由于是Linux系统,这里又涉及到了Glibc库,同样的跟CLR一样都是Glibc库来调用JVM入口(C Main)进行运行的。
大致如下:
因为调用太多,中间省略了部分...
Main->__clone3->JavaMain->call_helper->StubRoutines::call_stub
看下最后调用的这个call_stub,它里面调用了Java函数的函数头其实地址(当前你运行哪个函数,它里面就调用哪个函数的函数头进行机器码运行)。
call_stub原型如下:
static CallStub call_stub() (return CAST_TO_FN_PTR(CallStub, _call_stub_entry); }
这个CAST_TO_FN_PTR到底干嘛的呢?看下它的assembly
 0x7ffff6547da4 <+0>:  endbr64 
    0x7ffff6547da8 <+4>:  push   rbp
    0x7ffff6547da9 <+5>:  mov    rbp, rsp
    0x7ffff6547dac <+8>:  lea    rax, [rip + 0x17dc405]
    0x7ffff6547db3 <+15>: mov    rax, qword ptr [rax]
    0x7ffff6547db6 <+18>: pop    rbp
    0x7ffff6547db7 <+19>: ret    
上面代码很清楚了,把_call_stub_entry的函数指针([rip + 0x17dc405])
赋值(mov   rax, qword ptr [rax])给CallStub。
也就是把_call_stub_entry函数指针赋值给CallStub然后返回。那么这个call_stub函数调用相当于_call_stub_entry函数调用。

寄存器rax保存的就是_call_stub_entry函数指针,注意,它返回的时候把这个rax赋值给了寄存器rbx。
(lldb) di -l
libjvm.so`JavaCalls::call_helper:
    0x7ffff65474fb <+1389>: call   0x7ffff6547da4            ; StubRoutines::call_stub at stubRoutines.hpp:305:19
->  0x7ffff6547500 <+1394>: mov    rbx, rax
继续看
(lldb) si
(lldb) di -l
  0x7ffff6547503 <+1397>: mov    rax, qword ptr [rbp - 0x108]
    0x7ffff654750a <+1404>: mov    rdi, rax
    0x7ffff654750d <+1407>: call   0x7ffff6547f1e            ; JavaCallArguments::size_of_parameters at javaCalls.hpp:205:9
    0x7ffff6547512 <+1412>: mov    r12d, eax
    0x7ffff6547515 <+1415>: mov    rax, qword ptr [rbp - 0x100]
    0x7ffff654751c <+1422>: mov    rdi, rax
    0x7ffff654751f <+1425>: call   0x7ffff5bda5f2            ; methodHandle::operator() at handles.hpp:174:1
    0x7ffff6547524 <+1430>: mov    rcx, rax
    0x7ffff6547527 <+1433>: mov    r8, qword ptr [rbp - 0xb0]
    0x7ffff654752e <+1440>: mov    rdi, qword ptr [rbp - 0xd8]
    0x7ffff6547535 <+1447>: mov    edx, dword ptr [rbp - 0xec]
    0x7ffff654753b <+1453>: mov    rsi, qword ptr [rbp - 0xb8]
    0x7ffff6547542 <+1460>: lea    rax, [rbp - 0x60]
    0x7ffff6547546 <+1464>: push   qword ptr [rbp - 0x110]
    0x7ffff654754c <+1470>: push   r12
    0x7ffff654754e <+1472>: mov    r9, r8
    0x7ffff6547551 <+1475>: mov    r8, rdi
    0x7ffff6547554 <+1478>: mov    rdi, rax
    0x7ffff6547557 <+1481>: call   rbx
看它最后一句:call rbx。这个rbx就是上面的_call_stub_entry函数的指针,它直接跳转。而call_stub的几个参数分别存入寄存器,这里简单看下。
(lldb) b 0x7ffff6547557
(lldb) c
(lldb) di -l
(lldb) register read rdi rsi rdx rcx r8 r9
     rdi = 0x00007ffff576e5c0
     rsi = 0x00007ffff576e6e8
     rdx = 0x000000000000000a
     rcx = 0x00007fffc40301b0
      r8 = 0x00007fffe85564e0
      r9 = 0x00007ffff576e740
(lldb) p &link
(JavaCallWrapper *) $21 = 0x00007ffff576e5c0
(lldb) p result_val_address 
(intptr_t *) $22 = 0x00007ffff576e6e8
(lldb) p result_type
(BasicType) $23 = T_INT
(lldb) p method()
(Method *) $24 = 0x00007fffc40301b0
(lldb) p entry_point
(address) $25 = 0x00007fffe85564e0 "H\x8bS\U00000010\U0000000f\xb7J,\U0000000f\xb7R*+с\xfa\xf5\U00000001"
(lldb) p parameter_address 
(intptr_t *) $26 = 0x00007ffff576e740
我们看到
rdi=&plink
rsi=result_val_address 
rdx=result_type
rcx=method()
r8=entry_point
r9=parameter_address
这里着重关注下,r8寄存器。它保存的是当前需要运行的函数的函数头的起始地址。比如运行java源码的Main函数。上面运行到了call rbx此处,而rbx则是_call_stub_entry函数的函数头其实地址。进入里面。
(lldb) si
Process 511497 stopped
* thread #2, name = 'java', stop reason = instruction step into
    frame #0: 0x00007fffe8537c9c
->  0x7fffe8537c9c: push   rbp
    0x7fffe8537c9d: mov    rbp, rsp
    0x7fffe8537ca0: sub    rsp, 0x60
    0x7fffe8537ca4: mov    qword ptr [rbp - 0x8], r9
(lldb) di -s 0x7fffe8537c9c -c80
->  0x7fffe8537c9c: push   rbp
    0x7fffe8537c9d: mov    rbp, rsp
    0x7fffe8537ca0: sub    rsp, 0x60
    0x7fffe8537ca4: mov    qword ptr [rbp - 0x8], r9
    0x7fffe8537ca8: mov    qword ptr [rbp - 0x10], r8
    //中间太多,省略.......
    0x7fffe8537d3e: mov    rsi, qword ptr [rbp - 0x10]
    0x7fffe8537d42: mov    r13, rsp
    0x7fffe8537d45: call   rsi
看下最后一条指令:call rsi。rsi保存的是什么呢?
往上看地址0x7fffe8537d3e:mov  rsi, qword ptr [rbp - 0x10]。
可以看到rsi里面保存的是栈上rbp-0x10地址处的值。
继续往上,地址 0x7fffe8537ca8: mov    qword ptr [rbp - 0x10], r8可以看到rbp-0x10地址处保存到是当前需要运行的函数的函数头地址。而call rsi就是跳转到当前需要运行的函数的函数头地址处进行运行此函数.
那么基本上清楚了:
call_stub ->_call_stub_entry ->当前需要运行的函数函数头(比如托管Main入口函数).
entry_point
那么还有个问题,这个entry_point哪里来的呢?看下这个函数:generate_all。它的调用如下:
JavaMain -》InitializeJVM-》generate_all-》set_interpreter_entry
看下set_interpreter_entry函数原型:
 505   void set_interpreter_entry(address entry) {
 506     if (_i2i_entry != entry) {
 507       _i2i_entry = entry;
 508     }
 509     if (_from_interpreted_entry != entry) {
 510       _from_interpreted_entry = entry;
 511     }
 512   }
也就是说在JVM初始化的时候调用了generate_all把当前运行的函数编译成机器码,然后通过set_interpreter_entry设置当前函数的函数头地址,也就是_from_interpreted_entry。在调用的时候直接获取到entry_point ,跳转到当前正在运行的函数的函数头执行。

结尾:
JVM的机器码是先编译好,存储起来。然后在调用函数的时候直接获取到被调用的函数的函数头地址,跳转到此地址执行被调用的函数。
用户评论