第45篇-查找native方法的本地實現函數native_function

  • 2021 年 12 月 16 日
  • 筆記

在之前介紹為native方法設置解釋執行的入口時講到過Method實例的內存布局,如下:

對於第1個slot來說,如果是native方法,其對應的本地函數的實現會放到Method實例的native_function這個slot中,將本地函數放到這個slot就是registerNative()函數要完成的。

在前面介紹為native方法生成解釋執行入口時介紹過,當判斷出Method::native_function還沒有值時,會調用InterpreterRuntime::prepare_native_call()函數為Method::native_function賦值。

InterpreterRuntime::prepare_native_call()函數的實現如下:

IRT_ENTRY(void, InterpreterRuntime::prepare_native_call(
	JavaThread* thread,
	Method*     method
))
  methodHandle m(thread, method);
  bool in_base_library;

  // 如果Method::native_function還沒有值,需要調用NativeLookup::lookup()函數
  if (!m->has_native_function()) {
    NativeLookup::lookup(m, in_base_library, CHECK);
  }

  // 保證Method::signature_handler有值
  SignatureHandlerLibrary::add(m);
IRT_END

如上函數會先調用Method::has_native_function()函數檢查之前是否已經在Method實例里記錄下了本地函數的入口地址。如果已經記錄了的話,那麼可能是JNI庫在JNI_OnLoad()函數執行的時候調用了RegisterNatives()函數註冊了函數地址信息,也有可能不是第一次調用該native方法,之前已經完成了查找記錄的過程。

我們在之前介紹JavaVM和JNIEnv時舉過一個使用RegisterNatives()函數註冊函數地址的小實例,如下:

static JNINativeMethod method = { // 本地方法描述
        "getName",                // Java方法名
        "(I)Ljava/lang/String;",  // Java方法簽名
        (void *) getName          // 綁定到對應的本地函數
};
 
static bool  bindNative(JNIEnv *env) {
    jclass clazz;
    clazz = env->FindClass(CLASS_NAME);
    if (clazz == NULL) {
        return false;
    }
    return env->RegisterNatives(clazz, &method, 1) == 0;
}

native方法getName的本地實現函數為getName,通過RegisterNatives()函數確定這種映射關係。RegisterNatives()函數會調用JNI函數jni_RegisterNatives(),在jni_RegisterNatives()函數中調用register_native()函數,register_native()函數的實現如下:

static bool register_native(KlassHandle k, Symbol* name, Symbol* signature, address entry, TRAPS) {
  Method* method = k()->lookup_method(name, signature);
  // ...
  if (entry != NULL) {
    method->set_native_function(entry,Method::native_bind_event_is_interesting);
  } else {
    method->clear_native_function();
  }
  return true;
}

可以看到,將本地函數getName()的地址保存到了Method::native_function中,這樣在執行native方法時就可執行Method::native_function函數了。

如果沒有在Method::native_function中記錄下函數地址,需要調用NativeLookup::lookup()函數來尋找native方法真正的目標在什麼地方,然後把它記在Method實例里。調用NativeLookup::lookup()函數查找本地函數,實現如下:

源代碼位置:openjdk/hotspot/share/vm/prims/nativeLookup.cpp
address NativeLookup::lookup(
 methodHandle  method,
 bool&         in_base_library,
 TRAPS
) {
  if (!method->has_native_function()) {
    address entry = lookup_base(method, in_base_library, CHECK_NULL);
    method->set_native_function(entry,Method::native_bind_event_is_interesting);
  }
  return method->native_function();
}

調用lookup_base()函數獲取native_function,然後調用set_native_function()函數將native_function保存到Method實例中。調用的lookup_base()函數的實現如下:

address NativeLookup::lookup_base(
 methodHandle method, 
 bool& in_base_library,  
 TRAPS
) {
  address      entry = NULL;
  ResourceMark rm(THREAD);

  entry = lookup_entry(method, in_base_library, THREAD);
  if (entry != NULL)
     return entry;
  // ...
}

如上函數調用的lookup_entry()函數的實現如下 :

