第47篇-解釋執行的Java方法調用native方法小實例
- 2021 年 12 月 29 日
- 筆記
舉個小實例,如下:
public class TestJNI {
static {
// 程式在載入時,自動載入libdiaoyong.so庫
System.loadLibrary("diaoyong");
}
public static native int get();
public static void main(String[] args) {
TestJNI.get();
}
}
其位元組碼的實現如下:
Constant pool:
#1 = Methodref #6.#18 // java/lang/Object."<init>":()V
#2 = Methodref #5.#19 // TestJNI.get:()I
#3 = String #20 // diaoyong
#4 = Methodref #21.#22 // java/lang/System.loadLibrary:(Ljava/lang/String;)V
#5 = Class #23 // TestJNI
#6 = Class #24 // java/lang/Object
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 get
#12 = Utf8 ()I
#13 = Utf8 main
#14 = Utf8 ([Ljava/lang/String;)V
#15 = Utf8 <clinit>
#16 = Utf8 SourceFile
#17 = Utf8 TestJNI.java
#18 = NameAndType #7:#8 // "<init>":()V
#19 = NameAndType #11:#12 // get:()I
#20 = Utf8 diaoyong
#21 = Class #25 // java/lang/System
#22 = NameAndType #26:#27 // loadLibrary:(Ljava/lang/String;)V
#23 = Utf8 TestJNI
#24 = Utf8 java/lang/Object
#25 = Utf8 java/lang/System
#26 = Utf8 loadLibrary
#27 = Utf8 (Ljava/lang/String;)V
{
// ...
public static native int get();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC, ACC_NATIVE
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: invokestatic #2 // Method get:()I
3: pop
4: return
// ...
}
native方法get()對應的本地函數的頭文件TestJNI.h的實現如下:
#include <jni.h>
#ifndef _Included_TestJNI
#define _Included_TestJNI
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jint JNICALL Java_TestJNI_get(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
TestJNI.c文件的實現如下:
#include <stdio.h>
#include "TestJNI.h"
JNIEXPORT jint JNICALL Java_TestJNI_get(JNIEnv * env, jclass jc){
printf("ok!You have successfully passed the Java call c\n");
return 100;
}
為如上的本地方法生成libdiaoyong.so動態鏈接庫,運行後會輸出如下結果:
ok!You have successfully passed the Java call c
由於native方法本質上是C/C++函數,所以不會有對應的位元組碼。我們在main()方法中通過invokestatic位元組碼指令調用native方法,在執行invokestatic位元組碼之前棧狀態如下圖所示。
下面我們來簡單介紹一下解釋執行的main()方法調用native方法get()的具體過程。
調用的invokestatic位元組碼指令的彙編如下:
0x00007fffe101c030: mov %r13,-0x38(%rbp) 0x00007fffe101c034: movzwl 0x1(%r13),%edx 0x00007fffe101c039: mov -0x28(%rbp),%rcx 0x00007fffe101c03d: shl $0x2,%edx 0x00007fffe101c040: mov 0x10(%rcx,%rdx,8),%ebx 0x00007fffe101c044: shr $0x10,%ebx 0x00007fffe101c047: and $0xff,%ebx 0x00007fffe101c04d: cmp $0xb8,%ebx // 檢查invokestatic=184的bytecode是否已經連接,如果已經連接就進行跳轉 0x00007fffe101c053: je 0x00007fffe101c0f2 // 調用InterpreterRuntime::resolve_invoke()函數對invokestatic=184的 // 的bytecode進行連接,因為位元組碼指令還沒有連接 // ... 省略了解析invokestatic的彙編程式碼 // 將invokestatic x中的x載入到%edx中 0x00007fffe101c0e6: movzwl 0x1(%r13),%edx // 將ConstantPoolCache的首地址存儲到%rcx中 0x00007fffe101c0eb: mov -0x28(%rbp),%rcx // %edx中存儲的是ConstantPoolCacheEntry項的索引,轉換為字偏移 0x00007fffe101c0ef: shl $0x2,%edx // 獲取ConstantPoolCache::_f1屬性的值 0x00007fffe101c0f2: mov 0x18(%rcx,%rdx,8),%rbx // 獲取ConstantPoolCache::_flags屬性的值 0x00007fffe101c0f7: mov 0x28(%rcx,%rdx,8),%edx // 從flags中獲取return type,也就是從_flags的高4位保存的TosState 0x00007fffe101c0fb: shr $0x1c,%edx // 將TemplateInterpreter::invoke_return_entry地址存儲到%r10 0x00007fffe101c0fe: movabs $0x7ffff73b5d00,%r10 // 找到對應return type的invoke_return_entry的地址 0x00007fffe101c108: mov (%r10,%rdx,8),%rdx // 壓入返回地址,這個返回地址就是通過invokestatic指令調用的函數的返回地址 0x00007fffe101c10c: push %rdx // 設置調用者棧頂 0x00007fffe101c10d: lea 0x8(%rsp),%r13 // 向棧中last_sp的位置保存調用者棧頂 0x00007fffe101c112: mov %r13,-0x10(%rbp) // 跳轉到Method::_from_interpretered_entry入口去執行 0x00007fffe101c116: jmpq *0x58(%rbx)
根據ConstantCachePoolEntry中的資訊來獲取返回地址TemplateInterpreter::invoke_return_entry並壓入棧中,然後就會跳轉到Method::_from_interpretered_entry去執行,這個Method::_from_interpretered_entry保存的就是由
InterpreterGenerator::generate_native_entry()函數生成的常式入口。此時的棧幀狀態如下圖所示。

這裡需要提示一下,因為使用invokestatic調用的get()方法沒有參數,所以在-0x8(%rsp)的位置處並沒有本地變數表。我們可以舉一個需要本地變數表傳遞參數的例子,如下:
public class TestLocalTable {
public void get(int a,int b) {
// ...
}
public static void main(String args[]) {
get(1,2);
}
}
在test()方法中調用實例方法get(),並且傳遞了2個參數,生成的位元組碼如下:
public void test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
0: aload_0
1: iconst_1
2: iconst_2
3: invokevirtual #2 // Method get:(II)V
6: return
實際上會在test()方法的表達式棧中壓入3個實參,分別是接收者、常數1和常數2,而這3個參數會做為get()方法局部變數表的一部分存在,所以無論是invokevirtual還是invokestatic等位元組碼指令,在調用時,調用者的表達式棧中已經準備好了實參,這一部分將做為被調用者的局部變數表組成的一部分,這叫棧幀重疊,之前介紹過。
開始執行native方法的常式,如下:
// 在調用此常式時,各個暫存器中的值如下: // rbx: Method* // r13: sender sp // 將ConstMethod*存儲到%rcx中 0x00007fffe1014c00: mov 0x10(%rbx),%rcx // 將參數的大小存儲到%ecx中 0x00007fffe1014c04: movzwl 0x2a(%rcx),%ecx // 將返回地址彈出到%rax中 0x00007fffe1014c08: pop %rax // rbx: Method* // rcx: size of parameters 通過上面的操作,將參數的大小存儲到rcx暫存器中 // r13: sender sp // 根據%rsp和參數大小計算參數的地址 // %r14指向局部變數表第一個參數的位置 // 注意,由於調用的是native方法,所以局部變數表只用來單純傳遞參數, // 不用考慮本地變數,所以我們只開闢能存儲參數大小的局部變數表即可 0x00007fffe1014c09: lea -0x8(%rsp,%rcx,8),%r14 // 為本地調用初始化兩個8位元組的數據,其中一個保存result_handler,一個保存oop temp 0x00007fffe1014c0e: pushq $0x0 // oop temp對於靜態的native方法來說,保存的可能是mirror, // 或者native方法調用結果為對象時,保存這個對象 0x00007fffe1014c13: pushq $0x0
由於用來傳遞參數的局部變數表已經存在於棧中了,所以可通過lea -0x8(%rsp,%rcx,8),%r14彙編指令直接計算局部變數表第1個參數的地址,然後保存到%r14中。
接下來為native方法生成棧幀,如下:
0x00007fffe1014c18: push %rax 0x00007fffe1014c19: push %rbp 0x00007fffe1014c1a: mov %rsp,%rbp 0x00007fffe1014c1d: push %r13 0x00007fffe1014c1f: pushq $0x0 0x00007fffe1014c24: mov 0x10(%rbx),%r13 0x00007fffe1014c28: lea 0x30(%r13),%r13 0x00007fffe1014c2c: push %rbx 0x00007fffe1014c2d: mov 0x18(%rbx),%rdx 0x00007fffe1014c31: test %rdx,%rdx 0x00007fffe1014c34: je 0x00007fffe1014c41 0x00007fffe1014c3a: add $0x90,%rdx 0x00007fffe1014c41: push %rdx 0x00007fffe1014c42: mov 0x10(%rbx),%rdx 0x00007fffe1014c46: mov 0x8(%rdx),%rdx 0x00007fffe1014c4a: mov 0x18(%rdx),%rdx 0x00007fffe1014c4e: push %rdx 0x00007fffe1014c4f: push %r14 0x00007fffe1014c51: pushq $0x0 0x00007fffe1014c56: pushq $0x0 0x00007fffe1014c5b: mov %rsp,(%rsp)
執行完如上彙編後的棧幀狀態如下圖所示。

接著開闢傳參空間,這個空間將會存放native方法對應的本地函數需要的參數,如下:
// 從棧幀中取出Method*存儲到%rbx中 0x00007fffe1014d87: mov -0x18(%rbp),%rbx // 獲取ConstMethod*存儲到%r11中 0x00007fffe1014d8b: mov 0x10(%rbx),%r11 // 將方法參數的大小放到%r11d中 0x00007fffe1014d8f: movzwl 0x2a(%r11),%r11d // 將%r11d中的內容左移3位,也就是算出方法參數需要佔用的位元組數 0x00007fffe1014d94: shl $0x3,%r11d // 更新%rsp的值,為方法參數開闢存儲參數的空間 0x00007fffe1014d98: sub %r11,%rsp // 對linux系統來說不起作用 0x00007fffe1014d9b: sub $0x0,%rsp // 必須是16位元組邊界(see amd64 ABI) 0x00007fffe1014d9f: and $0xfffffffffffffff0,%rsp
本地函數Java_TestJNI_get()雖然需要JNIEnv*和jclass參數,但是這2個參數是通過暫存器傳遞的,所以本實例不需要開闢任何傳參空間。
我們能夠看到,一個解釋執行的Java方法調用native方法時,需要有局部變數表來給native方法傳遞參數,然後在調用native方法對應的本地函數時,還需要開闢另外一個傳參空間。現在局部變數表已經有值,而新開闢的空間還沒有設置對應的值,接著就是調用signature_handler來根據局部變數表中存儲的值設置新開闢空間中各個slot的值了。之所以這樣做,就是因為解釋執行的調用約定和本地函數的調用約定不同,也就是傳參的約定不同。
接下來是執行signature_handler,如下:
// 調用Method::signature_handler函數
0x00007fffe1014e40: callq *%r11
// 重新獲取Method
0x00007fffe1014e43: mov -0x18(%rbp),%rbx
// 將%rax中的result_handler存儲到方法棧幀中,result_handler
// 是執行signature_handler常式後的返回值,根據方法簽名的返回類型獲取的
0x00007fffe1014e47: mov %rax,0x18(%rbp)
Method實例的第2個附加slot的signature_handler指向的常式用來消除Java解釋器棧和C/C++棧調用約定的不同,將位於解析器棧中的參數適配到本地函數使用的C棧。生成的signature_handler與result_handler的常式如下:
argument handler #56 for: static TestJNI.get()I (fingerprint = 341, 11 bytes generated) // 將result_handler的地址存儲到%rax中 0x00007f98e911c85d: movabs $0x7f98e900f1f6,%rax 0x00007f98e911c867: retq --- associated result handler --- 0x00007f98e900f1f9: retq
result handler的實現非常簡單,因為本地方法根據調用約定,會將int類型的返回值放到%rax中,我們只需要從%rax中獲取值即可。
接下來會執行如下彙編程式碼:
// 將Method::access_flags存儲到%r11d中 0x00007fffe1014e4b: mov 0x28(%rbx),%r11d // 判斷是否為static本地方法,其中$0x8表示JVM_ACC_STATIC 0x00007fffe1014e4f: test $0x8,%r11d // 如果為0,表示是非static方法,要跳轉到-- L2 -- 0x00007fffe1014e56: je 0x00007fffe1014e74 // 執行這裡程式碼時,說明方法是static方法 // 如下4個mov指令將通過Method->ConstMehod->ConstantPool->mirror // 獲取到java.lang.Class的oop 0x00007fffe1014e5c: mov 0x10(%rbx),%r11 0x00007fffe1014e60: mov 0x8(%r11),%r11 0x00007fffe1014e64: mov 0x20(%r11),%r11 0x00007fffe1014e68: mov 0x70(%r11),%r11 // 將mirror存儲到棧幀中,也就是oop temp這個slot位置 0x00007fffe1014e6c: mov %r11,0x10(%rbp) // 將mirror拷到%rsi中作為靜態方法調用的第2個參數 0x00007fffe1014e70: lea 0x10(%rbp),%rsi
對於實例來說,get()方法是靜態方法,所以會將mirror放到棧幀中的oop temp中。
接下來執行如下彙編:
// 獲取Method::native_function的地址並存儲到%rax中 0x00007fffe1014e74: mov 0x60(%rbx),%rax // %r11中存儲的是SharedRuntime::native_method_throw_unsatisfied_link_error_entry() 0x00007fffe1014e78: movabs $0x7ffff6a08f14,%r11 // 判斷rax中的地址是否是native_method_throw_unsatisfied_link_error_entry的 // 地址,如果是說明本地方法未綁定 0x00007fffe1014e82: cmp %r11,%rax // 如果不等於,即native方法已經綁定,跳轉到----L3---- 0x00007fffe1014e85: jne 0x00007fffe1014f1b // ... 省略查找native_function的邏輯 // 重新獲取Method*到%rbx中 0x00007fffe1014f13: mov -0x18(%rbp),%rbx // 獲取native_function的地址拷到%rax中 0x00007fffe1014f17: mov 0x60(%rbx),%rax
我們假設native_function已經存儲到了Method實例的對應slot處,那麼接下來就直接調用這個本地函數了,如下:
// 將當前執行緒的JavaThread::jni_environment放入c_rarg0,也就是%rdi中 0x00007fffe1014f1b: lea 0x210(%r15),%rdi // ... // 調用native_function本地函數 0x00007fffe1014f4c: callq *%rax // ... // 如下4行程式碼是為了保存調用native_function函數後得到的結果,將 // 結果存儲到棧頂 0x00007fffe1014f51: sub $0x10,%rsp 0x00007fffe1014f55: vmovsd %xmm0,(%rsp) 0x00007fffe1014f5a: sub $0x10,%rsp 0x00007fffe1014f5e: mov %rax,(%rsp)
在調用native方法時,將JNIEnv*存儲到c_rarg0,mirror存儲到c_rarg1中,然後調用native方法的本地函數。根據C/C++函數的調用約定,如果返回浮點數,則會存儲到%xmm0中,如果是對象或整數等類型,則會存儲到%rax中。將%xmm0和%rax中的值壓入棧中,最後會執行如下彙編程式碼:
// 將棧頂的代表方法調用結果的數據pop到%rax和%xmm0暫存器中 0x00007fffe101543c: mov (%rsp),%rax 0x00007fffe1015440: add $0x10,%rsp 0x00007fffe1015444: vmovsd (%rsp),%xmm0 0x00007fffe1015449: add $0x10,%rsp // 獲取result_handler存儲到%r11中 0x00007fffe101544d: mov 0x18(%rbp),%r11 0x00007fffe1015451: callq *%r11 // 調用result_handler處理方法調用結果 0x00007fffe1015454: mov -0x8(%rbp),%r11 // 獲取sender sp,開始恢復上一個Java棧幀 0x00007fffe1015458: leaveq // 相當於指令mov %ebp,%esp和pop %ebp 0x00007fffe1015459: pop %rdi // 獲取return address 0x00007fffe101545a: mov %r11,%rsp // 設置sender sp 0x00007fffe101545d: jmpq *%rdi // 跳轉到返回地址處繼續執行
調用result_handler處理方法調用結果,最終只是執行了retq指令,所以此次的callq和retq指令執行後沒有對棧幀產生任何影響。
繼續執行Interpreter::_invoke_return_entry常式,如下:
// 將-0x10(%rbp)存儲到%rsp後,置空-0x10(%rbp)
0x00007fffe1006ce0: mov -0x10(%rbp),%rsp // 更改rsp
0x00007fffe1006ce4: movq $0x0,-0x10(%rbp) // 更改棧中特定位置的值
// 恢復bcp和locals,使%r14指向本地變數表,%r13指向bcp
0x00007fffe1006cec: mov -0x38(%rbp),%r13
0x00007fffe1006cf0: mov -0x30(%rbp),%r14
// 獲取ConstantPoolCacheEntry的索引並載入到%ecx
0x00007fffe1006cf4: movzwl 0x1(%r13),%ecx
// 獲取棧中-0x28(%rbp)的ConstantPoolCache並載入到%ecx
0x00007fffe1006cf9: mov -0x28(%rbp),%rbx
// shl是邏輯左移,獲取字偏移
0x00007fffe1006cfd: shl $0x2,%ecx
// 獲取ConstantPoolCacheEntry中的_flags屬性值
0x00007fffe1006d00: mov 0x28(%rbx,%rcx,8),%ebx
// 獲取_flags中的低8位中保存的參數大小
0x00007fffe1006d04: and $0xff,%ebx
// 注意這裡會更改%rsp的指向,會將調用方表達式棧(被調用方局部變數表組成的一部分)中壓入的、給調用的
// 方法傳遞參數的值從表達式棧中彈出去,這樣在解釋執行的情況下,由調用方完成實參的清理工作
0x00007fffe1006d0a: lea (%rsp,%rbx,8),%rsp
// 跳轉到下一指令執行
0x00007fffe1006d0e: movzbl 0x3(%r13),%ebx
0x00007fffe1006d13: add $0x3,%r13
0x00007fffe1006d17: movabs $0x7ffff73b7ca0,%r10
0x00007fffe1006d21: jmpq *(%r10,%rbx,8)
如上彙編主要是恢復調用方的棧幀狀態,同時清理表達式棧中因為調用方法而壓入的實參,最後就是繼續執行main()方法中剩餘指令了。
公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源程式碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流

