0%

Hook

Hook在Android系统的应用根据框架层次可以分为两类,Java层和Native层,常见的实现方式如下:

框架层次 Hook手段
Java层 动态代理,代码、字节码织入(AspectJ、ASM等)
Native层 GOT/PLT Hook,Trap Hook,Inline Hook

其中Native层的三种hook手段在应用范围、实现难度、性能等维度上有以下区别:

比较维度 GOT/PLT Hook Trap Hook Inline Hook
实现原理 修改延时绑定表 SIGTRAP断点信号 运行时指令替换
粒度 方法级 指令级 指令级
作用域 广 广
性能
难度 极高

这三种方式在实际环境中应用较多的是GOT/PLT Hook,由于只是在ELF动态链接的默认流程上稍作修改,这种方式侵入性较低,且能保证性能,可以方便的实现对so库的方法hook,唯一的缺点是只能作用于绑定表中存在的方法,作用域有一定限制。trap hook由于使用系统中断,在性能上表现不好。Inline hook是终极hook手段,通过直接修改运行时内存的方式替换指令,完全手工的完成hook及跳回操作,理论上可以实现任意位置的hook,不过手写指令时需要考虑abi兼容等众多因素,实现难度很高,实际应用的场景不多。

汇编

在手撕汇编之前,先简单回顾下基础知识

高级语言-低级语言转换

平时用来开发的高级语言必须转换成低级语言才能被CPU执行,根据是否有中间结果的区别,完成转换的可能是编译器或虚拟机。和百花齐放的高级语言不同,低级语言只有两种,汇编和机器码(即二进制码),汇编是机器码的文本化表示,两者是一对一的对应关系。

逻辑关系

逻辑上讲,汇编是为了解决机器码可读性的产物,汇编在执行前需要先翻译成机器码,这个过程叫assembing,所以汇编语言叫ASM。

$ gcc -S yourfile可以将c文件编译成汇编文件.s

寄存器

寄存器

为了填平运算组件(CPU)和存储单元(硬盘)的性能沟壑,会在其间加几个缓冲单元,从慢到快依次是RAM、Cache、寄存器。一般CPU会自带这些缓冲层,其中寄存器直接跟CPU交互,是读取速度最快的单位。随着发展CPU的寄存器数量从几个增长到了几十个(ARM有37个),其中前16个一般会被当作通用寄存器使用(编号0-15),而编号15的寄存器又最为特殊,一般会把r15当作program counter,熟悉JVM的同学知道在虚拟机中也有类似的概念,只是一个是虚拟的,一个是真实的:

PC

一般把r15当作PC寄存器,即program counter,也就是Instruction Pointer,指向程序当前执行到的那条指令。

Inline Hook

回到主题,指令级别的hook跟高级语言层面的实现方式在感官上有很大区别,高级语言中不管借助什么手段,只需将hook代码织入到目标代码之中即可,但这种方式在指令级别是行不通的,见下图:

Read more »

背景知识

WebDriver协议

一套抽象了页面行为的HTTP协议,Request表示UI操作,Response表示页面响应结果。该协议用JSON Body来描述命令和响应内容,是JSONWireProtocol的一种应用。

Appium

一个基于WebDriver协议实现的自动化测试框架,主要针对移动端,支持原生App、Web App、Hybird App。

Appium总体是一个C/S结构,即Appium Server和Appium Client。Appium Server是一个暴露一系列API的Node Server(npm install -g appium),它以HTTP Response的形式响应来自Appium Client的不同请求。Appium Client本质就是一个HTTP Request发送器,它封装了一系列API来方便的发送各种UI操作请求。Appium的这种设计优点在于Client可以被灵活的选择,不管用何种语言,何种方式,只要能够发送符合协议的HTTP Request就可以充当Appium Client。

Cucumber

一个实现BDD(Behavior-driven development)的框架。BDD暂不深究,我们主要关注Cucumber给自动化测试带来的改进。Cucumber为了良好的抽象软件行为,定义了一套DSL:Gherkin。使用Gherkin,我们可以将繁杂的自动化脚本进行拆分:将复杂的操作脚本沉淀为下层util,只暴露上层的自然语言的行为描述给开发者,大大降低脚本编写的门槛和成本。

工程设计

总体结构

工程结构

工程总体是一个node工程,主要依赖了webdriveriocucumberchai三个npm包。

Read more »

背景

在输出Android模块时,有时会因为个别原因(比如来自业务的不可抗力),要求将模块打包成一个文件提供给接入方。这就意味着在输出模块由多个子模块组成的情况下,我们需要把多个AAR(或JAR)合并成一个大AAR输出,这个合并过程涉及到了很多有用的知识和难点,这篇文章就详细解析下其中的内容。

