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的方式有如下两种:

  1. 通过JNI_OnLoad
  2. 如果JNI Lib没有定义JNI_OnLoad,则dvm调用dvmResolveNativeMethod进行动态解析

2. JNI_OnLoad方法

System.loadLibrary调用流程如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
System.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)
JNI函数在进程空间中的起始地址被保存在ClassObject->directMethods中。·、
1
2
3
4
5
6
7
8
9
struct ClassObject : Object {  
/* static, private, and <init> methods */
int directMethodCount;
Method* directMethods;

/* virtual methods defined in this class; invoked through vtable */
int virtualMethodCount;
Method* virtualMethods;
}
此ClassObject通过gDvm.jniGlobalRefTable或gDvm.jniWeakGlobalRefLock获取。

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
10
void 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//第一步,实现JNI_OnLoad方法
JNIEXPORT jint JNI_OnLoad(JavaVM* jvm, void* reserved){
//第二步,获取JNIEnv
JNIEnv* env = NULL;
if(jvm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK){
return JNI_FALSE;
}
//第三步,获取注册方法所在Java类的引用
jclass clazz = env->FindClass("com/curz0n/MainActivity");
if (!clazz){
return JNI_FALSE;
}
//第四步,动态注册native方法
if(env->RegisterNatives(clazz, gMethods, sizeof(gMethods)/sizeof(gMethods[0]))){
return JNI_FALSE;
}
return JNI_VERSION_1_6;
}

大概就是这样子的:

1711613380558

gMethods变量是JNINativeMethod结构体,用于映射Java方法与C/C++函数的关系,其定义如下:

1
2
3
4
5
typedef struct {
const char* name; //动态注册的Java方法名
const char* signature; //描述方法参数和返回值
void* fnPtr; //指向实现Java方法的C/C++函数指针
} JNINativeMethod;

所以就能从off_43CC8附近就能找到指向的函数。

JNINativeMethod结构体分析

1
2
3
4
5
typedef struct {
const char* name; //动态注册的Java方法名
const char* signature; //描述方法参数和返回值
void* fnPtr; //指向实现Java方法的C/C++函数指针
} JNINativeMethod;

主要分析第二个参数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
2
Ljava/lang/String; String jstring
Ljava/net/Socket; Socket jobject

如果JAVA函数位于一个嵌入类,则用$作为类名间的分隔符。

1
例如 "(Ljava/lang/String;Landroid/os/Utils$UtilsStatus;)Z"

对于signature的值,括号里面表示参数的类型,括号后面表示返回值。