Android ASM 插桩实践

Posted 张鹿鹿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android ASM 插桩实践相关的知识,希望对你有一定的参考价值。

上一章知道了如何获取 class 文件,那该如何进行插桩呢?本章告诉你!

什么是 ASM?

ASM 是一个字节码操作库,它可以直接修改已经存在的 class 文件或者生成 class 文件。 ASM 提供了一系列便捷的功能来操作字节码内容,与其它字节码的操作框架相比(例如 AspectJ),ASM 更加偏向于底层,直接操作字节码,在设计上更小、更快,性能上更好,而且几乎可以修改任意字节码。

参考网易乐得团队关于插桩库的实验结果:

通过上表,ASM 效率更高。不过效率高的代价就是 ASM 直接操作字节码,相对于其他库上手相对困难。

ASM 修改 class

接下来,看下 ASM 是如何修改 class 文件的,先看下 ASM 的核心 API:

  • ClassReader:对具体的 class 文件进行读取与解析
  • ClassVisitor、AdviceAdapter:可以访问class文件的各个部分,比如方法、变量、注解等,用于修改 class 文件。
  • ClassWriter:将修改后的class文件通过文件流的方式覆盖掉原来的 class 文件,从而实现 class 修改;

通过下图简单了解下 ASM 处理流程:

基础 Java 字节码知识

使用 ASM 之前,需要简单了解下基本的 Java 字节码知识。

什么是 Java 字节码?

Java 字节码(英语:Java bytecode)是Java虚拟机执行的一种指令格式。通俗来讲字节码就是经过 javac 命令编译之后生成的 class 文件。 class文件包含了 Java 虚拟机指令集和符号表以及若干其他的辅助信息。

可以通过 javap –c xxx.class 终端命令来查看对应的字节码。例如:

字节码描述符

从上面生成的 Java 字节码,可以看到这样的描述:java/lang/Object.“”😦)V,这就是字节码描述符

描述符的作用是描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

对于基本数据类型(byte char double float int long short boolean)以及代表无返回值的void类型都用一个大写字符来表示,对象类型则用字符“L”加对象的全限定名来表示(即把包名所有“.”换成了“/” ),一般对象类型末尾都会加一个“;”来表示全限定名的结束。

举个例子:

String[] ->  [Ljava/lang/String;

int[][]  ->  [[I;

对于方法则使用 (), 按照参数列表,返回值的顺序表示。 例如:

void init()                   ->   ()V
void test(object)             ->   (Ljava/lang/object;)V
String[] getArray(String s)   ->   (Ljava/lang/String;)[Ljava/lang/String;

给出下图理解下:

虚拟机执行字节码基础

为了控制版面,避免长篇大论的讨论具体内容而忽略需要解决的问题的本质,重点讨论 Java 运行时的内存布局:

虚拟机的内存分为堆内存与栈内存。堆内存所有线程共享,栈内存则线程私有,重点解释下栈内存。
Java 虚拟机栈描述的是 Java 方法执行的内存模型:每个方法在执行同时会创建一个 栈帧 用于存局部变量表、操作数栈、动态链接、方法返回地址等信息。每个方法从调用到执行完毕的过程,就对应着一个栈帧在虚拟机中从入栈到出栈的过程。每一个栈帧都包含了上述信息。一个线程中的方法调用链可能会很长,即会有很多栈帧。对于一个当前活动的线程中,只有位于线程栈顶的栈帧才是有效的,成为 前栈帧(current stack Frame) ,这个栈帧所关联的方法成为 当前方法(current method)

总之我们知道每个线程对应的中会有若干栈帧,每个栈帧对应着相应的方法

栈帧的概念图:

局部变量表:局部变量表是一组变量存储空间,用于存储方法参数(入参)和方法内部定义的局部变量。它的容量以容量槽为最小单位(slot)。虚拟机通过索引的定位方式使用这个局部变量表,从0开始,在非 static 方法中,0 代表的是“this”,其余参数从1开始分配。以 android click 方法为例

这个方法的局部变量表的容量槽为:

0 :this
1 :View v

操作数栈:它是一个后入先出的栈结构。当一个方法刚开始执行时,操作数栈是空的,执行过程中,会有各种字节码执行向操作数中写入和提取内容,也就是出栈和入栈的过程。

看下面两个方法,它们对应字节码指令的解释:

参考 JVM 指令集合:http://www.wangyuwei.me/2017/01/19/JVM%E6%8C%87%E4%BB%A4%E9%9B%86%E6%95%B4%E7%90%86/

小思考

大家看下面的例子:

可参考下方的助记符说明:

问题:

为什么在调用 funB 方法之前要把 this 压至栈顶(aload_0)呢?

解释:

如果清楚了前面的内容这个操作就非常好解释,因为 funB 方法是作为 TestClass2 类的内部方法,需要使用当前对象 this 来调用,故需要 aload_0 指令将 this 压至栈顶。


经过上面的一系列操作,我想大家应该。。。


ASM Bytecode Outline 插件

上面的字节码知识只是为了帮助大家理解后面插桩代码的含义,真正使用时推荐使用这个神器,一款 IDEA 的插件 ASM Bytecode Outline。它可以自动给你生成对应的 ASM 操作代码。

使用方法:

Bytecode Tab 会生成对应的字节码文件,ASMMified 会生成对应的 ASM 操作代码:


纯 Java 的插桩示例

通过前面一系列的准备,我们终于可以尝试去写一个插桩示例了。示例很简单,就是在 insertFun 方法前和后分别插入一段回调代码,并把 this,入参带过去。

为了验证插入的正确性,在回调方法中打印一些日志:

写一个 ASMJava 类,读取 TestClass,通过 TestClassVisitor 修改后写入 out/TestClass.class 中。

参考链接:https://www.jianshu.com/p/abd1b1b8d3f3

在写 TestClassVisitor 前,需要用到前面提到的工具 ASM Bytecode Outline ,将要需要插入的 ASM 代码 copy 出来:

写一个 TestClassVisitor 继承自 ClassVisitor,实现其 visitMethod 方法,并将刚才的代码 copy 过来:

TestMethodVisitor 类只需继承自 AdviceAdapter 即可。会有一些重要的可复写方法,有兴趣的同学可以打 log 试下。

经过上面的处理我们来看下成果,找到这个输出的 class,结果如我们所料,大功告成:

Android 插桩示例

基于前面的内容,接下来实现一个对 OnClick 自动监听的小示例:

在 第二章 CustomTransform 的基础上,进行修改,获取到对应的 class,进行修改 class 字节码,开始前不用忘记在插件模块的 build.gradle 文件中添加 asm 依赖:

implementation 'org.ow2.asm:asm:5.0.3'
implementation 'org.ow2.asm:asm-commons:5.0.3'

这里贴出主要的核心代码,其他代码篇幅限制,在 Demo 中查看:

效果展示:

Demo 工程

IDEA Java 工程:https://github.com/changer0/JavaASMDemo

Android 工程:https://github.com/changer0/ASMInjectDemo

以上就是本节内容,欢迎大家关注👇👇👇

以上是关于Android ASM 插桩实践的主要内容,如果未能解决你的问题,请参考以下文章

Android Gradle 中的字节码插桩之ASM

Android-ASM字节码插桩与APT原理补充

Android-ASM字节码插桩与APT原理补充

Android ASM插桩技术学习

Android Gradle 中的字节码插桩之ASM

Android Gradle 中的字节码插桩之ASM