第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,拉你入虛擬機群交流
  

  

  

 

 

  

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

 

Tags: