从AndFix源码分析JNI Hook热修复原理

AndFix的原理是在加载补丁文件后,通过Native层使用指针替换的方式将老方法Method对象的方法指针替换成补丁包中新方法的,从而达到修复bug的目的。

从AndFix源码分析JNI Hook热修复原理_第1张图片

AndFix具体的使用方法就不多介绍了,大家可以参考这篇文章,接下来我们直接从代码角度来分析整个修复的过程。

apkpatch工具解析

首先,进行热修复要有补丁包,AndFix的补丁包是由apkpatch工具生成的。apkpatch是一个jar包,将bug修复后的新apk包和已发布的线上apk包放在一起,通过这个jar工具可以生成查分补丁,但阿里并没有开源出来,我们可以用JD-GUI看下它的源码,版本1.0.3。
首先找到Main.class,位于com.euler.patch包下,找到Main()方法

    public static void main(final String[] args) {
        .....
        //根据脚本命令输入拿到参数        
       final ApkPatch apkPatch = new ApkPatch(from, to, name, out, keystore, password, alias, entry);
       apkPatch.doPatch();
    }

ApkPatch类的doPatch方法

    public void doPatch() {
        try {
        //生成smali文件夹
            final File smaliDir = new File(this.out, "smali");
            if (!smaliDir.exists()) {
                smaliDir.mkdir();
            }
            //新建diff.dex文件
            final File dexFile = new File(this.out, "diff.dex");
            //新建diff.apatch文件
            final File outFile = new File(this.out, "diff.apatch");
            //第一步,拿到两个apk文件对比,对比信息写入DiffInfo
            final DiffInfo info = new DexDiffer().diff(this.from, this.to);
            //第二步,将对比结果info写入.smali文件中,然后打包成dex文件
            this.classes = buildCode(smaliDir, dexFile, info);
            //第三步,将生成的dex文件写入jar包,并根据输入的签名信息进行签名,生成diff.apatch文件
            this.build(outFile, dexFile);
            //第四步,将diff.apatch文件重命名,结束
            this.release(this.out, dexFile, outFile);
        }
        catch (Exception e2) {
            e2.printStackTrace();
        }
    }

以上可以简单描述为两步

  1. 对比apk文件,得到需要的信息
  2. 将结果打包为apatch文件

对比apk文件

DexDiffer().diff()方法

    public DiffInfo diff(final File newFile, final File oldFile) throws IOException {
        //提取新apk的dex文件
        final DexBackedDexFile newDexFile = DexFileFactory.loadDexFile(newFile, 19, true);
        //提取旧apk的dex文件
        final DexBackedDexFile oldDexFile = DexFileFactory.loadDexFile(oldFile, 19, true);
        final DiffInfo info = DiffInfo.getInstance();
        boolean contains = false;
        for (final DexBackedClassDef newClazz : newDexFile.getClasses()) {
            final Set oldclasses = oldDexFile.getClasses();
            for (final DexBackedClassDef oldClazz : oldclasses) {

                if (newClazz.equals(oldClazz)) {
                     //对比class文件的变量
                    this.compareField(newClazz, oldClazz, info);
                    //对比class文件的方法,如果同一个类中没有相同的方法
                    //则判定为新增方法
                    this.compareMethod(newClazz, oldClazz, info);
                    contains = true;
                    break;
                }
            }
            if (!contains) {
                 //否则是新增的类
                info.addAddedClasses(newClazz);
            }
        }
        //返回包含diff信息的DiffInfo对象
        return info;
    }

其原理就是遍历两个apk文件的class来做差别信息。对比方法的过程中对比两个dex文件中同时存在的方法,如果方法实现不同则存储为修改过的方法;如果方法名不同,存储为新增的方法。

之后,将得到的diff信息写入smali文件,并且生成diff.dex文件。smali文件的命名以_CF.smali结尾,并且在修改的地方用自定义的Annotation(MethodReplace)标注,用于在替换之前查找修复的变量或方法,举个例子如下。

.method private getUserProfile()V
    .locals 2 #表示需要申请2个本地寄存器
    .annotation runtime Lcom/alipay/euler/andfix/annotation/MethodReplace;
        clazz = "com.abc.account.UserProfileActivity"
        method = "getUserProfile"
    .end annotation

