AndFix各个版本的改动以及原理

搞Android的工程师对于AndFix大家都应该耳熟能详了,我就不多做介绍了,下面简单的介绍下我接触过的AndFix的各个版本方法替换的大概原理

  1. 第一个版本的AndFix分为Dalvik与ART两个大的替换方向只要是针对于Android虚拟机来做的,Dalvik更多是基于解释执行的,ART如果开启了AOP的话那么思路会有所不同

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);
    Method* target = (Method*) env->FromReflectedMethod(dest);
    LOGD("dalvikMethod: %s", meth->name);

    meth->jniArgInfo = 0x80000000;
    meth->accessFlags |= ACC_NATIVE;  // 把替换的方法变成为Native方法

    int argsSize = dvmComputeMethodArgsSize_fnPtr(meth);
    if (!dvmIsStaticMethod(meth))
        argsSize++;
    meth->registersSize = meth->insSize = argsSize;
    meth->insns = (void*) target;

    meth->nativeFunc = dalvik_dispatcher;  //替换掉nativeFunc的函数
}

extern void dalvik_setFieldFlag(JNIEnv* env, jobject field) {
    Field* dalvikField = (Field*) env->FromReflectedField(field);
    dalvikField->accessFlags = dalvikField->accessFlags & (~ACC_PRIVATE)
            | ACC_PUBLIC;
    LOGD("dalvik_setFieldFlag: %d ", dalvikField->accessFlags);
}

static bool dvmIsPrimitiveClass(const ClassObject* clazz) {
    return clazz->primitiveType != PRIM_NOT;
}

static void dalvik_dispatcher(const u4* args, jvalue* pResult,
        const Method* method, void* self) {
    ClassObject* returnType;
    jvalue result;
    ArrayObject* argArray;

    LOGD("dalvik_dispatcher source method: %s %s", method->name,
            method->shorty);
    Method* meth = (Method*) method->insns;
    meth->accessFlags = meth->accessFlags | ACC_PUBLIC;
    LOGD("dalvik_dispatcher target method: %s %s", method->name,
            method->shorty);

    returnType = dvmGetBoxedReturnType_fnPtr(method);
    if (returnType == NULL) {
        assert(dvmCheckException_fnPtr(self));
        goto bail;
    }
    LOGD("dalvik_dispatcher start call->");

    if (!dvmIsStaticMethod(meth)) {
        Object* thisObj = (Object*) args[0];
        ClassObject* tmp = thisObj->clazz;
        thisObj->clazz = meth->clazz;
        argArray = boxMethodArgs(meth, args + 1);
        if (dvmCheckException_fnPtr(self))
            goto bail;

        dvmCallMethod_fnPtr(self, (Method*) jInvokeMethod,
                dvmCreateReflectMethodObject_fnPtr(meth), &result, thisObj,
                argArray);

        thisObj->clazz = tmp;
    } else {
        argArray = boxMethodArgs(meth, args);
        if (dvmCheckException_fnPtr(self))
            goto bail;

        dvmCallMethod_fnPtr(self, (Method*) jInvokeMethod,
                dvmCreateReflectMethodObject_fnPtr(meth), &result, NULL,
                argArray);
    }
    if (dvmCheckException_fnPtr(self)) {
        Object* excep = dvmGetException_fnPtr(self);
        jni_env->Throw((jthrowable) excep);
        goto bail;
    }

    if (returnType->primitiveType == PRIM_VOID) {
        LOGD("+++ ignoring return to void");
    } else if (result.l == NULL) {
        if (dvmIsPrimitiveClass(returnType)) {
            jni_env->ThrowNew(NPEClazz, "null result when primitive expected");
            goto bail;
        }
        pResult->l = NULL;
    } else {
        if (!dvmUnboxPrimitive_fnPtr(result.l, returnType, pResult)) {
            char msg[1024] = { 0 };
            snprintf(msg, sizeof(msg) - 1, "%s!=%s\0",
                    ((Object*) result.l)->clazz->descriptor,
                    returnType->descriptor);
            jni_env->ThrowNew(CastEClazz, msg);
            goto bail;
        }
    }

    bail: dvmReleaseTrackedAlloc_fnPtr((Object*) argArray, self);
}