认识AAR

AAR文件是一种Android归档包(类比Jar:Java Archive),这种归档包是由Gradle构建库的Android Library插件产出的。它是一个压缩包,里面的内容可以总结为5个目录和5个文件:

AAR文件内容))

*:卖个关子,在上面10个内容中,其中有一个是已经合并后的结果,它默认已经包含了所有子模块的内容,你能猜出来是那个么?

图里文字已经基本解释清楚了各个内容,不再赘述,现在我们根据现有的了解设想下合并AAR的大致思路:AAR里无非是几个目录和文件,所以最终AAR的5个目录下,必然包含了所有子模块的对应内容,而5个文件也肯定是各个子文件合并的结果。那么如何实现包含和合并呢?第一反应是写个脚本对AAR们做解压、整合,再压缩的思路,但稍微推演下就知道不现实(理论上也不可行,AAR和普通压缩文件还是有区别的),再思考下,既然直接拿产物做合并不可行,能不能在构建主模块AAR时,就顺便将子模块内容纳入进来呢?这无疑是个优雅的思路,理论上是否可行呢?答案是可以的,别忘了AAR是Android Gradle Plugin构建产出的,而Gradle强大的拓展支持刚好能实现我们的需求。所以合并AAR的方法,其实就是修改Gradle构建流程,在默认构建的基础上,插入我们自己的操作,最终产出一个包含子模块内容的大AAR。

在入题之前,有必要先理解下Gradle是如何支持构建拓展的,下图是Gradle官网截图:

Read more »

在阿里的插件化方案atlas中,有个细节是在宿主中用Class.forName()加载bundle中的类会报ClassNotFoundException异常,这很奇怪:从现象来看,atlas中宿主无疑可以加载bundle中的类(宿主启动bundle组件肯定需要先加载bundle中的组件类),可是Class.forName()抛出异常又说明无法加载,这不是矛盾么?这两者有什么区别呢?

我们都知道,类是由ClassLoader加载而来,ClassNotFoundException说明该类不在当前ClassLoader的classpath中。那么,Class.forName()抛异常很可能是因为用了跟组件加载不同的ClassLoader,具体是什么不同呢?这就引出了类加载器的两个概念:初始加载器和定义加载器。为了解释,先从使用atlas后ClassLoader的组织关系说起:

img

其中DelegateClassLoader顶替了系统的PathClassLoader,成为了LoadedApk的专职ClassLoader,这意味着DelegateClassLoader接管了四大组件的类加载行为:先遵循双亲委托机制委托PathClassLoader加载,若加载失败,再分派给各插件的BundleClassLoader尝试加载。对于通过ActivityManagerService等调起的四大组件来说,组件类的加载都会从LoadedApk的ClassLoader(也就是DelegateClassLoader)的loadClass方法进入,开始类加载流程,所以DelegateClassLoader就叫做这些组件类的初始加载器。随着流程进行,属于宿主的组件类,最终被PathClassLoader的defineClass方法加载成功,所以把PathClassLoader叫做这些类的定义加载器。而属于bundle的组件类,最终被各自的BundleClassLoader加载成功,BundleClassLoader就是它们的定义加载器。

了解了两种加载器的概念,再回过头看Class.forName()跟组件类加载的区别,见源码:

1
2
3
4
5
6
7
8
9
/**
* where {@code currentLoader} denotes the defining class loader of
* the current class.
*/
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
return forName(className, true, VMStack.getCallingClassLoader());
}
1
2
3
4
5
6
public final class VMStack {
/**
* Returns the defining class loader of the caller's caller.
*/
native public static ClassLoader getCallingClassLoader();
}

注释明确说明了Class.forName()使用的classloader是当前类的定义加载器,而非初始加载器。所以,在宿主中用Class.forName()加载一个类,使用的是最终定义当前类的PathClassLoader,而非DelegateClassLoader,而从PathClassLoader到DelegateClassLoader不存在委托关系(反过来才有),所以Class.forName()无法加载bundle中的类。

明白了原因,解决方法很简单:使用宿主类的初始加载器加载bundle类即可,其中初始加载器可通过getClassLoader()@ContextWrapper获得。

关于类加载过程,这里有详细的解释,还介绍了两种加载器的关联之处:

一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer引用了类 com.example.Inner,则由类 com.example.Outer的定义加载器负责启动类 com.example.Inner的加载过程。

可以看出,随着引用链的深入,定义加载器会在类加载器的继承树上不可逆的向上委托。这种设计的背后理念是:引用关系应该是单向的、由表及里的,一个底层模块不应该依赖一个上层模块。