编译成Java是这样的:

//生成的注解
@MethodReplace(clazz="com.abc.account.UserProfileActivity", method="getUserProfile")
private void getUserProfile() {
    ...
}

然后就是签名,打包,加密的流程,就不具体分析了。注意,apkPatch在生成.apatch补丁文件的时候会加入签名信息,并且会进行加密操作,在加载补丁的时候AndFix会验证签名信息是否正确。

AndFix加载补丁过程

补丁包下发给客户端后,AndFix就可以加载了,AndFix加载补丁有两种情况,一种是程序启动前补丁文件已经存在本地了,就在AndFix执行初始化完毕后加载补丁。另一种,程序启动之后(AndFix初始化之后)才开始下载补丁文件,待下载成功后,才去加载。

两种情况的区别仅仅是加载补丁的时机不同,加载前都要对AndFix进行初始化操作,加载的最终逻辑也都会走到com.alipay.euler.andfix.patch.PatchManager.loadPatch()方法中,loadPatch方法有三个重载实现,一个在AndFix初始化后调用,一个在下载补丁成功后调用,一个提供了自定义类加载器的实现。

见代码:

//PatchManager.java

/**
     * load patch,call when plugin be loaded. used for plugin architecture.
* * need name and classloader of the plugin * * @param patchName * patch name * @param classLoader * classloader */
public void loadPatch(String patchName, ClassLoader classLoader) { mLoaders.put(patchName, classLoader); Set patchNames; List classes; for (Patch patch : mPatchs) { patchNames = patch.getPatchNames(); if (patchNames.contains(patchName)) { classes = patch.getClasses(patchName); mAndFixManager.fix(patch.getFile(), classLoader, classes); } } } /** * 应用程序启动时调用 * load patch,call when application start * */ public void loadPatch() { mLoaders.put("*", mContext.getClassLoader());// wildcard Set patchNames; List classes; for (Patch patch : mPatchs) { patchNames = patch.getPatchNames(); for (String patchName : patchNames) { classes = patch.getClasses(patchName); mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(), classes); } } } /** * load specific patch * 补丁文件下载好后调用,参数是补丁文件 * @param patch * patch */ private void loadPatch(Patch patch) { Set patchNames = patch.getPatchNames(); ClassLoader cl; List classes; for (String patchName : patchNames) { if (mLoaders.containsKey("*")) { cl = mContext.getClassLoader(); } else { cl = mLoaders.get(patchName); } if (cl != null) { classes = patch.getClasses(patchName); mAndFixManager.fix(patch.getFile(), cl, classes); } } }

不管调用的机制如何,最终都是会走到AndFixManager.fix(File file, ClassLoader classLoader,
List classes) ;
方法
见代码:

/**
     * fix
     * 
     * @param file
     *            patch file
     * @param classLoader
     *            classloader of class that will be fixed
     * @param classes
     *            classes will be fixed
     */
    public synchronized void fix(File file, ClassLoader classLoader,
            List classes) {
        if (!mSupport) {
            return;
        }
        // 补丁文件的签名校验
        if (!mSecurityChecker.verifyApk(file)) {// security check fail
            return;
        }

        try {
            File optfile = new File(mOptDir, file.getName());
            boolean saveFingerprint = true;
            if (optfile.exists()) {
            // 如果本地已经存在补丁文件,则校验指纹信息(签名)
                // need to verify fingerprint when the optimize file exist,
                // prevent someone attack on jailbreak device with
                // Vulnerability-Parasyte.
                // btw:exaggerated android Vulnerability-Parasyte
                // http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html
                if (mSecurityChecker.verifyOpt(optfile)) {
                    saveFingerprint = false;
                } else if (!optfile.delete()) {
                    return;
                }
            }
            //将下载路径的pathch文件加载到data/data/packagename/file/  目录下,并返回一个DexFile对象
            final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
                    optfile.getAbsolutePath(), Context.MODE_PRIVATE);

            if (saveFingerprint) {
                mSecurityChecker.saveOptSig(optfile);
            }
            // 自定义一个类加载器,重写findClass方法
            ClassLoader patchClassLoader = new ClassLoader(classLoader) {
                @Override
                protected Class findClass(String className)
                        throws ClassNotFoundException {
                    Class clazz = dexFile.loadClass(className, this);
                    if (clazz == null
                            && className.startsWith("com.alipay.euler.andfix")) {
                        return Class.forName(className);// annotation’s class not found
                        // 没有找到类并且className以“com.alipay.euler.andfix”开头,则通过反射返回该类的实例
                    }
                    if (clazz == null) {
                        throw new ClassNotFoundException(className);
                    }
                    return clazz;
                }
            };
            Enumeration entrys = dexFile.entries();
            Class clazz = null;
            //遍历patch文件中的所有Entry
            while (entrys.hasMoreElements()) {
                String entry = entrys.nextElement();
                if (classes != null && !classes.contains(entry)) {
                    continue;// skip, not need fix
                }
                // 加载patch中的类
                clazz = dexFile.loadClass(entry, patchClassLoader);
                if (clazz != null) {
                    // 对这个class进行修复
                    fixClass(clazz, classLoader);
                }
            }
        } catch (IOException e) {
            Log.e(TAG, "pacth", e);
        }
    }