首先要做的是拿到Dalvik虚拟机的暴露出来的可以被操作的函数(通过dlopen与dvm_dlsym来进行操作熟悉Linux动态库的应该知道),举个例子:

void* dvm_hand = dlopen("libdvm.so", RTLD_NOW);
    if (dvm_hand) {
        dvmCallMethod_fnPtr = dvm_dlsym(dvm_hand,
                apilevel > 10 ?
                        "_Z13dvmCallMethodP6ThreadPK6MethodP6ObjectP6JValuez" :
                        "dvmCallMethod");

//拿到了虚拟机中的dvmCallMethod方法,此方法可以作为动态调用的基础

jInvokeMethod = env->GetMethodID(clazz, "invoke",
                "(Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");

//这里和虚拟机没关系,这里是通过JNI拿到了反射的invoke方法为下面做准备

其次是把要被替换的方法变为Native方法,因为由于虚拟机的执行顺序如果是JAVA方法会被解释执行(解释执行里面的字节码指令),如果是Native方法那么就会直接调用(Method 结构体里面的nativeFunc就是函数的地址),这里再覆盖他的nativeFunc为自定义的dalvik_dispatcher方法,这里面最核心的就是dvmCallMethod_fnPtr这个指针函数的调用,

dvmCallMethod_fnPtr(self, (Method*) jInvokeMethod,
                dvmCreateReflectMethodObject_fnPtr(meth), &result, thisObj,argArray);

这里通过这个JVM函数动态调用了JAVA的invoke的反射方法,方法体就是替换的函数内容dvmCreateReflectMethodObject_fnPtr(meth), 总结就是让被替换方法走Native的方式然后在JNI层去Hook住nativeFun,然后操作JVM的调用函数去调用反射的方式去执行一个新的JAVA方法

  1. ART的方法替换核心代码:
art::mirror::ArtMethod* smeth =
            (art::mirror::ArtMethod*) env->FromReflectedMethod(src);

art::mirror::ArtMethod* dmeth =
            (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);

    dmeth->declaring_class_->class_loader_ =
            smeth->declaring_class_->class_loader_; //for plugin classloader
    dmeth->declaring_class_->clinit_thread_id_ =
            smeth->declaring_class_->clinit_thread_id_;
    dmeth->declaring_class_->status_ = smeth->declaring_class_->status_-1;

    smeth->declaring_class_ = dmeth->declaring_class_;
    smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
    smeth->access_flags_ = dmeth->access_flags_;
    smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
    smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
    smeth->method_index_ = dmeth->method_index_;
    smeth->dex_method_index_ = dmeth->dex_method_index_;

    smeth->ptr_sized_fields_.entry_point_from_interpreter_ =
            dmeth->ptr_sized_fields_.entry_point_from_interpreter_;

    smeth->ptr_sized_fields_.entry_point_from_jni_ =
            dmeth->ptr_sized_fields_.entry_point_from_jni_;
    smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
            dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;

    LOGD("replace_6_0: %d , %d",
            smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
            dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);

看起来非常简单,就是在JNI层去找到被替换的与替换的ArtMethod* 结构体(每一个Java方法在art中都对应着一个ArtMethod),然后把这个结构体里面的内容全部替换就OK了就是这么简单,然后整个函数就会被替换了。

  1. 由于ART的方法非常的简单,所以新版本的AndFix中吸取了里面的精华部分,新版本的AndFix也分为Dalvik,ART两个方式

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);
    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;
}

就问你服没?,在JNI层里面获取被替换方法以及替换方法的Method(Method在Dalvik里面代表一个JAVA方法的结构体),然后整体替换里面的内容即可,这里面注意

meth->insns = target->insns;
    meth->nativeFunc = target->nativeFunc;

是不是又看见这个nativeFunc了?但是思路不同了这两句指的是如果是普通JAVA方法那么insns替换,如果是native方法那么nativeFunc替换原理上面应该是JAVA,Native方法替换都支持的

  1. 新版本的ART代码没有什么改变,
