阿里 Andfix 介绍及原理解析

开源项目官方介绍:

AndFix judges the methods should be replaced by java custom annotation and replaces it by hooking it. AndFix has a native method art_replaceMethod in ART or dalvik_replaceMethod in Dalvik.

从以上介绍我们可以知道,它是通过自定义注解来判断方法是否需要被替换。原理是hook native层的方法来实现方法替换以达到修复功能。由于andfix属于native方案,因此为了精简一下篇幅,这里我们就侧重讲解native层的原理,至于dex diff 以及补丁加载等Java层的代码逻辑,不是很多,大家有兴趣可自行下载andfix demo工程去看一看。

替换示意图:

阿里 Andfix 介绍及原理解析_第1张图片

方案原理

andfix 方案对于 dalivk 及 art 分别用不同方式去实现方法替换的。原理图如下:

ART环境:

阿里 Andfix 介绍及原理解析_第2张图片

Dalivk环境:
阿里 Andfix 介绍及原理解析_第3张图片

andfix 具体实现代码链接:
https://github.com/alibaba/AndFix/blob/master/jni/andfix.cpp

从官方文档及andfix 源码我们不难看出,其核心原理在于replaceMethod函数。该方案的替换过程大致如下:

阿里 Andfix 介绍及原理解析_第4张图片

Andfix是在已经加载了的类中直接在native层替换掉原有方法,它是在原来类的基础上进行修改的。

源码分析

从上面的介绍我们知道其方案核心在于方法替换。纸上谈兵多说无益,我们来看一下Andfix的源码,分析核心实现逻辑根据介绍成都原理我们大概可推测它的核心大概是replaceMethod方法,这是Java 层到Native 层的入口,因此我们从这个入口开始探索:

源文件路径: AndFix/src/com/alipay/euler/andfix/AndFix.java

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

从代码我们可以看出这是一个native方法,它的参数是在Java层通过反射机制得到的Method对象所对应的jobject。src对应的是需要被替换的原有方法,而dest对应的就是新方法,新方法存在于补丁包的新类中,也就是补丁方法。native层对应的方法如下:

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

众所周知,Android的Java运行环境在4.4以下用的是Dalvik虚拟机,而在4.4及以上用的是ART虚拟机。

ART平台

源文件: art_method_replace.cpp

extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(
		JNIEnv* env, jobject src, jobject dest) {
    if (apilevel > 23) {
        replace_7_0(env, src, dest);
    } else if (apilevel > 22) {
		replace_6_0(env, src, dest);
	} else if (apilevel > 21) {
		replace_5_1(env, src, dest);
	} else if (apilevel > 19) {
		replace_5_0(env, src, dest);
    }else{
        replace_4_4(env, src, dest);
    }
}

Dalvik平台

源文件: dalvik_method_replace.cpp

早期版本:
阿里 Andfix 介绍及原理解析_第5张图片

新版本:
阿里 Andfix 介绍及原理解析_第6张图片

从代码可以看出,原来早期的版本Andfix 是参考了 Dexposed 的思想。Dalivk 环境中通过指向公共分发函数然后反射回调来实现。而新版本则统一通过覆盖方法函数的成员指针引用来实现。

对于不同Android版本的art,底层Java对象的数据结构是不同的,因而会进一步区分不同的替换函数,这里我们以Android 6.0为例,对应的就是replace_6_0。

每一个Java方法在art中都对应着一个ArtMethod,ArtMethod记录了这个Java方法的所有信息,包括所属类、访问权限、代码执行地址等等。通过env->FromReflectedMethod,可以由Method对象得到这个方法对应的ArtMethod的真正起始地址。然后就可以把它强转为ArtMethod指针,从而对其所有成员进行替换。这样全部替换完之后就完成了热修复逻辑,以后调用这个方法时就会直接走到新方法的实现中了。为什么把所有成员替换完就可以实现热修复呢?这需要从虚拟机调用方法的原理说起。

虚拟机调用方法的原理

以Android 6.0 为例,art虚拟机中ArtMethod的结构是这个样子的:

 protected:
  // Field order required by test "ValidateFieldOrderOfJavaCppUnionClasses".
  // The class we are a part of.
  GcRoot declaring_class_;
  // Access flags; low 16 bits are defined by spec.
  // Getting and setting this flag needs to be atomic when concurrency is
  // possible, e.g. after this method's class is linked. Such as when setting
  // verifier flags and single-implementation flag.
  std::atomic access_flags_;
  /* Dex file fields. The defining dex file is available via declaring_class_->dex_cache_ */
  // Offset to the CodeItem.
  uint32_t dex_code_item_offset_;
  // Index into method_ids of the dex file associated with this method.
  uint32_t dex_method_index_;
  /* End of dex file fields. */
  // Entry within a dispatch table for this method. For static/direct methods the index is into
  // the declaringClass.directMethods, for virtual methods the vtable and for interface methods the
  // ifTable.
  uint16_t method_index_;
  // The hotness we measure for this method. Managed by the interpreter. Not atomic, as we allow
  // missing increments: if the method is hot, we will see it eventually.
  uint16_t hotness_count_;
  // Fake padding field gets inserted here.
  // Must be the last fields in the method.
  struct PtrSizedFields {
    // Short cuts to declaring_class_->dex_cache_ member for fast compiled code access.
    mirror::MethodDexCacheType* dex_cache_resolved_methods_;
    // Pointer to JNI function registered to this method, or a function to resolve the JNI function,
    // or the profiling data for non-native methods, or an ImtConflictTable, or the
    // single-implementation of an abstract/interface method.
    void* data_;
    // Method dispatch from quick compiled code invokes this pointer which may cause bridging into
    // the interpreter.
    void* entry_point_from_quick_compiled_code_;
  } ptr_sized_fields_;

这其中最重要的字段就是entry_point_from_interprete_和entry_point_from_quick_compiled_code_了,从名字可以看出来,他们就是方法的执行入口。我们知道,Java代码在Android中会被编译为Dex Code。

OAT名词科普

ART虚拟机,它的核心是OAT文件。OAT文件是一种Android私有ELF文件格式,它不仅包含有从DEX文件翻译而来的本地机器指令,还包含有原来的DEX文件内容,这使得我们无需重新编译原有的APK就可以让它正常地在ART里面运行。

AOT名词科普

AOT是”Ahead Of Time”的缩写,指的就是ART(Anroid RunTime)这种运行方式。

ART中可以采用解释模式或者AOT模式(本地机器指令执行模式)执行。
解释模式,就是取出Dex Code,逐条解释执行就行了。如果方法的调用者是以解释模式运行的,在调用这个方法时,就会取得这个方法的entry_point_from_interpreter_,然后跳转过去执行。
如果是AOT的方式,就会先预编译好Dex Code对应的机器码,然后运行期直接执行机器码就行了,不需要一条条地解释执行Dex Code。如果方法的调用者是以AOT机器码方式执行的,在调用这个方法时,就是跳转到entry_point_from_quick_compiled_code_执行。

那我们是不是只需要替换这几个entry_point_*入口地址就能够实现方法替换了呢?
并没有这么简单。因为不论是解释模式或是AOT机器码模式,在运行期间都还会需要用到ArtMethod里面的其他成员字段。
就以AOT机器码模式为例,虽然Dex Code被编译成了机器码。但是机器码并不是可以脱离虚拟机而单独运行的,以这段简单的代码为例:
阿里 Andfix 介绍及原理解析_第7张图片

编译为AOT机器码后,是这样的:
阿里 Andfix 介绍及原理解析_第8张图片

注:涉及到字节码/机器码的相关的内容比较偏底层,知识点非常深入也比较枯燥,对这方面若有兴趣深入了解可参考以下几篇文章:
Java字节码介绍:http://www.importnew.com/24088.html
编译AOT机器码指令:https://www.jianshu.com/p/794b64a3e9ab
ART加载oat文件的过程分析:
https://alleniverson.gitbooks.io/android/content/AndroidRuntime/2 ART加载oat文件的过程.html

