ART深度探索开篇:从Method Hook谈起
Android上的热修复框架 AndFix 想必已经是耳熟能详,它的原理实际上很简单:方法替换——Java层的每一个方法在虚拟机实现里面都对应着一个ArtMethod的结构体,只要把原方法的结构体内容替换成新的结构体的内容,在调用原方法的时候,真正执行的指令会是新方法的指令;这样就能实现热修复,详细代码见 AndFix。
为什么可以这么做呢?那得从 Android 虚拟机的方法调用过程说起。作为一个系列的开篇,本文不打算展开讲虚拟机原理等内容,首先给大家一道开胃菜;后续我们再深入探索ART。
众所周知,AndFix是一种 native 的hotfix方案,它的替换过程是用 c 在 native层完成的,但其实,我们也可以用纯Java实现它!而且,代码还非常精简,且看——
方法替换原理
既然我们知道 AndFix 的原理是方法替换,那么为什么直接替换Java里面的 java.lang.reflect.Method
有什么问题吗?直接这样貌似很难下结论,那我们换个思路。我们实现方法替换的结果,就是调用原方法的时候最终是调用被替换的方法。因此,我们可以看看 java.lang.reflect.Method
类的 invoke
方法。(这里有个疑问,Foo.bar()这种直接调用与反射调用Foo.class.getDeclaredMethod(“bar”).invoke(null) 有什么区别吗?这个问题后续再谈)
1 2 |
private native Object invoke(Object receiver, Object[] args, boolean accessible) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException; |
这个invoke是一个native方法,它的native实现在 art/runtime/native/java_lang_reflect_Method.cc
里面,这个jni方法最终调用了 art/runtime/reflection.cc
的 InvokeMethod
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
object InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod, jobject javaReceiver, jobject javaArgs, bool accessible) { // 略... mirror::ArtMethod* m = mirror::ArtMethod::FromReflectedMethod(soa, javaMethod); mirror::Class* declaring_class = m->GetDeclaringClass(); // 按需初始化类,略。。 mirror::Object* receiver = nullptr; if (!m->IsStatic()) { // Check that the receiver is non-null and an instance of the field's declaring class. receiver = soa.Decode<mirror::Object*>(javaReceiver); if (!VerifyObjectIsClass(receiver, declaring_class)) { return NULL; } // Find the actual implementation of the virtual method. m = receiver->GetClass()->FindVirtualMethodForVirtualOrInterface(m); } // 略.. InvokeWithArgArray(soa, m, &arg_array, &result, shorty); // 略 。。 // Box if necessary and return. return soa.AddLocalReference<jobject>(BoxPrimitive(mh.GetReturnType()->GetPrimitiveType(), result)); } |
上面函数 InvokeMethod 的第二个参数 javaMethod
就是Java层我们进行反射调用的那个Method对象,在jni层反映为一个jobject;InvokeMethod这个native方法首先通过 mirror::ArtMethod::FromReflectedMethod
获取了Java对象的在native层的 ArtMethod指针,我们跟进去看看是怎么实现的:
1 2 3 4 5 6 7 8 |
ArtMethod* ArtMethod::FromReflectedMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject jlr_method) { mirror::ArtField* f = soa.DecodeField(WellKnownClasses::java_lang_reflect_AbstractMethod_artMethod); mirror::ArtMethod* method = f->GetObject(soa.Decode<mirror::Object*>(jlr_method))->AsArtMethod(); DCHECK(method != nullptr); return method; } |
我们在这里看到了一点端倪,获取到了Java层那个Method对象的一个叫做 artMethod
的字段,然后强转成了ArtMethod指针(这里的说法不是很准确,但是要搞明白这里面的细节一两篇文章讲不清楚 ~_~,我们暂且这么认为吧。)
AndFix的实现里面,也正是使用这个 FromReflectedMethod
方法拿到Java层Method对应native层的ArtMethod指针,然后执行替换的。
上面我们也看到了,我们在native层替换的那个 ArtMethod 不是在 Java 层也有对应的东西么?我们直接替换掉 Java 层的这个artMethod 字段不就OK了?但是我们要注意的是,在Java里面除了基本类型,其他东西都是引用。要实现类似C++里面那种替换引用所指向内容的机智,需要一些黑科技。
Unsafe 和 Memory
要在Java层操作内容,也不是没有办法做到;JDK给我们留了一个后门:sun.misc.Unsafe
类;在OpenJDK里面这个类灰常强大,从内存操作到CAS到锁机制,无所不能(可惜的是据说JDK8要去掉?)但是在Android 平台还有一点点不一样,在 Android N之前,Android的JDK实现是 Apache Harmony,这个实现里面的Unsafe就有点鸡肋了,没法写内存;好在Android 又开了一个后门:Memory
类。
有了这两个类,我们就能在Java层进行简单的内存操作了!!由于这两个类是隐藏类,我写了一个wrapper,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
private static class Memory { // libcode.io.Memory#peekByte static byte peekByte(long address) { return (Byte) Reflection.call(null, "libcore.io.Memory", "peekByte", null, new Class[]{long.class}, new Object[]{address}); } static void pokeByte(long address, byte value) { Reflection.call(null, "libcore.io.Memory", "pokeByte", null, new Class[]{long.class, byte.class}, new Object[]{address, value}); } public static void memcpy(long dst, long src, long length) { for (long i = 0; i < length; i++) { pokeByte(dst, peekByte(src)); dst++; src++; } } } static class Unsafe { static final String UNSAFE_CLASS = "sun.misc.Unsafe"; static Object THE_UNSAFE; private static boolean is64Bit; static { THE_UNSAFE = Reflection.get(null, UNSAFE_CLASS, "THE_ONE", null); Object runtime = Reflection.call(null, "dalvik.system.VMRuntime", "getRuntime", null, null, null); is64Bit = (Boolean) Reflection.call(null, "dalvik.system.VMRuntime", "is64Bit", runtime, null, null); } public static long getObjectAddress(Object o) { Object[] objects = {o}; Integer baseOffset = (Integer) Reflection.call(null, UNSAFE_CLASS, "arrayBaseOffset", THE_UNSAFE, new Class[]{Class.class}, new Object[]{Object[].class}); return ((Number) Reflection.call(null, UNSAFE_CLASS, is64Bit ? "getLong" : "getInt", THE_UNSAFE, new Class[]{Object.class, long.class}, new Object[]{objects, baseOffset.longValue()})).longValue(); } } |
具体实现
接下来思路就很简单了呀,用伪代码表示就是:
1 |
memcopy(originArtMethod, replaceArtMethod); |
但是还有一个问题,我们要整个把 originMethod 的 artMethod 所在的内存直接替换为 replaceMethod 的artMethod 所在的内存(上面我们已经知道,Java层Method类的artMethod实际上就是native层的指针表示,在Android N上更明显,这玩意儿直接就是一个long),现在我们已经知道这两个地址是什么,那么我们把 replaceArtMethod 代表的内存复制到 originArtMethod 的区域,应该还需要知道一个 artMethod 有多大。
但是事情没有一个 sizeof 那么简单。你看AndFix的实现是在每个Android版本把ArtMethod这个结构体复制一份的;要想用sizeof还得把这个类所有的引用复制过来,及其麻烦。更何况在Java里面 sizeof都没有。不过也不是没有办法,既然我们已经能在Java层拿到对象的地址,只需要创建一个数组,丢两个ArtMethod,把两个数组元素的起始地址相减不就得到一个 artMethod的大小了吗?(此方法来自Android热修复升级探索——追寻极致的代码热替换)
不过,既然我们实现了方法替换;还有最后一个问题,如果我们需要在替换后的方法里面调用原函数呢?这个也很简单,我们只需要把原函数copy一份保存起来,需要调用原函数的时候调用那个copy的函数不就行了?不过在具体实现的时候,会遇到一个问题,就是 Java的非static 非private的方法默认是虚方法,在调用这个方法的时候会有一个类似查找虚函数表的过程,这个在上面的代码 InvokeMethod
里面可以看到:
1 2 3 4 5 6 7 8 9 10 11 |
mirror::Object* receiver = nullptr; if (!m->IsStatic()) { // Check that the receiver is non-null and an instance of the field's declaring class. receiver = soa.Decode<mirror::Object*>(javaReceiver); if (!VerifyObjectIsClass(receiver, declaring_class)) { return NULL; } // Find the actual implementation of the virtual method. m = receiver->GetClass()->FindVirtualMethodForVirtualOrInterface(m); } |
在调用的时候,如果不是static的方法,会去查找这个方法的真正实现;我们直接把原方法做了备份之后,去调用备份的那个方法,如果此方法是public的,则会查找到原来的那个函数,于是就无限循环了;我们只需要阻止这个过程,查看 FindVirtualMethodForVirtualOrInterface 这个方法的实现就知道,只要方法是 invoke-direct 进行调用的,就会直接返回原方法,这些方法包括:构造函数,private的方法( 见 https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html) 因此,我们手动把这个备份的方法属性修改为private即可解决这个问题。
详细代码见:github/epic
至此,我们就用纯Java实现了一个 AndFix,代码只有200行不到!!是不是很神奇?当然,这里面包含了很多黑科技,接下来我们将以这个为引子,深入探索Android ART的方方面面,揭开虚拟机底层的神秘面纱,敬请期待~~
原文地址: http://weishu.me/2017/03/20/dive-into-art-hello-world/
ART深度探索开篇:从Method Hook谈起相关推荐
- 转载:网络游戏外挂设计深度探索
网络游戏外挂设计深度探索 网络游戏的繁荣使它的"寄生虫"--外挂也迅速繁荣起来,进而形成了一种畸形产业,不少开发高手在这里找到了所谓的"第一桶金".由于外挂,游 ...
- hdfs读写流程_深度探索Hadoop分布式文件系统(HDFS)数据读取流程
一.开篇 Hadoop分布式文件系统(HDFS)是Hadoop大数据生态最底层的数据存储设施.因其具备了海量数据分布式存储能力,针对不同批处理业务的大吞吐数据计算承载力,使其综合复杂度要远远高于其他数 ...
- 深度探索Hyperledger技术与应用之超级账本初体验(附部署代码)
2019独角兽企业重金招聘Python工程师标准>>> 本章零基础地介绍了如何快速体验超级账本搭建的区块链网络,我们先绕过了比较复杂的初始化配置,用官方提供的fabric-sampl ...
- WebWork深度探索之号外
昨天开始对WebWork进行了一些初步的探索[1],虽然进展缓慢,但是在阅读与分析其源代码的时候,还是有颇多的收获.这些所得并不属于探索WebWork本身,因而将此篇列为号外. 在Ac ...
- 《Android深度探索(卷1):HAL与驱动开发》——1.6节 Linux设备驱动
本节书摘来自异步社区<Android深度探索(卷1):HAL与驱动开发>一书中的第1章,第1.6节 Linux设备驱动,作者李宁,更多章节内容可以访问云栖社区"异步社区" ...
- Android深度探索(卷1)HAL与驱动开发 第四章 源代码的下载和编译 读书笔记
Android深度探索(卷1)HAL与驱动开发 第四章 源代码的下载和编译 读书笔记 本章学习了使用git下载两套源代码并搭建两个开发环境.分别为Android源代码和Linux内核源代码.A ...
- 《Android深度探索(卷1):HAL与驱动开发》——6.4节使用多种方式测试Linux驱动...
本节书摘来自异步社区<Android深度探索(卷1):HAL与驱动开发>一书中的第6章,第6.4节使用多种方式测试Linux驱动,作者李宁,更多章节内容可以访问云栖社区"异步社区 ...
- 《深度探索C++对象模型》--5 构造析构拷贝 6 执行期语意学
<深度探索C++对象模型>--5构造.析构.拷贝语意学 1.纯虚函数: (1)C++可以定义和调用一个纯虚函数,不过只可以静态调用,不可以由虚拟机制调用. 注意:pure virtu ...
- Android深度探索第五章
开发板是开发和学习嵌入式技术的主要硬件设备,开发板拥有许多扩展的端口,可以很容易开发定制的硬件,并与开发板链接.目前市面上的开发板型号和种类很多,但目前最流行的是基于三星S3C6410ARM11架构的 ...
最新文章
- 每日一皮:当写的程序出现bug时,就是这么奇妙...
- 06-密码学基础-混合密码系统
- 引用校长对于管理工程学的学术研究的思考
- python 主语_前深度学习时代--FFM模型的原理与Python实现
- 多进程和多线程的优缺点
- 「递归」第9集 | 我在腾讯做研究
- 2017CCPC 杭州 J. Master of GCD【差分标记/线段树/GCD】
- zabbix监控远端主机
- 本地开发时连接后台数据库时出现的错误,附自救方法
- Hibernate框架ORM的实现原理-不是技术的技术
- Atitit 图像处理 深刻理解梯度原理计算.v1 qc8
- Intouch Historian历史曲线配置导入导出
- ajax submit 文件上传,ajaxSubmit 文件上传
- boost库使用总结
- 网传腾讯大规模裁员测试工程师,腾讯相关人员否认
- Are You a Software Architect?
- python Excel xlsx file; not supported
- 【OBS-STUDIO】OBSApp: OBS入口类
- C语言winmain函数的参数,c++:谁调用了main/WinMain函数!
- 本月(2019年8月)算法工程师一二线城市工资,杭州,广州,宁波,合肥半年涨幅在500元以上
热门文章
- 《快速构建Windows 8风格应用》系列文章汇总
- 详细解释CNN卷积神经网络各层的参数和链接个数的计算
- Go(GoLang)解决 cannot find package/golang.org/问题 Grpc+ProtoBuf所需的一些资源
- 基于VTK User Guide和VTK Textbook学习
- java lamda循环条件_Java lambda 循环累加求和代码
- 【Matlab】山地建模?立体热度?怎么绘制三维曲面图?
- [云炬学英语]每日一句2020.8.26
- <马哲>劳动价值论的理论及实践意义
- 云炬Android开发笔记 2-3Android Studio如何导入Github上的项目
- php ajax 弹窗修改,更改PHP/Ajax脚本来使用Meekrodb?