热修复原理学习(2)底层替换原理和突破底层差异的方法(2)

// AndFix/jni/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);

}

}

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

// AndFix/jni/art/art_method_replace_6_0.cpp

void replace_6_0(JNIEnv* env, jobject src, jobject dest) {

art::mirror::ArtMethod* smeth =

(art::mirror::ArtMethod*) env->FromReflectedMethod(src); // 1

art::mirror::ArtMethod* dmeth =

(art::mirror::ArtMethod*) env->FromReflectedMethod(dest); // 2

// 3

smeth->declaring_class_ = dmeth->declaring_class_;

smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;

smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;

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

}

注释1、2:通过Method对象得到底层Java函数对应ArtMethod的真实地址。

注释3:把就函数的所有成员变量都替换为新的。(通过ArtMehtod结构体)(注意注意!这里相当于是替换了原结构体中的字段信息)

每一个Java方法在Art虚拟机中都对应着一个 ArtMethod,ArtMethod记录了这个Java方法的所有信息,包括所属类、访问权限、代码执行地址等。

通过 env->FromReflectedMethod,可以由 Method对象得到这个方法所对应的 ArtMethod的真正其实地址,然后就可以把它强制转化为ArtMethod指针,从而对其包含的所有成员进行修改。

这样全部替换完之后就完成了热修复功能。以后调用方法时就会直接运行到新方法中实现。

1.2 虚拟机调用方法的原理


为什么这样替换完就可以实现热修复呢?这需要从虚拟机调用方法的原理说起。

在Android6.0版本中,Art虚拟机中的 Art虚拟机中ArtMethod的结构如下:

// art/runtime/art_method.h

class ArtMethod FINAL {

protected:

GcRootmirror::Class declaring_class_;

GcRootmirror::PointerArray dex_cache_resolved_methods_;

GcRoot<mirror::ObjectArraymirror::Class> dex_cache_resolved_types_;

uint32_t access_flags_;

uint32_t dex_code_item_offset_;

uint32_t dex_method_index_;

uint32_t method_index_;

struct PACKED(4) PtrSizedFields {

void* entry_point_from_interpreter_; // 1

void* entry_point_from_jni_;

void* entry_point_from_quick_compiled_code_; //2

} ptr_sized_fields_;

}

在 ArtMethod结构体中,最重要的就是 注释1和注释2标注的内容,从名字可以看出来,他们就是方法的执行入口。

我们知道,Java代码在Android中会被编译为 Dex Code。

Art虚拟机中可以采用解释模式或者 AOT机器码模式执行 Dex Code

  • 解释模式

就是去除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已经被编译成了机器码。但是机器码并非可以脱离虚拟机而单独运行,以下面这段简单的代码为例。

public class MainActivity extends Activity {

protected void onCreate(Bundle saveInstanceState) {

super.onCreate(saveInstanceState);

}

}

编译为 AOT机器码后,是这样的:

7:void com.rikkatheworld.demo.MainActivity.onCreate(android.os.Bundle) (dex_method_idx)

DEX CODE:

0x0000: 6f20 4600 1000 | invoke-super {v0, v1}, void anroid.App.Activity.onCreate(android.os.Bundle)

0x0003: 0e00 | return-void

CODE: (code_offset=0x006fdbac size_offset=0x006fdba8 size=96)

… …

0x006fdbe0: f94003e0 ldr x0, [sp]

;x0 = MainActivity.onCreate 对应的 ArtMethod指针

