字节码是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

字节码示例

我们来看下这段简单的代码,在字节码下是怎样的。

image-20210623103259980

通过ASMPlugin,我们看下生成的字节码,如下所示。

image-20210623103419893

可以发现,这里主要分成了两个部分——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来看,还是上面的例子。

默认的构造函数。

image-20210623105109646

onCreate:

image-20210623105143214

这里面有些生成的代码,例如:

Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(9, label0);
methodVisitor.visitLocalVariable("this", "Lcom/yw/asmtest/MainActivity;", null, label0, label4, 0);

这些都是调试代码和写入变量表的方法,我们不必关心。

剩下的代码,就是我们可以在ASM中所需要的代码。

向大家推荐下我的网站 https://xuyisheng.top/ 专注 Android-Kotlin-Flutter 欢迎大家访问