先谈谈个人对Dexposed的理解
dexposed是阿里推出的一个AOP框架,github官网地址https://github.com/alibaba/dexposed, 从官网描述来看最初定位是用来解决面向切面编程的一些问题,不过因为具备java方法hook的效果,所以在刚推出时也被开发者当作代码热修复框架。该框架实现原理绝大部分参考了大名鼎鼎的Xposed。不过在我看来DexPosed和Xposed相比是巨大的进步和思维的创新,因为Xposed的使用需要root,因此只具备研究价值而不具备应用价值(黑灰产等不符合价值观的应用不在我们考虑范围之内)。一个需要root才能使用的框架只适合开发者在做一些研究和分析的时候使用,比如做逆向分析研究某个App某个核心函数的关键参数的值,可以把该App放到自己的root手机上,再使用Xposed框架写个hook模块hook这个函数即可。而Dexposed从设计之初就只打算支持hook自己App所在的进程,所以无需root。比如该框架可以被用作热修复用,也可以引入到开发者的App做一些AOP操作。换而言之就是DexPosed不仅具备研究价值还具备应用价值。不过因为Dexposed对art运行时支持较差,所以现在来看的话应用价值也不是很大。但是我们可以学些下其原理实现部分,关于Dexposed的实现原理网上有很多文章进行讲解,因此不做过多赘述。而且Dexposed实现原理部分大部分参考自Xposed,所以你也可以直接去看Xposed的实现原理,这个原理的理论依据我在前面实际上已经提到过,就是native方法注册和执行流程的实现原理。不清楚的可以先看下:【native层方法替换热修复原理】之native方法在dalvik虚拟机中的执行流程
也就是Dexposed参考借鉴了Xposed,而Xposed是参考借鉴了dalvik虚拟机JNI模块的实现原理。
AndFix介绍
AndFix是阿里推出的一个安卓热修复框架,github官网地址:https://github.com/alibaba/AndFix。从官网描述来看其定位就是用来进行热修复的(AndFix is a library that offer hot-fix for Android App.)。而Dexposed官网定位是用来进行AOP操作。AndFix和其他厂商的热修复方案相比其最大的特点是即时生效,无需重启。原因是AndFix做的是方法替换,处理粒度是方法,而其他厂商本质上做的是类替换,处理粒度是类。不管时JVM虚拟机还是安卓自有的虚拟机,一个类被加载到虚拟机中之后,再次使用时是不会重新加载的,所以如果不重启原先的bug类还在虚拟机中,就不会去加载同名的修复bug后的类。而方法替换是在原来类的基础上进行的,而且方法每次调用都会执行,所以当替换后再调用就会使用替换后的方法。
实现原理解析
AndFix的实现原理我就不做过多介绍,在阿里官方出品的《深入探索Android热修复技术原理》这本书中已经做了较为详细的解释,我这里先大概提一下:
- 使用DexFile.loadDex方法加载补丁dex,获取到补丁Dex对应的DexFile对象dexFile
- 使用dexFile.loadClass加载类的时候,传入自定义的ClassLoader,在该ClassLoader中使用dexFile.loadClass来加载类。
- 在加载类的过程中会通过反射判断该类所有声明的方法是否包含需要热修复的注解,如果包含该注解,说明该方法是需要被热修复方法
- 对于需要热修复的方法传入获取到原bug方法src和修复bug后的方法dest对应的java层反射获取的Method对象给native层方法替换函数,完成这2个方法在native层对应的Mehotd/ArtMethod对象的替换
具体的替换逻辑就是把这2个java层方法对应在虚拟机运行时的Method/ArtMethod数据结构中的每一个元素的值进行替换。那么这里有2个问题值得思考:
- 为什么替换这2个数据结构的内容就能达到热修复效果呢?
- 通过java层反射得到的Method对象如何能够找到其对应的虚拟机运行时的Method/ArtMethod数据结构?
第一个问题,我们在分析【native层方法替换热修复原理】之native方法在dalvik虚拟机中的执行流程时其实已经讲过。这里还是简单提下,以dalvik虚拟机为例:
当虚拟机完成类加载工作之后,解释器会对已经装载的代码进行检查,验证符合虚拟机规范格式之后,最终调用dvmInterpret函数初始化解释器斌开始解释执行字节码,dvmInterpret函数是解释器的入口函数。该函数定义如下:
dvmInterpret(Thread* self, const Method* method, JValue* pResult)该函数的第二个参数即是Mehtod类型的指针,所以Method对象是dalvik虚拟机解释器的重要输入。对方法的执行本质上就是对Method数据结构的操作,Art下类似本质上是对ArtMethod进行操作。
所以只要我们替换了这2个数据结构在运行时的内容,那么执行的逻辑就是替换后的内容(更详细原理介绍请参看【native层方法替换热修复原理】之native方法在dalvik虚拟机中的执行流程)。
对于第二个问题如何通过反射时得到的Method对象获取到运行时对于的Method和ArtMethod对象呢?我们先看下AndFix是如何做的,以dalvik为例,如下是方法替换的代码:
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
clz->status = CLASS_INITIALIZED;
Method* meth = (Method*) env->FromReflectedMethod(src); //根据src得到Method对象的指针
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);
// meth->clazz = target->clazz;
meth->accessFlags |= ACC_PUBLIC;
meth->methodIndex = target->methodIndex;
meth->jniArgInfo = target->jniArgInfo;
meth->registersSize = target->registersSize;
meth->outsSize = target->outsSize;
meth->insSize = target->insSize;
meth->prototype = target->prototype;
meth->insns = target->insns;
meth->nativeFunc = target->nativeFunc;
}
代码中我注释的那一行就是根据java层反射获取的Method对象src获取native层Method指针meth的逻辑。因此我们看下FromReflectedMethod函数的定义,该函数位于安卓源码的dalvik\vm\oo\Jni.cpp中。
/*
* Given a java.lang.reflect.Method or .Constructor, return a methodID.
*/
static jmethodID FromReflectedMethod(JNIEnv* env, jobject jmethod) {
ScopedJniThreadState ts(env);
Object* method = dvmDecodeIndirectRef(ts.self(), jmethod);
return (jmethodID) dvmGetMethodFromReflectObj(method);
}
从该函数的注释上可以看到其作用是根据一个反射的方法或构造方法得到其对应的methodID,所以很明显这本质上就是一个JNI调用。另外从返回值来看应该是jmethdoID才对,但是我们希望得到的是Method数据结构,所以jmethodID这个数据结构的第一个成员一定是一个Method对象指针。另外我们看下dvmGetMethodFromReflectObj的实现,该函数位于安卓源码的\dalvik\vm\Reflect.cpp中。
/*
* JNI reflection support: convert reflection object to Method ptr.
*/
Method* dvmGetMethodFromReflectObj(Object* obj)
{
ClassObject* clazz;
int slot;
if (obj->clazz == gDvm.classJavaLangReflectConstructor) {
clazz = (ClassObject*)dvmGetFieldObject(obj,
gDvm.offJavaLangReflectConstructor_declClass);
slot = dvmGetFieldInt(obj, gDvm.offJavaLangReflectConstructor_slot);
} else if (obj->clazz == gDvm.classJavaLangReflectMethod) {
clazz = (ClassObject*)dvmGetFieldObject(obj,
gDvm.offJavaLangReflectMethod_declClass);
slot = dvmGetFieldInt(obj, gDvm.offJavaLangReflectMethod_slot);
} else {
assert(false);
return NULL;
}
/* must initialize the class before returning a method ID */
if (!dvmInitClass(clazz))
return NULL;
return dvmSlotToMethod(clazz, slot);
}
从注释可以看到,其作用是根据一个反射得到的Method对象来获取native层对应的Method指针,而且可以看到是通过dvmSlotToMethod这个函数来得到的,如果你对Dexposed或Xposed的hook原理比较清楚的话,对这个函数应该比较熟悉,其作用是从一个ClassObject类型的对象中根据slot序号取对应的Method指针。
/*
* Convert a slot number to a method pointer.
*/
Method* dvmSlotToMethod(ClassObject* clazz, int slot)
{
if (slot < 0) {
slot = -(slot+1);
assert(slot < clazz->directMethodCount);
return &clazz->directMethods[slot];
} else {
assert(slot < clazz->virtualMethodCount);
return &clazz->virtualMethods[slot];
}
}
那么这个slot是什么呢?能在java层获取到吗?实际上这个slot就是java层反射Method中的一个成员变量,所以在java层完全能够获取到,比如Xposed就是这么做的,如下是Xposedjava层hook的一段代码:
public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) {
...省略若干细节代码...
if (newMethod) {
Class<?> declaringClass = hookMethod.getDeclaringClass();
int slot;
Class<?>[] parameterTypes;
Class<?> returnType;
if (runtime == RUNTIME_ART) {
slot = 0;
parameterTypes = null;
returnType = null;
} else if (hookMethod instanceof Method) {
slot = getIntField(hookMethod, "slot"); //反射获取slot的值
parameterTypes = ((Method) hookMethod).getParameterTypes();
returnType = ((Method) hookMethod).getReturnType();
} else {
slot = getIntField(hookMethod, "slot"); //反射获取slot的值
parameterTypes = ((Constructor<?>) hookMethod).getParameterTypes();
returnType = null;
}
AdditionalHookInfo additionalInfo = new AdditionalHookInfo(callbacks, parameterTypes, returnType);
hookMethodNative(hookMethod, declaringClass, slot, additionalInfo);
}
return callback.new Unhook(hookMethod);
}
总结
到这里我们就把native层方法替换热修复系列的难点给讲解透彻了,现在做个总结:
- native层方法替换热修复的关键是把java层通过反射获取到的Method对象对应在native层的Method/ArtMethod数据结构的成员的值给替换掉
- 使用JNI接口FromReflectedMethod即可依据java层反射获取的Method对象来找到其对应的native层的Method/ArtMethod对象。
- FromReflectedMethod本质上仍然是使用dvmSlotToMethod来得到native层Mehotd对象指针,所以如果能够在java层就得到该函数对应的slot的话可以直接使用dvmSlotToMethod函数得到其对应的native层Mehotd对象指针,像Xposed那样。
文章评论