Andfix源码分析

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很可能直接就会挂掉。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值