AndFix中的方法热替换原理

2,173 阅读6分钟

概述

方法热替换是一种运行时方法Hook技术的应用,达到调用旧方法实际执行新方法的目的。
在Dalvik/ART中,方法是一个Method对象存放在对应的内存区域中,通过method_idx映射到具体对象。
我们替换的是Method对象的属性。
截屏2020-10-13上午11.29.56.png

准备知识

Android 代码是怎么执行的

在 Android 中,Java 类被转换成 DEX 字节码。DEX 字节码通过 ART 或者 Dalvik runtime 转换成机器码。这里 DEX 字节码和设备架构无关。
Dalvik 是一个基于 JIT(Just in time)编译的引擎。使用 Dalvik 存在一些缺点,所以从 Android 4.4(Kitkat)开始引入了 ART 作为运行时,从 Android 5.0(Lollipop)开始 ART 就全面取代了Dalvik。Android 7.0 向 ART 中添加了一个 just-in-time(JIT)编译器,这样就可以在应用运行时持续的提高其性能。
**重点:**Dalvik 使用 JIT(Just in time)编译而 ART 使用 AOT(Ahead of time)编译。

JVM VS Dalvik

下图描述了 Dalvik 虚拟机和 Java 虚拟机之间的差别。
image.png

Dalvik VS ART

image.png

Class类文件结构

Class文件结构采用类似C语言的结构体来存储数据的,主要有两类数据项,无符号数和表,无符号数用来表述数字,索引引用以及字符串等,比如 u1,u2,u4,u8分别代表1个字节,2个字节,4个字节,8个字节的无符号数,而表是有多个无符号数以及其它的表组成的复合结构,习惯地以_info结尾。表用于描述有层次关系的符合结构的数据,整个Class文件本质上就是一张表。

ClassFile {
    u4             magic;
    u2             minor_version;
    u2             major_version;
    u2             constant_pool_count;
    cp_info        constant_pool[constant_pool_count-1];
    u2             access_flags;
    u2             this_class;
    u2             super_class;
    u2             interfaces_count;
    u2             interfaces[interfaces_count];
    u2             fields_count;
    field_info     fields[fields_count];
    u2             methods_count;
    method_info    methods[methods_count];
    u2             attributes_count;
    attribute_info attributes[attributes_count];
}

Dex文件

Android 平台中没有直接使用 Class 文件格式,因为早期的 Anrdroid 手机内存,存储都比较小,而 Class 文件显然有很多可以优化的地方,比如每个 Class 文件都有一个常量池,里边存储了一些字符串。一串内容完全相同的字符串很有可能在不同的 Class 文件的常量池中存在,这就是一个可以优化的地方。当然,Dex 文件结构和 Class 文件结构差异的地方还很多,但是从携带的信息上来看,Dex 和 Class 文件是一致的。所以,你了解了 Class 文件(作为 Java VM 官方 Spec 的标准),Dex 文件结构只不过是一个变种罢了(从学习到什么程度为止的问题来看,如果不是要自己来解析 Dex 文件,或者反编译 / 修改 dex 文件,我觉得大致了解下 Dex 文件结构的情况就可以了)。

Dex 文件结构

image.png

ArtMethod结构体

Art运行时,会把Class加载到内存中,从dex中读取方法,以ArtMethod对象数组的形式存放在内存中。
ArtMethod7.0地址在art/runtime/art_method.h中。不同版本的结构是有差异的

class ArtMethod {
            uint32_t declaring_class_;
            uint32_t access_flags_;
            uint32_t dex_code_item_offset_;
            uint32_t dex_method_index_;
            uint16_t method_index_;uint16_t hotness_count_;
            struct PtrSizedFields {
                ArtMethod **dex_cache_resolved_methods_;
                void *dex_cache_resolved_types_;
                void *entry_point_from_jni_;
                void *entry_point_from_quick_compiled_code_;
            } ptr_sized_fields_;
};

ArtMethodArray

art/runtime/class_linker.cc

void ClassLinker::LoadClassMembers(Thread* self,
                                   const DexFile& dex_file,
                                   const uint8_t* class_data,
                                   Handle<mirror::Class> klass,
                                   const OatFile::OatClass* oat_class) {
    ...
    // Load methods.
    klass->SetMethodsPtr(
        AllocArtMethodArray(self, allocator, it.NumDirectMethods() + it.NumVirtualMethods()),
        it.NumDirectMethods(),
        it.NumVirtualMethods());
    ...
}
LengthPrefixedArray<ArtMethod>* ClassLinker::AllocArtMethodArray(Thread* self,
                                                                 LinearAlloc* allocator,
                                                                 size_t length) {
  ...
  const size_t method_alignment = ArtMethod::Alignment(image_pointer_size_);
  const size_t method_size = ArtMethod::Size(image_pointer_size_);
  const size_t storage_size =
      LengthPrefixedArray<ArtMethod>::ComputeSize(length, method_size, method_alignment);
  void* array_storage = allocator->Alloc(self, storage_size);
  auto* ret = new (array_storage) LengthPrefixedArray<ArtMethod>(length);
  CHECK(ret != nullptr);
  for (size_t i = 0; i < length; ++i) {
    new(reinterpret_cast<void*>(&ret->At(i, method_size, method_alignment))) ArtMethod;
  }
  return ret;
}

