AndFix的原理是在加载补丁文件后,通过Native层使用指针替换的方式将老方法Method对象的方法指针替换成补丁包中新方法的,从而达到修复bug的目的。网上已经有很多文章都对Andfix的原理做了介绍,但绝大多数文章都是将重点放在了java层的介绍,对于native层进行的参数替换基本上都是一笔带过。而本文的重点就主要放在native层,本人现在对于art了解的不多,所以本文仅仅介绍Dalvik相关内容。
直接就从dalvik_replaceMethod代码开始吧。
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
//获取新函数的所属类名
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
//得到新函数的所属类在dalvik中的内存对象,非java对象
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
//class状态设为已初始化
clz->status = CLASS_INITIALIZED;
//根据反射的method对象得到dalvik内存中的对象,分别是旧函数和新函数
Method* meth = (Method*) env->FromReflectedMethod(src);
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;
}
这个方法大体是做了三件事:
1. 得到新方法所属类在Dalvik中的ClassObject,然后将其状态设为CLASS_INITIALIZED。为什么要在这里强制对状态赋值呢?类的使用需要加载,验证,准备,解析和初始化几个步骤。忽略中间三个步骤,类的加载阶段会为每个class生成对应的ClassObject和Method等Dalvik结构体,初始化阶段会真正的更改类的状态,成员变量等。只有初始化成功后,该class的status才会置为CLASS_INITIALIZED。
由于Andfix不是通过替换class,而是通过在Dalvik替换函数执行code来实现bug修复,所以新方法所属的类在实际运行中只有加载,没有初始化阶段。Dalvik中不允许执行一个未初始化类的方法,所以在这强制设置ClassObject的status。
2. 得到新旧两个方法在Dalvik中的Method Object
3. 将新方法的参数copy给旧方法的参数。
方法如此简单,但我相信阿里团队在实现该方案时肯定经历了很多jvm的代码分析。接下来,我们重点关注Method这个结构体,对应着java中Method,但他们不是一样的。后面我们会再介绍env->FromReflectedMethod的实现。
struct Method {
//方法所属类
ClassObject* clazz;
//访问属性,比如public、final、static等。
u4 accessFlags;
/*对于具体已经实现了的虚方法来说,这个是该方法在类虚函数表(vtable)中的偏移;对于未实现的纯接口方法来说,这个是该方法在对应的接口表(假设这个方法定义在类继承的第n+1个接口中,则表示iftable[n]->methodIndexArray)中的偏移;*/
u2 methodIndex;
//:该方法总共用到的寄存器个数,包含入口参数所用到的寄存器,还有方法内部自己所用到的其它本地寄存器;
u2 registersSize;
//当该方法要调用其它方法时,用作参数传递而使用的寄存器个数;
u2 outsSize;
//作为调用该方法时,参数传递而使用到的寄存器个数;
u2 insSize;
//方法的名称;
const char* name;
//方法对应的协议(也就是对该方法调用参数类型、顺序还有返回类型的描述)
DexProto prototype;
//方法对应协议的短表示法,一个字符代表一种类型;
const char* shorty;
//如果这个方法不是Native的话,则这里存放了指向方法具体的Dalvik指令的指针(这个变量指向的是实际加载到内存中的Dalvik指令,而不是在Dex文件中的)。如果这个方法是一个Dalvik虚拟机自带的Native函数(Internal Native)的话,则这个变量会是Null。如果这个方法是一个普通的Native函数的话,则这里存放了指向JNI实际函数机器码的首地址;
const u2* insns;
//这个变量记录了一些预先计算好的信息,从而不需要在调用的时候再通过方法的参数和返回值实时计算了,方便了JNI的调用,提高了调用的速度。如果第一位为1(即0x80000000),则Dalvik虚拟机会忽略后面的所有信息,强制在调用时实时计算;
int jniArgInfo;
//如果这个方法是一个Dalvik虚拟机自带的Native函数(Internal Native)的话,则这里存放了指向JNI实际函数机器码的首地址。如果这个方法是一个普通的Native函数的话,则这里将指向一个中间的跳转JNI桥(Bridge)代码;
DalvikBridgeFunc nativeFunc;
bool fastJni;
bool noRef;
bool shouldTrace;
//表示这个方法在每一个GC安全点上,有哪些寄存器其存放的数值是指向某个对象的引用,它主要是给Dalvik虚拟机做精确垃圾收集使用的。如果感兴趣的话,可以参看《Dalvik虚拟机中RegisterMap解析》这篇博客。
const RegisterMap* registerMap;
bool inProfile;
};
Method是如何被执行的呢。以下内容摘于http://blog.youkuaiyun.com/innost/article/details/50377905。
Davlik中,如果需要调用某个函数,会调用dvmCallMethod,有这么一段代码:
如果是native方法,则直接调用method中的nativeFunc。如果是java方法,则调用dvmInterpret。
大家注意self->interpSave.pc = method->insns。我们再描述一次insns的意义:如果这个方法不是Native的话,则这里存放了指向方法具体的Dalvik指令的指针(这个变量指向的是实际加载到内存中的Dalvik指令,而不是在Dex文件中的)。Andfix主要解决java层的方法替换,所以真正能让method替换的代码在这里。
前面我们提到了要介绍env的FromReflectedMethod方法。该方法能够从java的method对象得到dalvik中的struct Method。
在dalvik/vm/jni.c中找到这个方法实现:
/*
* Given a java.lang.reflect.Method or .Constructor, return a methodID.
*/
static jmethodID FromReflectedMethod(JNIEnv* env, jobject jmethod)
{
JNI_ENTER();
jmethodID methodID;
Object* method = dvmDecodeIndirectRef(env, jmethod);
methodID = (jmethodID) dvmGetMethodFromReflectObj(method);
JNI_EXIT();
return methodID;
}
这个函数的作用就是将反射的Method对象转换成真正的Method对象。我们来看看它是怎么做到的:
Method* dvmGetMethodFromReflectObj(Object* obj)
{
ClassObject *clazz = (ClassObject*)dvmGetFieldObject(obj,
gDvm.offJavaLangReflectMethod_declClass);
int slot = dvmGetFieldInt(obj, gDvm.offJavaLangReflectMethod_slot);
return dvmSlotToMethod(clazz, slot);
}
原来它是通过slot来获取到的!!
那接下来的问题就很清楚了,搞定了slot一切都好说。看看dvmSlotToMethod的实现:
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];
}
}
看起来,如果是virtualMethods,则slot >0,如果是正常函数,则slot < 0。
再来看看slot是在哪里设置的,仍然在dalvik/vm/reflect.c中:
static int methodToSlot(const Method* meth)
{
ClassObject* clazz = meth->clazz;
int slot;
if (dvmIsDirectMethod(meth)) {
slot = meth - clazz->directMethods;
slot = -(slot+1);
} else {
slot = meth - clazz->virtualMethods;
}
return slot;
}
注:AndFix支持2.3-7.0,但是不清楚是否有一些机型的坑在里面,毕竟jni层不像java层一样标准,从实现来说,是通过jni来替换方法,但是实现上更简洁直接,应用patch不需要重启。但由于从实现上直接跳过了类初始化,设置为初始化完毕,所以像是静态函数、静态成员、构造函数都会出现问题,复杂点的类Class.forname很可能直接就会挂掉。