private void fixClass(Class clazz, ClassLoader classLoader) {
        // 反射找到该类中的所有方法
        Method[] methods = clazz.getDeclaredMethods();
        // MethodReplace是个注解
        MethodReplace methodReplace;
        String clz;
        String meth;
        for (Method method : methods) {
            //遍历该类的所有方法,找到有MethodReplace注解的就是需要替换的方法
            methodReplace = method.getAnnotation(MethodReplace.class);
            if (methodReplace == null)
                continue;
            clz = methodReplace.clazz();
            meth = methodReplace.method();
            //找到注解中标注的clazz和method属性后调用replaceMethod
            if (!isEmpty(clz) && !isEmpty(meth)) {
                replaceMethod(classLoader, clz, meth, method);
            }
        }
    }
/**
     * replace method
     * 
     * @param classLoader classloader
     * @param clz class
     * @param meth name of target method 
     * @param method source method
     */
    private void replaceMethod(ClassLoader classLoader, String clz,
            String meth, Method method) {
        try {

            String key = clz + "@" + classLoader.toString();
            // 根据key,查找缓存中的数据,该缓存记录了已经被修复过的class对象。
            Class clazz = mFixedClass.get(key);
            if (clazz == null) {// class not load
            //找不到则表示该class没有被修复过,则通过类加载器去加载。
                Class clzz = classLoader.loadClass(clz);
                // initialize target class
                // 通过C层,改写accessFlags,把需要替换的类的所有方法(Field)改成了public
                clazz = AndFix.initTargetClass(clzz);
            }
            if (clazz != null) {// initialize class OK
                mFixedClass.put(key, clazz);
                // 反射得到修复前老的Method对象
                Method src = clazz.getDeclaredMethod(meth,
                        method.getParameterTypes());
                AndFix.addReplaceMethod(src, method);
            }
        } catch (Exception e) {
            Log.e(TAG, "replaceMethod", e);
        }
    }

最终,AndFix.addReplaceMethod()调用了native的replaceMethod方法。

/**
     * replace method's body
     * 
     * @param src
     *            source method
     * @param dest
     *            target method
     * 
     */
    public static void addReplaceMethod(Method src, Method dest) {
        try {
            replaceMethod(src, dest);
            initFields(dest.getDeclaringClass());
        } catch (Throwable e) {
            Log.e(TAG, "addReplaceMethod", e);
        }
    }

    private static native void replaceMethod(Method dest, Method src);

可以看到,最后把老方法的Method对象和新方法的Method对象作为参数传入了C层,Java层的代码到此结束,接下来看Native层

Native层分析

//andfix.cpp
static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,
        jobject dest) {
    if (isArt) {
        art_replaceMethod(env, src, dest);
    } else {
        dalvik_replaceMethod(env, src, dest);
    }
}

看到dalvik和art的替换逻辑不一样,接下来,我们分析art的

