从精准化测试看ASM在Android中的强势插入-字节码
从精准化测试看ASM在Android中的强势插入-字节码
字节码是ASM的基础,要想熟练的使用ASM,那么了解字节码就是必备基础。
Class的文件格式
Class文件作为Java虚拟机所执行的直接文件,内部结构设计有着固定的协议,每一个Class文件只对应一个类或接口的定义信息。
每个Class文件都以8位为单位的字节流组成,下面是一个Class文件中所包括的内容,在Class文件中,各项内容按照严格顺序连续存放,Java虚拟机只要按照协议顺序来读取即可。
ClassFile {
u4 magic;
u2 minor_version;
u2 major_version;
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];
u2 access_flags;
u2 this_class;
u2 super_class;
u2 interfaces_count;
u2 interfaces[interfaces_count];
u2 fields_count;
field_info fields[fields_count];
u2 methods_count;
method_info methods[methods_count];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
在Class文件结构中,上面各项的含义如下。
Name | 含义 |
---|---|
magic | 作为一个魔数,确定这个文件是否是一个能被虚拟机接受的class文件,值固定为0xCAFEBABE。 |
minor_version,major_version | 分别表示class文件的副,主版本号,不同版本的虚拟机实现支持的Class文件版本号不同。 |
constant_pool_count | 常量池计数器,constant_pool_count的值等于常量池表中的成员数加1。 |
constant_pool | 常量池,constant_pool是一种表结构,包含class文件结构及其子结构中引用的所有字符常量、类或接口名、字段名和其他常量。 |
access_flags | access_flags是一种访问标志,表示这个类或者接口的访问权限及属性,包括有ACC_PUBLIC,ACC_FINAL,ACC_SUPER等等。 |
this_class | 类索引,指向常量池表中项的一个索引。 |
super_class | 父类索引,这个值必须为0或者是对常量池中项的一个有效索引值,如果为0,表示这个class只能是Object类,只有它是唯一没有父类的类。 |
interfaces_count | 接口计算器,表示当前类或者接口的直接父接口数量。 |
interfaces[] | 接口表,里面的每个成员的值必须是一个对常量池表中项的一个有效索引值。 |
fields_count | 字段计算器,表示当前class文件中fields表的成员个数,每个成员都是一个field_info。 |
fields | 字段表,每个成员都是一个完整的fields_info结构,表示当前类或接口中某个字段的完整描述,不包括父类或父接口的部分。 |
methods_count | 方法计数器,表示当前class文件methos表的成员个数。 |
methods | 方法表,每个成员都是一个完整的method_info结构,可以表示类或接口中定义的所有方法,包括实例方法,类方法,以及类或接口初始化方法。 |
attributes_count | 属性表,其中是每一个attribute_info,包含以下这些属性,InnerClasses,EnclosingMethod,Synthetic,Signature,Annonation等。 |
以上内容来自网络,我也不知道从哪copy来的。
字节码和Java代码还是有很大区别的。
- 一个字节码文件只能描述一个类,而一个Java文件中可以则包含多个类。当一个Java文件是描述一个包含内部类的类,那么该Java文件则会被编译为两个类文件,文件名上通过「$」来区分,主类文件中包含对其内部类的引用,定义了内部方法的内部类会包含外部引用
- 字节码文件中不包含注释,只有有效的可执行代码,例如类、字段、方法和属性
- 字节码文件中不包含package和import部分, 所有类型名字都必须是完全限定的
- 字节码文件还包含常量池(constant pool),这些内容是编译时生成的,常量池本质上就是一个数组存储了类中出现的所有数值、字符串和类型常量,这些常量仅需要在这个常量池部分中定义一次,就可以利用其索引,在类文件中的所有其他各部分进行引用
字节码的执行过程
字节码在Java虚拟机中是以堆栈的方式进行运算的,类似CPU中的寄存器,在Java虚拟机中,它使用堆栈来完成运算,例如实现「a+b」的加法操作,在Java虚拟机中,首先会将「a」push到堆栈中,然后再将「b」push到堆栈中,最后执行「ADD」指令,取出用于计算的两个变量,完成计算后,将返回值「a+b」push到堆栈中,完成指令。
类型描述符
我们在Java代码中的类型,在字节码中,有相应的表示协议。
Java Type | Type description |
---|---|
boolean | Z |
char | C |
byte | B |
short | S |
int | I |
float | F |
long | J |
double | D |
object | Ljava/lang/Object; |
int[] | [I |
Object[][] | [[Ljava/lang/Object; |
void | V |
引用类型 | L |
- Java基本类型的描述符是单个字符,例如Z表示boolean、C表示char
- 类的类型的描述符是这个类的全限定名,前面加上字符L , 后面跟上一个「;」,例如String的类型描述符为Ljava/lang/String;
- 数组类型的描述符是一个方括号后面跟有该数组元素类型的描述符,多维数组则使用多个方括号
借助上面的协议分析,想要看到字节码中参数的类型,就比较简单了。
方法描述符
方法描述符(方法签名)是一个类型描述符列表,它用一个字符串描述一个方法的参数类型和返回类型。
方法描述符以左括号开头,然后是每个形参的类型描述符,然后是是右括号,接下来是返回类型的类型描述符,例如,该方法返回void,则是V,要注意的是,方法描述符中不包含方法的名字或参数名。
Java方法声明 | 方法描述符 | 说明 |
---|---|---|
void m(int i, float f) | (IF)V | 接收一个int和float型参数且无返回值 |
int m(Object o) | (Ljava/lang/Object;)I | 接收Object型参数返回int |
int[] m(int i, String s) | (ILjava/lang/String;)[I | 接受int和String返回一个int[] |
Object m(int[] i) | ([I)Ljava/lang/Object; | 接受一个int[]返回Object |
字节码示例
我们来看下这段简单的代码,在字节码下是怎样的。
通过ASMPlugin,我们看下生成的字节码,如下所示。
可以发现,这里主要分成了两个部分——init和onCreate。
Java中的每一个方法在执行的时候,Java虚拟机都会为其分配一个「栈帧」,栈帧是用来存储方法中计算所需要的所有数据的。
其中第0个元素就是「this」,如果方法有参数传入会排在它的后面。
字节码中有很多指令,下面对一些比较常用的指令进行下讲解。
- ALOAD 0:这个指令是LOAD系列指令中的一个,它的意思表示push当前第0个元素到堆栈中。代码上相当于使用「this」,A表示这个数据元素的类型是一个引用类型。类似的指令还有:ALOAD,ILOAD,LLOAD,FLOAD,DLOAD,它们的作用就是针对不用数据类型而准备的LOAD指令
- INVOKESPECIAL:这个指令是调用系列指令中的一个。其目的是调用对象类的方法。后面需要给上父类的方法完整签
- INVOKEVIRTUAL:这个指令区别于INVOKESPECIAL的是,它是根据引用调用对象类的方法
- INVOKESTATIC:调用类的静态方法
大家不用完全掌握这些指令,结合代码来看的话,还是能看懂的,我们需要的是修改字节码,而不是从0开始。
对于Java源文件:如果只有一个方法,编译生成时,也会有两个方法,其中一个是默认构造函数 对于Kotlin源文件:如果只有一个方法,编译生成时,会产生四个方法,一个是默认构造函数,还有两个是kotlin合成的方法,以及退出时清除内存的默认函数
ASM Code
再结合ASM Code来看,还是上面的例子。
默认的构造函数。
onCreate:
这里面有些生成的代码,例如:
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(9, label0);
methodVisitor.visitLocalVariable("this", "Lcom/yw/asmtest/MainActivity;", null, label0, label4, 0);
这些都是调试代码和写入变量表的方法,我们不必关心。
剩下的代码,就是我们可以在ASM中所需要的代码。