0x006fdbe4: b9400400 ldr w0, [x0, #4]

;w0 = [x0 + 4] = dex_cache_resolved_methods_ 字段

0x006fdbe8: f9412000 ldr x0, [w0, #576]

;x0 = [x0 + 576]; dex_cache_resolved_methods_ 数组的第72(=576/8)个元素,即对应 Activity.onCreate的 ArtMethod指针

0x006fdbec: f940181e ldr lr, [x0, #48]

;lr = [x0 + 48] = Activity.onCreate 的ArtMethod成员的执行入口点

;即 entry_point_from_quick_compiled_code_

0x006fdbf0: d63f03c0 blr lr

;调用 Activity.onCreate

这里去掉了一些校验之类的无关代码,可以看到,在调用一个方法时,获取了 ArtMethod中的 dex_cache_resolved_methods文件,这是一个存放 ArtMethod* 的指针数组,通过它就可以访问到这个 Method所在Dex中所有的 Method所对应的 ArtMethod*

Activity.onCreate() 的方法索引是70,由于是64位系统,因此每个指针的大小为8kb,又由于ArtMethod*元素时从这个数组的第0×2 个位置开始存放的,因此偏移量为 (70 + 2) * 8 = 576的位置正是 Activity.onCreate() 方法的 ArtMethod指针。

这只是一个比较简单的例子,而在实际代码中,有许多更为复杂的调用情况,很多情况下还需要调用 dex_code_item_offset_字段。由此可以看出,AOT机器码的执行过程,还是会有对虚拟机以及ArtMethod成员字段的依赖。

因此,当把一个旧方法的所有成员字段都换为新方法的成员字段后,执行时所有的数据就可以保持和新方法的数据一致。这样在所有执行到旧方法的地方,会获取新方法的执行入口、所属类型、方法索引号以及所属dex信息,然后像调用旧方法一样去执行新方法的逻辑。

1.3 兼容性问题的根源


然而,目前市场上几乎所有的native替换方案,比如 Andfix和其他安全界的Hook方案,ArtMethod结构体的结构都是固定的,这回带来巨大的兼容性问题。

从刚才的分析可以看到,虽然Andfix是把底层结构强转为 art::mirror::ArtMethod,但这里的 art::mirror::ArtMethod并非等同于App运行时所在设备虚拟机底层的 art::mirror::ArtMethod,而是 Andfix自己构造的 art::mirror::ArtMethod

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

举个例子,在Andfix替换 declaring_class_ 的地方:

//这里是设备本身的ArtMethod和AndFix的ArtMethod一样的情况下

smeth->declaring_class_ = demth->declaring_class;

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

(uint32_t) (smth + 0) = (uint32_t) (dmeth + 0)

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

//这里是设备本身的ArtMethod和AndFix的ArtMethod不一样的情况下

smeth->additional_ = dmeth->additional_;

这样就和原有的替换逻辑不一致了。

这也正是Andfix不支持所有机型的原因,很大的可能,是因为这些手机机型修改了底层的虚拟机结构

2.突破底层差异的方法

============================================================================

2.1 突破底层结构差异


知道了 native替换方式兼容性问题的原因,我们是否有办法寻求一种新的方式,不依赖ROM底层方法结构的实现而达到替换效果呢?

我们发现,这样的native层面替换思路,其实就是替换 ArtMethod的所有成员,那么,并不需要构造出ArtMethod具体的各个成员字段,只要把 ArtMethod作为整体进行替换,这样不就可以了吗?

因此 Andfix这一系列烦琐的替换:

smeth->declaring_class_ = dmeth->declaring_class_;

smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;

smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;

可以缩写成:

memcpy(smeth, dmeth, sizeof(ArtMethod));

这正是 Sophix为了解决Andfix的问题,而研发出的新的方案。

刚才提到过,不同的手机厂商都可以对 ArtMethod进行替换,只要像这样吧 ArtMethod整个结构体完整替换,就能够把所有旧方法成员自动对应的换成新方法的成员。

但这其中最关键的地方在于,sizeof(ArtMethod)的计算结果,如果计算结果有偏差,导致部分成员没有被替换,或者替换区域超出了边界,都会导致严重的问题。

对于ROM开发者而言,是在 Art源代码中开发,所以一个简单的 sizeof(ArtMethod)就行了,因为这是在编译器就可以决定的。

但对于上层开发者,App会被下发给各式各样的Android设备,所以需要在运行时动态地获取App运行设备中的底层 ArtMethod大小的,就没有那么简单了。

想要忽略ArtMethod具体结构成员直接获取其size的精确值,还是需要从虚拟机的源码入手,从底层的数据结构以及排列特点探寻答案。

在 Art中,初始化一个类的时候会给这个类的所有方法分配内存空间,我们可以看到这个分配内存空间的地方(Android 8.0代码):

// art/runtime/class_linker.cc

void ClassLinker::LoadClassMembers(Thread* self,

const DexFile& dex_file,

const uint8_t* class_data,

Handlemirror::Class klass) {

{

klass->SetMethodsPtr(

AllocArtMethodArray(self, allocator, it.NumDirectMethods() + it.NumVirtualMethods()),

it.NumDirectMethods(),

it.NumVirtualMethods());

}

类的方法中有 direct方法和 virtual方法。

direct方法包含static方法和所有不可继承的对象方法。而 virtual方法包含了所有可以继承的对象方法

AllocArtMethodArray() 函数用来分配他们的方法所在区域,第三个参数传的是所有的direct方法和virtual的总数:

// class-linker.cc

LengthPrefixedArray* ClassLinker::AllocArtMethodArray(Thread* self,

LinearAlloc* allocator,

size_t length) {

if (length == 0) {

return nullptr;

}

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::ComputeSize(length, method_size, method_alignment);

void* array_storage = allocator->Alloc(self, storage_size);

auto* ret = new (array_storage) LengthPrefixedArray(length);

CHECK(ret != nullptr);

// 1

for (size_t i = 0; i < length; ++i) {

new(reinterpret_cast<void*>(&ret->At(i, method_size, method_alignment))) ArtMethod;

}

return ret;

}

注释1中,建立所有方法总数和的一个循环。每次循环使用 ret->(i, method_size, method_alignment)创建出一个ArtMethod。

而这个方法的作用就是分配空间,因为i是连续的,所以分配出来的空间也是连续的。

这是只是分配出内存空间,还没有对ArtMethod的各个成员赋值,不过这并不影响观察ArtMethod的空间结构,Artmethod空间结构如下图所示:

在这里插入图片描述

这里给了我们启示,ArtMethod是紧密排列的,所以一个ArtMethod的大小,不就是相邻两个ArtMethod的起始地址的差值吗?

正是如此,我们就从这个排列特点入手,自己构造一个类,以一种巧妙的方式获取到这个差值:

public class NativeStructsModel {

final public static void f1() {}

final public static void f2() {}

}

由于 f1和f2都是static方法,所以都属于 direct ArtMethod Array。由于 NativeStructsModel类中只存这两个方法,因此它们肯定是相邻的。

那么就可以在JNI层取得它们的地址差值:

size_t firMid = (size_t) env->GetStaticMethodID(nativeStructsModelClazz, “f1”, “()V”); // 计算出 f1()的地址

size_t secMid = (size_t) env->GetStaticMethodID(nativeStructsModelClazz, “f2”, “()V”); // 计算出 f2()的地址

size_t methSize = secMid - firMid; //因为f2和f1是相邻的,所以 两者相减就是一个 ArtMethod的大小

memcpy(smeth, dmeth, methSize); //将size代入,完成替换

**值得一提的是,由于忽略了底层ArtMethod的结构差异,对于所有Android版本都不需要区分,而统一以 memcpy()实现即可,代码量大大减少。

即使以后的Android版本不断修改ArtMethod的成员,只要保证ArtMethod数组仍是以线性结构排列,就无须再做适配。**

2.2 访问权限的问题


(1)方法调用时的权限检查

看到这里,你可能会产生疑惑:我们只是替换了ArtMethod的内容,但替换方案的所属类,和原有方法的所属类,是不同的类型,被替换的方法有权限访问这个类的其他private方法吗?

以这段简单的代码为例子:

public class Demo {

Demo() {

func();

文末

今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。

最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

还有 高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【算法合集】

【延伸Android必备知识点】

【Android部分高级架构视频学习资源】

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

大厂面试真题解析】**

[外链图片转存中…(img-AYoG77lQ-1714665783361)]

【算法合集】

[外链图片转存中…(img-fKSAqeBe-1714665783362)]

【延伸Android必备知识点】

[外链图片转存中…(img-1HwwRr6G-1714665783363)]

【Android部分高级架构视频学习资源】

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 26
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值