Art方法调用原理


在ART中,每一个Java方法在虚拟机(注:ART与虚拟机虽有细微差别,但本文不作区分,两者含义相同,下同)内部都由一个ArtMethod对象表示(native层,实际上是一个C++对象),这个native 的 ArtMethod对象包含了此Java方法的所有信息,比如名字,参数类型,方法本身代码的入口地址(entrypoint)等;暂时放下trampoline以及interpreter和jit不谈,一个Java方法的执行非常简单:

  1. 想办法拿到这个Java方法所代表的ArtMethod对象
  2. 取出其entrypoint,然后跳转到此处开始执行


image.png

方法替换过程解析

这里我们以Andfix中ArtMethod替换为例来分析,实际的替换是在native层完成的。

Java Method

我们定义三个方法,旧方法M.a(),新方法MFix.a(),还有操作JNI的方法NDKTools.replaceMethod(Method,Method)

public class M {
    public static String a() {return "aaa";}
}

public class MFix {
    private static String a() {return "a fixxxx"; }
}

public class NDKTools {
    static {System.loadLibrary("hello"); }
    
    public static native void  replaceMethod(Method mthA, Method mthB);

    public static void doReplace() {
      
       new MFix();
       Method mthA=M.class.getDeclaredMethod("a");
       Method mthB= MFix.class.getDeclaredMethod("a");
       NDKTools.replaceMethod(mthA,mthB);
    }
}

Native ArtMethod指针

JNI提供了FromReflectedMethod方法,转换一个java.lang.reflect.Method对象到一个ArtMethod对象指针

//通过JNI RegisterNatives关联 java类中的native void  replaceMethod(..)
static void replaceMethod(JNIEnv *env, jclass clazz, jobject src, jobject dest) {
    art::mirror::ArtMethod* smeth =
            (art::mirror::ArtMethod*) env->FromReflectedMethod(src);
    art::mirror::ArtMethod* dmeth =
            (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);

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

定义ArtMethod

上述代码中的art::mirror::ArtMethod并不是引用 art/runtime/art_method.h中的ArtMethod。其实是把源码抄了出来。还有关联的Object,Class,ArtFields。基于Android7.0。

class ArtMethod {
            uint32_t declaring_class_;
            uint32_t access_flags_;
            uint32_t dex_code_item_offset_;
            uint32_t dex_method_index_;
            uint16_t method_index_;uint16_t hotness_count_;
    		struct PtrSizedFields {
            	ArtMethod **dex_cache_resolved_methods_;
            	void *dex_cache_resolved_types_;
				void *entry_point_from_jni_;
				void *entry_point_from_quick_compiled_code_;
            } ptr_sized_fields_;
};

运行结果

Log.d("Fix",M.a());//输出:aaa
NDKTools.doReplace();
Log.d("Fix",M.a());//输出:a fixxxx

memcpy整体替换

使用ArtMethod结构体的局限性

知得注意的是,上述代码在Android7.0可以跑起来,但是在Android8.0就不行了。因为Android8.0的结构定义变了。阿里Andfix针对不同版本Art,分别做了适配。 而阿里Sophix提出了memcpy整体替换的方案。
由于ArtMethod是存放在数组中,且单个版本内ArtMethod的内存长度是固定。所以只要取到ArtMethod的size就可以通过JNI提供的 void *memcpy(void *, const void *, size_t) ,把destMethod的内容复制到 srtMethod即可。

ArtMethod Size

阿里Sophix给到一个计算size的方法,即在一个类中定义两个方法,计算对应ArtMethod地址差值

public class MethodSize {
    final  public static void f1(){}
    final  public static void f2(){}
}
static int methodSize;
extern  int calculateMethodSize(JNIEnv *env) {
    jclass clz=env->FindClass("com/fix/MethodSize");
    jbyte *f1 = reinterpret_cast<jbyte *>(env->GetStaticMethodID(clz, "f1", "()V"));
    jbyte *f2 = reinterpret_cast<jbyte *>(env->GetStaticMethodID(clz, "f2", "()V"));
    methodSize = (jbyte *) f2 - (jbyte *) f1;
    return methodSize;
}

memcpy

通过代码我们就可以屏蔽ArtMethod结构体的细节,达到整体替换的目的

extern void replace_memcpy(JNIEnv *env, jclass clazz, jobject src, jobject dest) {
    LOGD("replace memcpy methodSize %d",methodSize);
    void *smethodIds = env->FromReflectedMethod(src);
    void *dmethodIds = env->FromReflectedMethod(dest);
    memcpy(smethodIds, dmethodIds, methodSize);
}

局限性

实际在Android8及以下是可以的。Android9和Android10是不行的,还不清楚原因。

小结

Demo地址
Demo中包含Android_hotfix.pdf,即阿里图书《深入探索Android热修复技术原理》

参考:
走进Android运行时 DVM vs ART
Dalvik虚拟机介绍
豆瓣:深入理解Android:Java虚拟机ART 邓凡平
github alibaba Andfix