插桩,顾名思义,就是在代码编译期间修改已有的代码获证生成新代码。
基础知识
1、使用插桩的场景
- 代码生成
- 代码监控
- 代码修改
- 代码分析
从技术实现上看,编译插桩是从代码编译的流程介入可以分为两类: -
Java文件,类似APT、AndroidAnnotation这些代码生成的场景,他们生成的都是Java文件,是在编译的最开始接入。
-
字节码(Bytecode),字节码的方式可以操作“.class”的Java字节码,也可以操作“.dex”的Dalvik字节码,这取决于我们使用的插桩方法。
2、字节码
对于Java平台,Java虚拟机运行的是class文件,内部对应的是Java字节码。而针对Android的字节码是Google专门为其设计的一种Dalvik字节码,虽然增加了指令长度但却缩减了指令数量,执行更会快速。
它们的主要区别有:
-
体系架构,Java虚拟机是基于栈实现,而Android虚拟机是基于寄存器实现,在ARM平台,寄存器实现性能会高于栈实现。
- 格式结构,对于Class文件,每个文件都会有自己的单独的常量池以及其他一些公共字段。对于Dex文件,整个Dex中所有Class共用一个常量池和公共字段,所以整体结构更加紧凑,因而大大减少了体积。
- 指令优化,Dalvik字节码对大量的指令专门做了精简和优化。
编译插桩的三种方法
AspectJ和ASM框架的输入和输出都是Class文件,他们是我们最常使用的java字节码处理框架。
1、AspectJ
AspectJ是Java中流行的AOP编程扩展框架,从底层实现上来看,AspectJ内部使用的是BCEL框架来完成,在使用上来看,AspectJ框架有自己的一定优势:
- 成熟稳定
- 使用简单
我们完全不用关心底层Java字节码的处理流程,就可以轻松实现编译插桩功能。但是它的功能无法满足某些场景的需求,如果想针对所有函数都做插桩,AspectJ会带来不少的性能影响。
2、ASM
ASM的功能非常强大,它可以满足100%的场景,其主要特点有:
- 操作灵活,可以根据需求自定义修改、插入、删除。
- 上手难, 需要对Java字节码有比较深入的了解。
ASM更加高效直接,因而有时需要掌握一些必不可少的Java字节码知识换人Java虚拟机运行机制。Java虚拟机是基于栈实现的,Java虚拟机的描述:
每一条 Java 虚拟机线程都有自己私有的 Java 虚拟机栈,这个栈与线程同时创建,用于存储栈帧(Stack Frame)。
所以在多线程应用中多个线程就会有多个栈,每个栈都有自己的栈帧。
如下图所示,我们可以简单的认为栈帧包含3个重要的内容:本地变量表(Local Variable Array)、操作数栈(Operand Stack)和常量池引用(Constant Pool Reference)。
- 本地变量表,可以认为本地变量表是存放临时数据的,并且本地变量表有个很重要的功能就是用来传递方法调用时的参数,当调用方法的时候,参数会依次传递到本地变量表中从0开始的位置上,并且如果调用的方法是实例方法,那么我们可以通过第0个本地变量中获取当前实例的引用,也就是this所指向的对象。
- 操作数栈,可以认为操作数栈是一个用于存放指令执行所需要的数据位置,指令从操作数栈中取走数据并将操作结构重入栈。
由于本地变量表和操作数栈的最大深度是在编译时就确定的,所以在使用ASM进行字节码操作后需要调用ASM提供的visitMaxs方法来设置maxLocal和maxStack数,不过,ASM为了方便用户使用,已经提供了自动计算的方法,在实例化ClassWriter操作类的时候传入COMPUTE_MAXS,ASM就会自动计算本地变量表和操作数栈。
在具体的字节码处理过程中,特别需要注意的是本地变量表和操作数栈的数据交换和try catch block的处理。 -
数据交换,如下图所示,在经过IADD指令操作后,又通过ISTORE 0 指令将栈顶int值存入第1个本地变量中个,用于临时保存,在最后的加法过程中,将0和1位置的本地变量取出压入操作数栈中功IADD指令使用。
- 遗产处理,在字节码操作过程中需要特别注意异常处理对操作数栈的影响,如果在try和catch之间抛出了一个可捕获的异常,那么当前的操作数栈会被清空,并将异常对象压入这个空栈中,执行过程在catch处继续。幸运的是,如果生产了错误的字节码,编译器可以辨别出该情况并导致编译异常,ASM中也提供了字节码Verify的接口,可以在修改完成后验证一下字节码是否正常。
如果想在一个方法执行完成后增加代码,ASM相对也要简单很多,可以在字节码中出现的每一条RETURN系或者ATHROW的指令前,增加处理的逻辑即可。
ReDex
ReDex不仅只是作为一款Dex优化工具,它也提供了很多的小工具和功能,比如在ReDex里提供了一个简单的Method Tracing和Block Tracing工具,这个工具可以在所有方法或者指定方法前插入一跟踪代码。
ReDex的这个功能并不是完整的AOP工具,但它提供了一系列指令生成API和Opcode插入API,我们可以参照这个功能实现自己的字节码注入工具,这个功能的代码在Instrument.cpp中。
由于Dalvik字节码发展时间尚端,而且因为Dex格式更加紧凑,修改起来往往牵一发而动全身,并且Dalvik字节码的处理相比Java字节码会更加复杂一些,所以直接操作Dalivk字节码的工具并不是很多。
市面上大部分需要直接修改Dex的情况是逆向的,很多同学都采用手动书写Smail代码然后编译回去,总结一下Dex字节码库:
-ASMDEX,开发者是ASM库的作者,但很久未更新了
- Dexter, Google官方开发的Dex操作库,更新很频繁,但是使用起来很复杂
- Dexmaker,用来生成Dalvik字节码的代码
- Soot,修改Dex的方法很另类,是将Dalvik字节码转换成一种Jimple three-address code,然后插入Jimple Opcode后再转回Dalvik字节码。
网友评论