void replace_7_0(JNIEnv* env, jobject src, jobject dest) {
    art::mirror::ArtMethod* smeth =
            (art::mirror::ArtMethod*) env->FromReflectedMethod(src);

    art::mirror::ArtMethod* dmeth =
            (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);

//  reinterpret_cast(smeth->declaring_class_)->class_loader_ =
//          reinterpret_cast(dmeth->declaring_class_)->class_loader_; //for plugin classloader
    reinterpret_cast(dmeth->declaring_class_)->clinit_thread_id_ =
            reinterpret_cast(smeth->declaring_class_)->clinit_thread_id_;
    reinterpret_cast(dmeth->declaring_class_)->status_ =
            reinterpret_cast(smeth->declaring_class_)->status_ -1;
    //for reflection invoke
    reinterpret_cast(dmeth->declaring_class_)->super_class_ = 0;

    smeth->declaring_class_ = dmeth->declaring_class_;
    smeth->access_flags_ = dmeth->access_flags_  | 0x0001;
    smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
    smeth->dex_method_index_ = dmeth->dex_method_index_;
    smeth->method_index_ = dmeth->method_index_;
    smeth->hotness_count_ = dmeth->hotness_count_;

    smeth->ptr_sized_fields_.dex_cache_resolved_methods_ =
            dmeth->ptr_sized_fields_.dex_cache_resolved_methods_;
    smeth->ptr_sized_fields_.dex_cache_resolved_types_ =
            dmeth->ptr_sized_fields_.dex_cache_resolved_types_;

    smeth->ptr_sized_fields_.entry_point_from_jni_ =
            dmeth->ptr_sized_fields_.entry_point_from_jni_;
    smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
            dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;

    LOGD("replace_7_0: %d , %d",
            smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
            dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);

}

但是有一个问题


image.png

看见没只支持到7.0,这个是为什么呢?

因为Andfix是把底层结构强转为了art::mirror::ArtMethod,但这里的art::mirror::ArtMethod并非等同于app运行时所在设备虚拟机底层的art::mirror::ArtMethod,而是Andfix自己构造的art::mirror::ArtMethod(就是上图对应的那些头文件里面的结构体)

但是由于Android是开源的,各个手机厂商都可以对代码进行改造,而Andfix里ArtMethod的结构是根据公开的Android源码中的结构写死的。如果某个厂商对这个ArtMethod结构体进行了修改,就和原先开源代码里的结构不一致,那么在这个修改过了的设备上,替换机制就会出问题。

而且大家也看到了AndFix现在处于不维护的状态了(Android都出到了10了),那么怎么办呢,我在Sophix上面找到了方案(这是铁了心要抛弃Andfix了?)

image.png

怎么整体替换呢?在art里面初始化一个类的时候会给这个类的所有方法分配空间
有direct方法和virtual方法(direct方法包含static方法和所有不可继承的对象方法。而virtual方法就是所有可以继承的对象方法了)就是利用ART虚拟机中分配类方法的的地址是连续Alloc出来的,那么只要计算两个方法的地址空间就能得出一个ArtMethod* 结构体的size,得到这个size就利用memcpy整体拷贝不就OK了是吗?

size_t firMid = (size_t) env->GetStaticMethodID(nativeStructsModelClazz, "f1", "()V");
    size_t secMid = (size_t) env->GetStaticMethodID(nativeStructsModelClazz, "f2", "()V");
    size_t methSize = secMid - firMid;

memcpy(smeth, dmeth, methSize);

值得一提的是,由于忽略了底层ArtMethod结构的差异,对于所有的Android版本都不再需要区分,而统一以memcpy实现即可,代码量大大减少。即使以后的Android版本不断修改ArtMethod的成员,只要保证ArtMethod数组仍是以线性结构排列,就能直接适用于Android 8.0、9.0等新版本,无需再针对新的系统版本进行适配了。

有耐心的小伙伴可以参考这篇文章:https://yq.aliyun.com/articles/74598?t=t1

最后大家也看出来了,方法替换核心还是在JVM上面动手脚,如果想再深入的话首先要对于JVM有足够多的了解,大家加油···

你可能感兴趣的:(AndFix各个版本的改动以及原理)