//art_method_replace.cpp
extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(
        JNIEnv* env, jobject src, jobject dest) {
    if (apilevel > 22) {
        replace_6_0(env, src, dest);
    } else if (apilevel > 21) {
        replace_5_1(env, src, dest);
    } else {
        replace_5_0(env, src, dest);
    }
}

看到根据系统版本又分出了几个分支,我们看5.0的

//art_method_replace_5_0.cpp
void replace_5_0(JNIEnv* env, jobject src, jobject dest) {
    // 通过jni.h中的FromReflectedMethod方法反射得到源方法和替换方法的ArtMethod指针(ArtMethod的数据结构定义在头文件中,接下来会分析)
    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
    // 替换用于检查递归调用的线程id
    dmeth->declaring_class_->clinit_thread_id_ =
            smeth->declaring_class_->clinit_thread_id_;
    // 把目标方法所在类的初始化状态值设置成源方法的状态值-1 
    dmeth->declaring_class_->status_ = smeth->declaring_class_->status_-1;

// 把原方法的各种属性都改成补丁方法的
    smeth->declaring_class_ = dmeth->declaring_class_;
    smeth->access_flags_ = dmeth->access_flags_;
    smeth->frame_size_in_bytes_ = dmeth->frame_size_in_bytes_;
    smeth->dex_cache_initialized_static_storage_ =
            dmeth->dex_cache_initialized_static_storage_;
    smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
    smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
    smeth->vmap_table_ = dmeth->vmap_table_;
    smeth->core_spill_mask_ = dmeth->core_spill_mask_;
    smeth->fp_spill_mask_ = dmeth->fp_spill_mask_;
    smeth->mapping_table_ = dmeth->mapping_table_;
    smeth->code_item_offset_ = dmeth->code_item_offset_;

    // 最重要的两个方法指针替换,下面两个entry_point指针代表了ART运行时执行方法的两种模式(compiled_code,interpreter),Andfix根据方法不同的调用机制通过这两个指针做方法替换
    //方法执行方式为本地机器指令的指针入口
    smeth->entry_point_from_compiled_code_ =
            dmeth->entry_point_from_compiled_code_;
    // 方法执行方式为解释执行的指针入口
    smeth->entry_point_from_interpreter_ = dmeth->entry_point_from_interpreter_;


    smeth->native_method_ = dmeth->native_method_;
    smeth->method_index_ = dmeth->method_index_;
    smeth->method_dex_index_ = dmeth->method_dex_index_;

    LOGD("replace_5_0: %d , %d", smeth->entry_point_from_compiled_code_,
            dmeth->entry_point_from_compiled_code_);

}

在头文件art_5_0.h中找到了ArtMethod的定义,也看到了上面代码中替换的所有变量的定义

// art_5_0.h
class ArtMethod: public Object {
public:
    // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
    // The class we are a part of
    Class* declaring_class_;

    // short cuts to declaring_class_->dex_cache_ member for fast compiled code access
    void* dex_cache_initialized_static_storage_;

    // short cuts to declaring_class_->dex_cache_ member for fast compiled code access
    void* dex_cache_resolved_methods_;

    // short cuts to declaring_class_->dex_cache_ member for fast compiled code access
    void* dex_cache_resolved_types_;

    // short cuts to declaring_class_->dex_cache_ member for fast compiled code access
    void* dex_cache_strings_;

    // Access flags; low 16 bits are defined by spec.
    uint32_t access_flags_;

    // Offset to the CodeItem.
    uint32_t code_item_offset_;

    // Architecture-dependent register spill mask
    uint32_t core_spill_mask_;

    //  compiled_code调用方式,本地机器指令入口
    // Compiled code associated with this method for callers from managed code.
    // May be compiled managed code or a bridge for invoking a native method.
    // TODO: Break apart this into portable and quick.
    const void* entry_point_from_compiled_code_;

    // 通过interpreter方式调用方法 解释执行入口
    // Called by the interpreter to execute this method.
    void* entry_point_from_interpreter_;

    // Architecture-dependent register spill mask
    uint32_t fp_spill_mask_;

    // Total size in bytes of the frame
    size_t frame_size_in_bytes_;

    // Garbage collection map of native PC offsets (quick) or dex PCs (portable) to reference bitmaps.
    const uint8_t* gc_map_;

