各大热补丁方案分析和比较

来源:互联网 发布:前端转后端 知乎 编辑:程序博客网 时间:2024/06/05 07:19

最近开源界涌现了很多热补丁项目,但从方案上来说,主要包括Dexposed、AndFix、ClassLoader(来源是原QZone,现淘宝的工程师陈钟,在15年年初就已经开始实现)三种。前两个都是阿里巴巴内部的不同团队做的(淘宝和支付宝),后者则来自腾讯的QQ空间团队。

开源界往往一个方案会有好几种实现(比如ClassLoader方案已经有不下三种实现了),但这三种方案的原理却徊然不同,那么让我们来看看它们三者的原理和各自的优缺点吧。

Dexposed

基于Xposed的AOP框架,方法级粒度,可以进行AOP编程、插桩、热补丁、SDK hook等功能。

Xposed需要Root权限,是因为它要修改其他应用、系统的行为,而对单个应用来说,其实不需要root。 Xposed通过修改Android Dalvik运行时的Zygote进程,并使用Xposed Bridge来hook方法并注入自己的代码,实现非侵入式的runtime修改。比如蜻蜓fm和喜马拉雅做的事情,其实就很适合这种场景,别人反编译市场下载的代码是看不到patch的行为的。小米(onVmCreated里面还未小米做了资源的处理)也重用了dexposed,去做了很多自定义主题的功能,还有沉浸式状态栏等。

我们知道,应用启动的时候,都会fork zygote进程,装载class和invoke各种初始化方法,Xposed就是在这个过程中,替换了app_process,hook了各种入口级方法(比如handleBindApplication、ServerThread、ActivityThread、ApplicationPackageManager的getResourcesForApplication等),加载XposedBridge.jar提供动态hook基础。

具体到方法,可参见XposedBridge:

12345
/** * Intercept every call to the specified method and call a handler function instead. * @param method The method to intercept */private native synchronized static void hookMethodNative(Member method, Class<?> declaringClass, int slot, Object additionalInfo);

其具体native实现则在Xposed的libxposed_common.cpp里面有注册,根据系统版本分发到libxposed_dalvik和libxposed_art里面,以dalvik为例大致来说就是记录下原来的方法信息,并把方法指针指向我们的hookedMethodCallback,从而实现拦截的目的。

方法级的替换是指,可以在方法前、方法后插入代码,或者直接替换方法。只能针对java方法做拦截,不支持C的方法。

来说说硬伤吧,不支持art,不支持art,不支持art。
重要的事情要说三遍。尽管在6月,项目网站的roadmap就写了7、8月会支持art,但事实是现在还无法解决art的兼容。

另外,如果线上release版本进行了混淆,那写patch也是一件很痛苦的事情,反射+内部类,可能还有包名和内部类的名字冲突,总而言之就是写得很痛苦。

AndFix

同样是方法的hook,AndFix不像Dexposed从Method入手,而是以Field为切入点。

先看Java入口,AndFixManager.fix:

12345678910111213141516171819202122232425262728293031323334353637383940
/** * 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<String> classes) {// 省略...判断是否支持,安全检查,读取补丁的dex文件ClassLoader patchClassLoader = new ClassLoader(classLoader) {@Overrideprotected 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}if (clazz == null) {throw new ClassNotFoundException(className);}return clazz;}};Enumeration<String> entrys = dexFile.entries();Class<?> clazz = null;while (entrys.hasMoreElements()) {String entry = entrys.nextElement();if (classes != null && !classes.contains(entry)) {continue;// skip, not need fix}      // 找到了,加载补丁classclazz = dexFile.loadClass(entry, patchClassLoader);if (clazz != null) {fixClass(clazz, classLoader);}}} catch (IOException e) {Log.e(TAG, "pacth", e);}}

看来最终fix是在fixClass方法:

123456789101112131415161718192021222324252627282930313233343536373839
private void fixClass(Class<?> clazz, ClassLoader classLoader) {  Method[] methods = clazz.getDeclaredMethods();  MethodReplace methodReplace;  String clz;  String meth;  // 遍历补丁class里的方法,进行一一替换,annotation则是补丁包工具自动加上的  for (Method method : methods) {    methodReplace = method.getAnnotation(MethodReplace.class);    if (methodReplace == null)      continue;    clz = methodReplace.clazz();    meth = methodReplace.method();    if (!isEmpty(clz) && !isEmpty(meth)) {      replaceMethod(classLoader, clz, meth, method);    }  }}private void replaceMethod(ClassLoader classLoader, String clz, String meth, Method method) {  try {    String key = clz + "@" + classLoader.toString();    Class<?> clazz = mFixedClass.get(key);    if (clazz == null) {// class not load      // 要被替换的class      Class<?> clzz = classLoader.loadClass(clz);      // 这里也很黑科技,通过C层,改写accessFlags,把需要替换的类的所有方法(Field)改成了public,具体可以看Method结构体      clazz = AndFix.initTargetClass(clzz);    }    if (clazz != null) {// initialize class OK      mFixedClass.put(key, clazz);      // 需要被替换的函数      Method src = clazz.getDeclaredMethod(meth, method.getParameterTypes());      // 这里是调用了jni,art和dalvik分别执行不同的替换逻辑,在cpp进行实现      AndFix.addReplaceMethod(src, method);    }  } catch (Exception e) {    Log.e(TAG, "replaceMethod", e);  }}

在dalvik和art上,系统的调用不同,但是原理类似,这里我们尝个鲜,以6.0为例art_method_replace_6_0

12345678910111213141516171819202122232425262728293031323334353637383940
// 进行方法的替换void replace_6_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);dmeth->declaring_class_->class_loader_ =smeth->declaring_class_->class_loader_; //for plugin classloaderdmeth->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_);}// 这就是上面提到的,把方法都改成public的,所以说了解一下jni还是很有必要的,java世界在c世界是有映射关系的void setFieldFlag_6_0(JNIEnv* env, jobject field) {art::mirror::ArtField* artField =(art::mirror::ArtField*) env->FromReflectedField(field);artField->access_flags_ = artField->access_flags_ & (~0x0002) | 0x0001;LOGD("setFieldFlag_6_0: %d ", artField->access_flags_);}

在dalvik上的实现略有不同,是通过jni bridge来指向补丁的方法。

使用上,直接写一个新的类,会由补丁工具会生成注解,描述其与要打补丁的类和方法的对应关系。

ClassLoader

原腾讯空间Android工程师,也是我的启蒙老师的陈钟发明的热补丁方案,是他在看源码的时候偶然发现的切入点。

我们知道,multidex方案的实现,其实就是把多个dex放进app的classloader之中,从而使得所有dex的类都能被找到。而实际上findClass的过程中,如果出现了重复的类,参照下面的类加载的实现,是会使用第一个找到的类的。

12345678910111213141516
public Class findClass(String name, List<Throwable> suppressed) {      for (Element element : dexElements) {  //每个Element就是一个dex文件        DexFile dex = element.dexFile;        if (dex != null) {            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);            if (clazz != null) {              return clazz;            }        }    }    if (dexElementsSuppressedExceptions != null) {          suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));    }    return null;}

该热补丁方案就是从这一点出发,只要把有问题的类修复后,放到一个单独的dex,通过反射插入到dexElements数组的最前面,不就可以让虚拟机加载到打完补丁的class了吗。

说到此处,似乎已经是一个完整的方案了,但在实践中,会发现运行加载类的时候报preverified错误,原来在DexPrepare.cpp,将dex转化成odex的过程中,会在DexVerify.cpp进行校验,验证如果直接引用到的类和clazz是否在同一个dex,如果是,则会打上CLASS_ISPREVERIFIED标志。通过在所有类(Application除外,当时还没加载自定义类的代码)的构造函数插入一个对在单独的dex的类的引用,就可以解决这个问题。空间使用了javaassist进行编译时字节码插入。

开源实现有Nuwa, HotFix, DroidFix。

比较

Dexposed不支持Art模式(5.0+),且写补丁有点困难,需要反射写混淆后的代码,粒度太细,要替换的方法多的话,工作量会比较大。

AndFix支持2.3-6.0,但是不清楚是否有一些机型的坑在里面,毕竟jni层不像java曾一样标准,从实现来说,方法类似Dexposed,都是通过jni来替换方法,但是实现上更简洁直接,应用patch不需要重启。但由于从实现上直接跳过了类初始化,设置为初始化完毕,所以像是静态函数、静态成员、构造函数都会出现问题,复杂点的类Class.forname很可能直接就会挂掉。

ClassLoader方案支持2.3-6.0,会对启动速度略微有影响,只能在下一次应用启动时生效,在空间中已经有了较长时间的线上应用,如果可以接受在下次启动才应用补丁,是很好的选择。

总的来说,在兼容性稳定性上,ClassLoader方案很可靠,如果需要应用不重启就能修复,而且方法足够简单,可以使用AndFix,而Dexposed由于还不能支持art,所以只能暂时放弃,希望开发者们可以改进使它能支持art模式,毕竟xposed的种种能力还是很吸引人的(比如hook别人app的方法拿到解密后的数据,嘿嘿),还有比如无痕埋点啊线上追踪问题之类的,随时可以下掉。

0 0
原创粉丝点击