address NativeLookup::lookup_entry(
 methodHandle  method,
 bool&         in_base_library,
 TRAPS
) {
  address entry = NULL;
  // in_base_library是引用傳遞
  in_base_library = false; 
  // 構造出符合JNI規範的函數名
  char* pure_name = pure_jni_name(method);

  // 計算實參的參數數量
  int args_size = 1                             // JNIEnv
                + (method->is_static() ? 1 : 0) // class for static methods
                + method->size_of_parameters(); // actual parameters


  // 1) Try JNI short style
  entry = lookup_style(method, pure_name, "",args_size, true,  in_base_library, CHECK_NULL);
  if (entry != NULL){
	  return entry;
  }
  // Compute long name
  char* long_name = long_jni_name(method);

  // 2) Try JNI long style
  entry = lookup_style(method, pure_name, long_name, args_size, true,  in_base_library, CHECK_NULL);
  if (entry != NULL){
	  return entry;
  }
  // 3) Try JNI short style without os prefix/suffix
  entry = lookup_style(method, pure_name, "",args_size, false, in_base_library, CHECK_NULL);
  if (entry != NULL){
	  return entry;
  }
  // 4) Try JNI long style without os prefix/suffix
  entry = lookup_style(method, pure_name, long_name, args_size, false, in_base_library, CHECK_NULL);

 // entry可能為NULL,當為NULL時,表示沒有查找到對應的本地函數實現
  return entry;
}

如上函數通過NativeLookup::pure_jni_name()函數來構造出符合JNI規範的函數名,然後通過NativeLookup::lookup_style()函數在查找路徑中能夠找到的所有動態鏈接庫里去找這個名字對應的地址。我們可以看到,函數的名稱有許多種可能,所以在查找不到對應的本地函數時,會多次調用NativeLookup::lookup_style()函數查找,如果最後沒有查到,則返回NULL。

其實對linux來說,如果第1次和第3次的查找邏輯一樣,第2次和第4次的查找邏輯一樣,所以我們只看第1次和第2次的查找邏輯即可。

(1)第1次查找

第一次查找時,調用的pure_jni_name()函數的實現如下:

char* NativeLookup::pure_jni_name(methodHandle method) {
  stringStream st;
  // 前綴
  st.print("Java_");
  // 類名稱
  mangle_name_on(&st, method->klass_name());
  st.print("_");
  // 方法名稱
  mangle_name_on(&st, method->name());
  return st.as_string();
}

拼接出來的函數名稱是「Java_Java程序的package路徑_函數名」。

(2)第2次查找

如果有重載的native方法,那麼按第1次查找時生成的函數名稱是無法查找到的,還需要在生成的函數名稱中加上參數相關信息,這樣才能區分出2個重載的native方法對應的本地函數的不同。