    // Mapping from native pc to dex pc
    const uint32_t* mapping_table_;

    // Index into method_ids of the dex file associated with this method
    uint32_t method_dex_index_;

    // For concrete virtual methods, this is the offset of the method in Class::vtable_.
    //
    // For abstract methods in an interface class, this is the offset of the method in
    // "iftable_->Get(n)->GetMethodArray()".
    //
    // For static and direct methods this is the index in the direct methods table.
    uint32_t method_index_;

    // The target native method registered with this method
    const void* native_method_;

    // When a register is promoted into a register, the spill mask holds which registers hold dex
    // registers. The first promoted register's corresponding dex register is vmap_table_[1], the Nth
    // is vmap_table_[N]. vmap_table_[0] holds the length of the table.
    const uint16_t* vmap_table_;

    static void* java_lang_reflect_ArtMethod_;
};
}

代码分析到这里,思路也大致有了,补丁被andfix框架加载后,先通过一系列的校验和解析,最终会得到两个Method对象(源方法和目的方法)传给jni层,jni层根据虚拟机版本去实现相应的替换逻辑,参照art5.0版本的代码,发现andfix通过ArtMethod指针将源方法Method对象的成员变量转换成为新方法的成员变量,从而实现方法替换的功能。(我的个人理解,如有错误还请指正)

而上面说的ART执行方法的两种方式是什么呢?

ART虚拟机引入了AOT(Ahead Of Time)机制,也就是在app运行之前(程序安装时),系统会把apk包中的classes.dex文件通过工具dex2oat翻译成本地机器指令,最终得到一个ELF格式的OAT文件。app启动时会将OAT文件加载到内存中,当ART需要执行某个方法时,就可以直接在OAT中查找该方法对应的本地机器指令来执行。

上面说的ART运行方法是直接将其本地机器指令交给CPU执行,是默认的执行方式。ART另一种执行方法的方式是像Dalvik虚拟机一样,将其DEX字节码交给解释器执行。

而具体的执行方案,是由dex字节码翻译成本地机器指令的后端所决定的(分为Quick类型和Portable类型,默认Quick类型)。Quick类型对应本地机器指令,Portable类型对应解释执行。

具体如下图所示
从AndFix源码分析JNI Hook热修复原理_第2张图片

这两种方法调用的方案正好对应了刚才代码中ArtMethod类的两个entry_point指针(entry_point_from_compiled_code_entry_point_from_interpreter_

但是,我的疑问来了,在默认情况下,app安装时已经在OAT文件中将老方法对应的本地机器指令生成了,那么ART要执行AndFix替换后的新方法必然会去找新方法对应的本地机器指令,那么新方法对应的机器指令是如何生成的呢?

带着问题,我又去研究了一下ART加载OAT文件的过程。发现一个应用程序的classes.dex文件经过ART运行时编译后,得到一个OAT主执行文件。不过,该应用程序也可以在运行时动态加载dex文件(热修复加载补丁)。这些动态加载的dex文件在加载的时候同样会被翻译成OAT再运行,但动态加载的dex文件不属于主执行文件。

如果我参考的资料没有错误的话,就意味着补丁包中的dex文件在被加载时就被ART翻译成OAT文件,新的方法也生成了对应的本地机器指令。那么我们在ART上使用AndFix进行热修复替换方法时,因为替换了Method对象中的成员变量(entry_point指针),所以新方法所指向的本地机器指令也一起被替换了。

总结

AndFix热补丁原理就是,通过加载差分补丁,把需要替换的方法注入到native层,然后通过替换新老方法的函数指针,从而达到bug修复目的,但需要注意的是,因为Andfix是动态的,跳过了类的初始化,所以对于静态方法、静态成员变量、构造方法或者class.forname()的处理可能会有问题,也不支持新增成员变量和修改成员变量。


参考资料
Android运行时ART加载OAT文件的过程分析
Android运行时ART加载类和方法的过程分析
Android运行时ART执行类方法的过程分析
Android热补丁之AndFix原理解析
各大热补丁方案分析和比较
AndFix项目地址


你可能感兴趣的:(jni,bug,补丁)