Android的JNI_OnLoad
一、JNI_OnLoad简介
Java JNI有两种方法,一种是通过javah,获取一组带签名函数,然后实现这些函数。 这种方法很常用,也是官方推荐的方法。 还有一种就是JNI_OnLoad方法。
当Android的VM(Virtual Machine)执行到C组件(即so档)里的System.loadLibrary()函数时,
首先会去执行C组件里的JNI_OnLoad()函数。
它的用途有二:
- 告诉VM此C组件使用那一个JNI版本。 如果你的.so档没有提供JNI_OnLoad()函数,VM会默认该*.so档是使用最老的JNI 1.1版本。 由于新版的JNI做了许多扩充,如果需要使用JNI的新版功能, 例如JNI 1.4的java.nio.ByteBuffer,就必须藉由JNI_OnLoad()函数来告知VM。 
- 由于VM执行到System.loadLibrary()函数时,就会立即先呼叫JNI_OnLoad(), 所以C组件的开发者可以藉由JNI_OnLoad()来进行C组件内的初期值之设定(Initialization) 。 
其实Android中的so文件就像是Windows下的DLL一样,JNI_OnLoad和JNI_OnUnLoad函数 就像是DLL中的PROCESS ATTATCH和DEATTATCH的过程一样,可以同样做一些初始化和反初始化的动作。
二、Android系统加载JNI Lib的方式
1.Android系统加载JNI Lib的方式
Android系统加载JNI Lib的方式有如下两种:
- 通过JNI_OnLoad
- 如果JNI Lib没有定义JNI_OnLoad,则dvm调用dvmResolveNativeMethod进行动态解析
2. JNI_OnLoad方法
System.loadLibrary调用流程如下所示: 1
2
3
4
5
6
7
8
9
10
11
12System.loadLibrary->
   Runtime.loadLibrary->(Java)
     nativeLoad->(C: java_lang_Runtime.cpp)
       Dalvik_java_lang_Runtime_nativeLoad->
          dvmLoadNativeCode-> (dalvik/vm/Native.cpp)
              1) dlopen(pathName, RTLD_LAZY) (把.so mmap到进程空间,并把func等相关信息填充到soinfo中)
              2) dlsym(handle, "JNI_OnLoad")
              3) JNI_OnLoad->
                      RegisterNatives->
                         dvmRegisterJNIMethod(ClassObject* clazz, const char* methodName,
                                                const char* signature, void* fnPtr)->
                            dvmUseJNIBridge(method, fnPtr)->  (method->nativeFunc = func)1
2
3
4
5
6
7
8
9struct ClassObject : Object {  
    /* static, private, and <init> methods */  
    int             directMethodCount;  
    Method*         directMethods;  
    /* virtual methods defined in this class; invoked through vtable */  
    int             virtualMethodCount;  
    Method*         virtualMethods;  
}  
3.dvmResolveNativeMethod延迟解析机制
如果JNI Lib中没有JNI_OnLoad,即在执行System.loadLibrary时, 无法把此JNI Lib实现的函数在进程中的地址增加到ClassObject->directMethods。 则直到需要调用的时候才会解析这些javah风格的函数 。 这样的函数dvmResolveNativeMethod(dalvik/vm/Native.cpp)来进行解析, 其执行流程如下所示: 1
2
3
4
5
6
7
8
9
10void dvmResolveNativeMethod(const u4* args, JValue* pResult,
       const Method* method, Thread* self)  --> (Resolve a native method and invoke it.)
   1) void* func = lookupSharedLibMethod(method)(根据signature在所有已经打开的.so中寻找此函数实现)
           dvmHashForeach(gDvm.nativeLibs, findMethodInLib,(void*) method)->
                findMethodInLib(void* vlib, void* vmethod)->
                   dlsym(pLib->handle, mangleCM)
  2) dvmUseJNIBridge((Method*) method, func);
  3) (*method->nativeFunc)(args, pResult, method, self);  (调用执行)
2024.3.28更新
三、JNI_OnLoad动态注册流程与逆向分析
| 1 |  | 
大概就是这样子的:
 
gMethods变量是JNINativeMethod结构体,用于映射Java方法与C/C++函数的关系,其定义如下:
| 1 |  | 
所以就能从off_43CC8附近就能找到指向的函数。
JNINativeMethod结构体分析
| 1 |  | 
主要分析第二个参数signature:
java有自己的基本数据类型,但是java的数据类型是不能直接和c/c++交互的,为了统一这个问题,jni也 给出了一套数据类型的于Java一一对应。
| 字符 | c/c++类型 | Java类型 | 
|---|---|---|
| V | void | void | 
| Z | jboolean | boolean | 
| I | jint | int | 
| J | jlong | long | 
| D | jdouble | double | 
| F | jfloat | float | 
| B | jbyte | byte | 
| C | jchar | char | 
| S | jshort | short | 
| [I | jintArray | int[] | 
| [F | jfloatArray | float[] | 
| [B | jbyteArray | byte[] | 
| [C | jcharArray | char[] | 
| [S | jshortArray | short[] | 
| [D | jdoubleArray | double[] | 
| [J | jlongArray | long[] | 
| [Z | jbooleanArray | boolean[] | 
如果Java函数的参数是class,则以"L"开头,以";" 结尾中间是用"/" 隔开的包及类名。而其对应的C函数名的参数则为jobject.
举一个例子:Java是String类,其对应的类为jstring:
| 1 |  | 
如果JAVA函数位于一个嵌入类,则用$作为类名间的分隔符。
| 1 |  | 
对于signature的值,括号里面表示参数的类型,括号后面表示返回值。