調用NativeLookup::long_jni_name(函數生成帶有參數相關信息的函數名稱,函數的實現如下:

char* NativeLookup::long_jni_name(methodHandle method) {
  // Signature ignore the wrapping parenteses and the trailing return type
  stringStream st;
  Symbol* signature = method->signature();
  st.print("__");
  // find ')'
  int end;
  for (end = 0; end < signature->utf8_length() && signature->byte_at(end) != ')'; end++);
  // skip first '('
  mangle_name_on(&st, signature, 1, end);
  return st.as_string();
}

調用的mangle_name_on()函數的實現如下:

static void mangle_name_on(outputStream* st, Symbol* name, int begin, int end) {
  char* bytes = (char*)name->bytes() + begin;
  char* end_bytes = (char*)name->bytes() + end;
  while (bytes < end_bytes) {
    jchar c;
    bytes = UTF8::next(bytes, &c);
    if (c <= 0x7f && isalnum(c)) {
      st->put((char) c);
    } else {
           if (c == '_') st->print("_1");
      else if (c == '/') st->print("_");
      else if (c == ';') st->print("_2");
      else if (c == '[') st->print("_3");
      else               st->print("_%.5x", c);
    }
  }
}

我們舉個例子,如下:

public class TestJNIName {
	public native void get();
	public native void get(Object a,int b);
}

通過javah生成的TestJNIName.h文件的主要內容如下:

JNIEXPORT void JNICALL  Java_TestJNIName_get__
  (JNIEnv *, jobject);

JNIEXPORT void JNICALL  Java_TestJNIName_get__Ljava_lang_Object_2I
  (JNIEnv *, jobject, jobject, jint);

可以看到在方法名稱後會添加雙下劃線,然後就是按照一定的規則拼接參數類型了。 

第1次和第2次都會調用NativeLookup::lookup_style()函數查找本地函數。NativeLookup::lookup_style()函數的實現如下: 

address NativeLookup::lookup_style(
 methodHandle   method,
 char*          pure_name,
 const char*   long_name,
 int            args_size,
 bool           os_style,
 bool&          in_base_library,
 TRAPS
) {
  address entry;
  // 拼接pure_name和long_name
  stringStream st;
  st.print_raw(pure_name);
  st.print_raw(long_name);
  char* jni_name = st.as_string();

  Handle loader(THREAD, method->method_holder()->class_loader());
  // 當loader為NULL時,表示method所屬的類是通過系統類加載器加載的
  if (loader.is_null()) {
    // 如果是查找registerNatives()函數,則直接返回實現函數的地址
    entry = lookup_special_native(jni_name);
    if (entry == NULL) {
       // 查找本地動態鏈接庫,Linux下則是libjava.so
       void* tmp = os::native_java_library();
       // 找到本地動態鏈接庫,調用os::dll_lookup查找符號表
       entry = (address) os::dll_lookup(tmp, jni_name);
    }
    if (entry != NULL) {
      in_base_library = true;
      return entry;
    }
  }

  // Otherwise call static method findNative in ClassLoader
  // 調用java.lang.ClassLoader中的findNative()方法查找
  KlassHandle   klass (THREAD, SystemDictionary::ClassLoader_klass());
  Handle name_arg = java_lang_String::create_from_str(jni_name, CHECK_NULL);

  JavaValue result(T_LONG);
  JavaCalls::call_static(&result,
                         klass,
                         vmSymbols::findNative_name(),
                         vmSymbols::classloader_string_long_signature(),
                         loader,   // 為findNative()傳遞的第1個參數
                         name_arg, // 為findNative()傳遞的第2個參數
                         CHECK_NULL);
  entry = (address) (intptr_t) result.get_jlong();

  if (entry == NULL) {
    // findNative didn't find it, if there are any agent libraries look in them
    AgentLibrary* agent;
    for (agent = Arguments::agents(); agent != NULL; agent = agent->next()) {
      // 找到本地動態鏈接庫,調用os::dll_lookup查找符號表
      entry = (address) os::dll_lookup(agent->os_lib(), jni_name);
      if (entry != NULL) {
        return entry;
      }
    }
  }

  return entry;
}

根據如上函數的實現,我們可以從3個地方來查找動態鏈接庫,找到動態鏈接庫後就可以調用os::dll_lookup()函數查找指定名稱的本地函數了。

(1)如果native方法所屬的類是系統類加載器加載的,那麼系統類加載器中的native方法的本地函數實現一般會在libjava.so中。

(2)如果在libjava.so中沒有找到,則調用java.lang.ClassLoader.findNative()方法進行查找。調用java.lang.ClassLoader.findNative()方法能夠查找到用戶自己創建出的動態鏈接庫,如我們編寫native方法時,通常會通過System.load()或System.loadLibrary()方法加載動態鏈接庫,這2個方法最終會調用到ClassLoader.loadLibrary()方法將相關的動態鏈接庫保存下來供findNative()方法查找使用;​

(3)如果步驟1和步驟2都沒有找到,則從加載的代理庫中查找,如我們在虛擬機啟動時配置的-agentlib或attach到目標進程後發送load命令加載的動態鏈接庫都有可以包含本地函數的實現。

通過navie方法找對應的本地函數的實現過程如下圖所示。

 

公眾號 深入剖析Java虛擬機HotSpot 已經更新虛擬機源代碼剖析相關文章到60+,歡迎關注,如果有任何問題,可加作者微信mazhimazh,拉你入虛擬機群交流