Android so的热升级尝试

Android so的热升级尝试,第1张

概述一、So的热升级尝试在Android代码中,加载so库是通过调用System.loadLibrary函数实现的。但和Android的许多特性一样,只提供了加载,而没有卸载和更换等功能。为了研究能否实现卸载和升级等功能,首先要了解清楚JNI

一、So的热升级尝试

在AndroID代码中,加载so库是通过调用System.loadlibrary函数实现的。但和AndroID的许多特性一样,只提供了加载,而没有卸载和更换等功能。为了研究能否实现卸载和升级等功能,首先要了解清楚JNI so加载的流程。

 

在以上流程中,使用dlopen加载so之后,会继续调用JNI_Onload函数,通过系统提供的RegisterNatives函数完成一些列初始化,向虚拟机注册so库提供的JNI函数。So库也可以不实现JNI_Onload函数,而是采用自动查找的方式。

AndroID虚拟机会在首次调用JNI函数时按照JNI规范的命名规则自动查找。通过分析AndroID代码,这种方法最终也会调用到上图中的dvmSetNativeFunc等函数,将函数地址保存到虚拟机中供下次调用。

二、卸载及重新加载

如果想要提供热升级的能力,首先要做的是关闭已打开的so文件。但AndroID虚拟机没有提供unloadlibrary这样的接口,因此需要我们自己自己实现。

根据上一节的分析,loadlibrary在native层加载文件使用的是dlopen,与之对应的系统接口是dlclose。而接下来的RegisterNatives由于没有对应的unRegister,我们暂且先放一放,看看卸载的效果再来处理。

卸载so

提供卸载能力的接口需要完成以下几项任务:

1、找到要卸载so的句柄;

2、调用JNI_OnUnload;

3、调用dlclose卸载。

如下便是我们写出的卸载函数:

voID JNICALL Java_com_example_Unloader_unload(jnienv* env,jobject obj){voID* handle = dlopen(“/data/data/com.example.unloader/lib/libtest.so”,RTLD_GLOBAL);if(!handle) return;LOGD(“unload so: 0x%x\n”,(unsigned int)handle);voID* symbol = dlsym(handle,“JNI_OnUnload”);if(symbol){OnLoadFunc func = (OnLoadFunc)symbol;JavaVM* jvm = 0;(*env)->GetJavaVM(env,&jvm);if(jvm)(*func)(jvm,0);}int result = dlclose(handle);LOGD(“unload result %d\n”,result);result = dlclose(handle);result = dlclose(handle);LOGD(“unload result %d\n”,result);}

其中dlclose调用了2次,因为函数内的dlopen会增加handle的引用计数。

卸载之后如果我们先尝试调用原来的JNI函数,会发生什么事呢?显而易见会出现crash。

 

究其原因,是由于so在加载或使用时已经在虚拟机中注册了JNI函数的地址,卸载后原地址变为非法地址,导致crash。那我们再重新加载so会发生什么呢?

重新加载so

分析代码可得知,由于so已经使用System.loadlibrary加载过,我们之前在卸载时也没有触及到JNI层,因此重复调用loadlibrary并不会重新加载so。我们可以按照dvmloadNativeCode的流程,在native层用dlopen重新加载so。

按照之前的分析,很容易就能写出加载函数:

voID JNICALL Java_com_example_Unloader_load(jnienv* env,jobject obj){voID* handle = dlopen(“/data/data/com.example.Unloader/lib/libtest.so”,RTLD_GLOBAL);if(!handle) return;LOGD(“load so: 0x%x\n”,“JNI_OnLoad”);if(symbol){OnLoadFunc func = (OnLoadFunc)symbol;JavaVM* jvm = 0;(*env)->GetJavaVM(env,0);}}

三、问题及解决

重新加载so后,再次调用原来的JNI函数。发现有时候会成功,但有时候也会crash。经过追踪后注意到,报错的函数地址和卸载前一样,但so加载的地址变化了。

 

由于dlopen加载so时,并不能保证每次都加载在同一地址上。即使能够加载到同一地址,如果升级造成so文件变化,那函数地址也是不准确的。所以要使新的so工作,那我们也必须要设法更新虚拟机已经保存的函数指针,将其指向新加载so的正确地址。

这时候就需要我们之前忽略的RegisterNatives登场了,这个函数可以用来手动注册JNI函数地址。让我们重复与第一节文字相似但含义不同的这段话:

在以上流程中,so库在使用dlopen加载后,还需要调用JNI_Onload函数,通过系统提供的RegisterNatives函数完成一些列初始化,向虚拟机注册新的JNI函数地址。

static JNINativeMethod gMethods[] = {{ “foo”,“()V”,(voID*)Java_com_tencent_example_foo },};// Register several native methods for one class.static int registerNativeMethods(jnienv* env,const char* classname,JNINativeMethod* gMethods,int numMethods){jclass clazz = (*env)->FindClass(env,classname);if (clazz == NulL) {return JNI_FALSE;}if ((*env)->RegisterNatives(env,clazz,gMethods,numMethods) < 0) {return JNI_FALSE;}return JNI_TRUE;}

使用RegisterNatives注册后,即使so的地址发生变化,也能够更新虚拟机中记录的函数地址。

 

本篇小结

如果想要在运行时更新so,则新的so文件必须要实现JNI_Onload函数,并且在JNI_Onload中调用系统提供的RegisterNatives注册所有的JNI函数,不能使用自动查找JNI函数名的方式。

四、其他问题

以上方案主要解决了so的卸载,重加载和JNI函数调用问题。但除了这些问题之外,so代码的细节上还有许多要注意的地方。

CRASH

卸载so后,除了JNI函数的指针,其它指向so地址的指针也都会失效,包括指向静态变量,常量,native函数的指针等。所有引用到该so地址的指针都需要更新。

内存和资源泄漏

native代码中可能存在各种分配内存和资源的行为,使用以上方法更新so前,如果没有仔细处理这些资源,就会丢失原指针,造成内存泄漏。

1、malloc/mmap/shmem等方式分配的内存。

2、socket,pipe,mutex,thread等各种系统资源。

3、使用NewGlobalRef分配并持有Java对象,丢失指针后会造成虚拟机的Java内存泄漏。

综上所述,对于所有可能丢失,造成泄露的资源,必须在卸载so前设法保存或删除。这些工作可以在卸载时调用的JNI_OnUnload中完成。

版权所属,禁止转载

总结

以上是内存溢出为你收集整理的Android so的热升级尝试全部内容,希望文章能够帮你解决Android so的热升级尝试所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1143619.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-31
下一篇 2022-05-31

发表评论

登录后才能评论

评论列表(0条)

保存