Android编译和虚拟机
Andorid编译打包生成APK文件时,会有这样一个流程:
- Java编译器将Java文件编译为class文件
- dx工具将编译输出的类文件转换为dex文件(Android虚拟机不支持class文件) Android虚拟机有两种:Dalvik和ART,JIT与AOT是虚拟机为了提高运行效率等采用的不同的编译策略。
Dalvik和JIT
JIT意思是Just In Time Compiler,就是即时编译技术,与Dalvik虚拟机相关。 JIT在Android2.2到Android4.4版本(7.0版本也有,后文会叙述),JIT的目的是为了提高Android的运行效率。
Dalvik虚拟机可以看做是一个Java虚拟机。在 Android系统初期,每次运行程序的时候,Dalvik负责将dex翻译为机器码交由系统调用。这样有一个缺陷:每次执行代码,都需要Dalvik将操作码代码翻译为机器对应的微处理器指令,然后交给底层系统处理,运行效率很低。
为了提升效率Android在2.2版本中添加了JIT编译器,当App运行时,每当遇到一个新类,JIT编译器就会对这个类进行即时编译,经过编译后的代码,会被优化成相当精简的原生型指令码(即native code),这样在下次执行到相同逻辑的时候,速度就会更快。JIT 编译器可以对执行次数频繁的 dex/odex 代码进行编译与优化,将 dex/odex 中的 Dalvik Code(Smali 指令集)翻译成相当精简的 Native Code 去执行,JIT 的引入使得 Dalvik 的性能提升了 3~6 倍。
JIT缺点:
- 每次启动应用都需要重新编译(没有缓存)
- 运行时比较耗电,耗电量大
ART和AOT
AOT是指"Ahead Of Time",提前编译
JIT是运行时编译,是动态编译,可以对执行次数频繁的dex代码进行编译和优化,减少以后使用时的翻译时间,虽然可以加快Dalvik运行速度,但是有一个很大的问题:将dex翻译为本地机器码也要占用时间。 所以Google在4.4推出了全新的虚拟机运行环境ART(Android RunTime),用来替换Dalvik(4.4上ART和Dalvik共存,用户可以手动选择,5.0 后Dalvik被替换)。
AOT 是静态编译,应用在安装的时候会启动 dex2oat 过程把 dex预编译成 ELF 文件,每次运行程序的时候不用重新编译。 ART 对 Garbage Collection(GC)过程的也进行了改进:
- 只有一次 GC 暂停(Dalvik 需要两次)
- 在 GC 保持暂停状态期间并行处理
- 在清理最近分配的短时对象这种特殊情况中,回收器的总 GC 时间更短
- 优化了垃圾回收的工效,能够更加及时地进行并行垃圾回收,这使得 GC_FOR_ALLOC 事件在典型用例中极为罕见
- 压缩 GC 以减少后台内存使用和碎片
AOT缺点
- 应用安装和系统升级之后的应用优化比较耗时(重新编译,把程序代码转换成机器语言)
- 优化后的文件会占用额外的存储空间(缓存转换结果)
JIT和AOT
Android 7.0上,JIT 编译器被再次使用,采用AOT/JIT 混合编译的策略,特点是:
- 应用在安装的时候dex不会再被编译
- App运行时,dex文件先通过解析器被直接执行,热点函数会被识别并被JIT编译后存储在
jit code cache中并生成profile文件以记录热点函数的信息。 - 手机进入 IDLE(空闲) 或者 Charging(充电) 状态的时候,系统会扫描 App 目录下的 profile 文件并执行 AOT 过程进行编译。
Android中apk打包流程
Apk整体优化
APK 分析工具
- ApkTool 反编译工具
- Analyze apk:查看apk组成大小,各资源占比 - 查看dex文件组成 - 可以进行apk对比
- classyshark 二进制检查工具
- nimbledroid app 性能指标系统:- 文件大小及排行 - Dex方法数、SDK方法数 - 启动时间、内存等
APK组成
- lib:存放so文件,可能会有armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips,大多数情况下只需要支持armabi与x86的架构即可,如果非必需,可以考虑拿掉x86的部分;
- res:存放编译后的资源文件,例如:drawable、layout等等;
- assets:应用程序的资源,应用程序可以使用AssetManager来检索该资源;
- META-INF:该文件夹一般存放于已经签名的APK中,它包含了APK中所有文件的签名摘要等信息;
- kotlin:这些文件包含用于声明标准(“内置”)Kotlin类的数据,这些类未编译为.class文件,而是映射到平台上的现有类型(在本例中为JVM)。例如,kotlin / kotlin.kotlin_builtins包含kotlin包中的非物理类的信息:Int,String,Enum,Annotation,Collection等;
- classes(n).dex:classes文件是Java Class,被DEX编译后可供Dalvik/ART虚拟机所理解的文件格式;
- resources.arsc:编译后的二进制资源文件;
- AndroidManifest.xml:Android的清单文件,格式为AXML,用于描述应用程序的名称、版本、所需权限、注册的四大组件。
Hook(钩子)流程
Hook 又叫“钩子” ,它可以在事件传送的过程中截获并监控事件的传输,将自身的代码与系统方法进行融入。
这样当这些方法被调用时,也就可以执行我们自己的代码,这也是面向切面编程的思想(AOP)
Android 进程hook
反射/动态代理
如图中A点, 作用于Java层。反射/动态代理是虚拟机提供的标准编程接口,可靠性较高。反射API可以帮助我们们访问到private属性并修改,动态代理可以直接从Interface中动态的构造出代理对象,并去监控这个对象。
常见的用法是,用动态代理构造出一个代理对象,然后用反射API去替换进程中的对象,从而达到hook的目的。如:对Java Framework API的修改常用这种方法,修改ActivityThread、修当前进程的系统调用等。
缺点:只在java层,只能通过替换对象达到目的,适用范围较小
优点:稳定性好,调用反射和动态代理并不存在适配问题,技术门槛低
JNI Hook
如图中B点, java代码和native之间的调用是通过JNI接口调用的,所有JNI接口的函数指针都会被保存在虚拟机的一张表中。所以,java和native之间调用可以通过修改函数指针达到。
优点:稳定性高
缺点:只能hook Java和Native之间的native接口函数
ClassLoader
如图中C点, java代码的执行都是靠虚拟机的类加载器ClassLoader去加载,ClassLoader默认的双亲委派机制保证了ClassLoader总是从父类优先去加载java class。所以一类hook方案就是通过修改ClassLoader加载java class的Path路径达到目的。常见的应用场景有一些热修复技术。
优点:稳定性高
缺点:需要提前编译好修改后的class去替换,灵活性降低了
Xposed相关
如图中D点, 这类hook技术的原理都是去修改ART/Dalvik虚拟机,虚拟机为java提供运行时环境,所有的java method都保存在虚拟机一张Map维护,每个Java Method都有个是否是JNI函数的标志位,如果是JNI函数则去查找对应的native函数。所以,一个hook方案是通过把要hook的函数修改为JNI函数,然后实现一个对应的native函数从而达到hook。
大量的一些自动化测试、动态调试都采用这个方法
优点:java层所有的class都可以修改,Activity等都可以注入。灵活性极高。
缺点:ART/Dalvik每次Android系统发布大版本都会被大改,导致每个Android版本都要去适配配。稳定性变差。
前面hook技术都是去修改虚拟机中的java层,如果一个应用还包含Native code话,则得使用不同hook技术
GOT动态链接库hook
如图中E点, Android进程(linux进程)加载动态链接库的时候,都是通过dlopen()函数去把SO读入到当前进程中的一个内存区域中。当调用so代码时,直接跳转到so的内存区域去执行。so对外提供的函数表及函数地址也都在这块内存中。所以,一个hook方法是,修改这块函数地址,从而达到hook的目的。
例如native层的IO重定向,就可以通过这个技术hook libc的open read write等函数实现。
有大量的GOT注入库可以使用
优点:所有so的入口函数都可以被hook,稳定性高
缺点:替换后的函数签名要保持一致,只能hook so入口,无法hook so内部代码逻辑,且so的调用出现内联调用时(不查表直接跳函数地址)无法hook。
Inline hook
如图中F点,因为GOT技术的局限性,有些场景需要hook so内部函数。这就要用到inline hook技术。基本原理是在目标函数执行区域中插入Jump指令,使得cpu跳转到我们的hook函数(shellcode)中。如果我们的hook函数和原目标函数的签名不一致,还需额外保存寄存器信息,跳转回原函数时恢复寄存器信息。inline hook原理虽简单,但是实现起来需要处理的细节很多,因为是直接去改so,所以和指令平台强相关,armv7,armv8,arm64,x86, Thumb指令集都需要去针对性的实现。github中有大量的开源hook框架可参考使用,但稳定性值得考究。
Android进程通信hook
Android进程和其他进程交换数据主要依赖于linux内核提供的进程通信接口。如:socket、Binder等等。所以,还存在一类hook这些通信接口的技术方案。
Binder进程通信hook
Binder进程通信结构如图,是一种典型的Proxy代理接口。Client端通过Proxy向服务端Imp发送消息。Proxy和Imp实现同样的Interface。所以Binder通信都是可以利用动态代理技术去替换Proxy或Imp来达到监控Binder通信的目的。常见的如:hook AMS、WMS、IMS等服务。沙箱技术如VirtualApp、一些自动化监测技术常常用到。且稳定性较高
Socket通信hook
Socket通信提供几个hook思路:
- 只hook Java层的调用,用Xposed类方案hook socket相关class就可做到
- 如果socket是client端,或者支持重新建立连接,某些场景可以构造自己的socket去conect,从而达到hook的目的
- native层则要用户GOT hook
IO重定向
思路:
- 简单的hook可以通过反射等手段修改Path达到
- java层可以用xposed,但因xposed的缘故,稳定性欠佳
- 通用的方案是用GOT hook Libc达到重定向,java层和native都可以解决
Hook技术使用心得
hook方案的选择是一门学问,几个心得原则是:
- 从简,能用简单方案解决的用简单方案,切勿轻易增加复杂度。否则稳定性和后期的维护都可能得不偿失。
- 并不是越底层的方案越好,越底层的hook技术可能反而引入局限性。例如:Xposed修改所有的Activity很简单,但是只修改某个Activity就变得复杂,因为为了定位出这个特殊Activiy会引入一堆复杂度。