这里面去掉了一些校验之类的无关代码,可以很清楚看到,在调用一个方法时,取得了ArtMethod中的dex_cache_resolved_methods_,这是一个存放ArtMethod的指针数组,通过它就可以访问到这个Method所在Dex中所有的Method所对应的ArtMethod
Activity.onCreate的方法索引是70,由于是64位系统,因此每个指针的大小为8字节,又由于ArtMethod*元素是从这个数组的第0x2个位置开始存放的,因此偏移(70 + 2) * 8 = 576的位置正是Activity.onCreate的ArtMethod指针。

以上是一个比较简单的例子,在实际代码中,有许多更为复杂的调用情况,很多情况下还需要用到dex_code_item_offset_等字段。由此可以看出,AOT机器码的执行过程,还是会有对于虚拟机以及ArtMethod其他成员字段的依赖。因此,当把一个旧方法的所有成员字段换成都新方法的之后,执行时所有数据才能够确保可以和新方法的一致。这样在所有执行到旧方法的地方,会取得新方法的执行入口、所属class、方法索引号以及所属dex信息,然后像调用旧方法一样顺滑地执行到新方法的逻辑。

Andfix 的 ArtMethod 对比 Android 源码

可以看到,ArtMethod结构里的各个成员的位置是和AOSP开源代码里完全一致的。这是由于Android源码是公开的,Andfix里面的这个ArtMethod自然是遵照android虚拟机art源码里面的ArtMethod构建的。
但正是因为Android是开源的,各个手机厂商都可以对代码进行改造定制。而Andfix里ArtMethod的结构是根据公开的Android源码中的结构写死的,如果某个厂商对这个ArtMethod结构体进行了修改,就和原先开源代码里的结构不一致,那么在这种修改过结构体的设备系统上,Andfix的补丁替换机制就会出问题。

比如,在Andfix替换declaring_class_的地方:

 smeth->declaring_class_ = dmeth->declaring_class_;

由于declaring_class_是andfix里ArtMethod的第一个成员,因此它和以下这行代码等价:

*(unit32_t*) (smeth + 0)  =  *(unit32_t*) (dmeth + 0)

如果手机厂商在ArtMethod结构体的declaring_class_前面添加了一个字段additional_,那么,additional_就成为了ArtMethod的第一个成员,所以smeth + 0这个位置在这台设备上实际就变成了additional_,而不再是declaring_class_字段。所以这行代码的真正含义就变成了:

 smeth->additional_ = dmeth->additional_ ;

这样就和原先替换declaring_class_的逻辑不一致,从而无法正常执行热修复逻辑。
这也正是Andfix不支持很多机型的原因,很大的可能,就是因为这些机型修改了底层的虚拟机结构。

有趣的是,它甚至也不支持兼容自家的YunOS…

结语

从本文分析我们了解了Andfix的大致实现原理,从它的原理上我们可以知道它的优势是支持实时修复,不需要重启即可生效。另外一个优势是对系统的hook点较少,方案整体的逻辑相对比较简单。不过它也有诸多不足:

  1. 由于很多厂商定制改造了 Rom,导致该方案兼容性比较差。
  2. 使用加固平台可能会使热补丁功能失效。
  3. 不支持添加新类和新的字段,仅支持方法级别的修复。
  4. 不支持资源替换。
  5. 可修复场景限制程度高。由于从实现上直接跳过了类初始化,设置为初始化完毕。所以像静态函数、静态成员、构造函数等都无法修复,一改就会出现问题。复杂点的类调用Class.forname很可能直接就会挂掉。
  6. 已停止维护,还没有对7.0之后的版本进行适配。

由于native底层替换方案和ClasssLoader方案各有其优缺点,那么把他们的优点结合起来,岂不是更好的选择吗?
2017年6月,阿里巴巴手淘技术团队推出了全新的移动热更新解决方案——Sophix。如上所述,Sophix的代码修复体系正是汲取 了ClassLoader 方案和 Native Hook 思想,同时涵盖了这两种方案来实现热更新机制的。

关于Sophix的介绍及原理,我们接下来的这篇文章将会详细分析。

参考文献:

《ART加载oat文件的过程分析》 - 罗升阳
《Android热修复升级探索—追寻极致的代码热替换》

你可能感兴趣的:(热修复,Android,Android热修复系列)