简单介绍
首先先简单描述一下JVM和字节码。JVM(java虚拟机)是java的核心组件,java(也可是scala、grovvy等语言)通过编译器编译成字节码文件(.class文件),并在任何一台状态JVM的设备上运行。当JVM加载字节码文件时候,会通过解释或者实时编译的方式将字节码转换成具体的机器指令。
从上述可以看出来,字节码是一种介乎于源代码和机器指令之间的语言,可以使java程序达到"一次编译,到处运行"的目标。
字节码的结构和组成
字节码由以下几个部分组成
1. 魔数:字节码文件前四个字节是魔数,用于标识这是一个有效的字节码文件。魔数固定为
0xCAFEBABE
2. 版本信息:包括主版本号和次版本号,表示生成字节码的JDK版本
3. 常量池:包含字节码文件中所有的常量和符号引用,包含类和接口完全限定名、字段名称和描述符、方法名称和描述符、final常量值等
4. 类访问表示:定义了类的访问权限和属性,如public、private、abstract、final
5. 类信息:包含当前类和父类的完全限定名
6. 接口信息:包含实现接口的完全限定名
7. 字段表:包含了类或接口中声明的所有字段信息
8. 方法表:包含了类或接口中声明的所有方法信息
9. 类属性表:包含了类或接口中的一些额外信息,例如源文件名、注解信息等
如何查看和理解字节码
我们用一个例子来理解字节码中各个组成部分
下图为源码文件和字节码文件
下图为class字节码文件结构
1、魔数
首先我们看源码文件和字节码文件的图,结合class字节码文件结构。可以看出字节码文件中, 前四个字节CA FE BA BE便是上文所说的魔数。即划线部分
2、版本信息
00 00 00 34为jdk版本号,16进制转10进制后,副版本号为0,主版本号是52。根据版本号和jdk映射关系,可以确定对应的是JDK8。版本信息和jdk映射关系图如下
3、常量池
常量池的"常量类型和结构"图如下
00 1C是常量计数器,换算成十进制为28,说明有28-1个常量(下标为0的未使用,需减1。00000000行 A列的值为0A,换算成10进制为10。结合"常量类型和结构"图可知,常量类型为CONSTANT_Methodref_info,属于符号引用,字节长度为u1+u2+u2,也就是字节码文件中第一行的A-E列的值:0A 00 06 00 16,其中0A为固定的tag值,00 06换算成10进制为6,我们记做cp_info#6,00 16换算成10进制为22,我们记做cp_info#22。同理,按照常量类型和类型对应的长度,我们梳理剩下的常量列,我们有以下图表格
如何我们使用javap -v TestClass.class命令会出现如下图的输出,可以看到图中Constant pool下面部分和我们上图的excel中的细节值描述部分相同
4、类访问表示
根据class字节码文件结构,常量池后面的字节为类访问标识(长度u2),类访问标识为00 21。参考下图,下图的计算规则为位或。00 21由0x0001位为真,0x0020位为真,其余为否得出。最终访问的标记为ACC_PUBLIC、ACC_SUPER。可以看出来该类的访问标识为为public
5、类信息
类的索引集合(3个u2长度+ 索引个数乘u2长度)。类索引 00 05,父类索引 00 06,接口计数器 00 00
类索引00 05,对应着常量池中的cp_info#5,而cp_info#5又对应着cp_info#26,可以看出来为 class org/zhangzhao/order/TestClass
父类索引00 06,对应着常量池中的cp_info#6,而cp_info#6又对应着cp_info#27,可以看出来为父类为 class java/java/Object
接口计数器00 00 ,也就是0,说明没有接口
6、接口信息
因为没有接口,所以跳过接口信息
7、字段表
字段表前两位是字段个数,字段表中每个成员都是field_info结构,field_info又由access_flags(访问标识)、name_index(字段名索引)、descriptor_index(字段描述符索引)、attributes_count(属性数量)和attributes(属性表)组成。
我们当前的字节码文件中,前两位是00 02,代表着2个字段。首先我们解析第一个字段项:
1. access_flags(访问标识): 表示字段的访问权限和特性。也是通过位或的计算规则。也就是00 02,为0x0002为真,其余为否,也就是private
2. name_index(字段名索引): 指向常量池中存储字段名的常量项的索引。在字节码文件中为00 07 ,也就是cp_info#7对应着 a
3. descriptor_index(字段描述符索引): 指向常量池中存储字段描述符的常量项的索引。在字节码文件中为00 08,对应着cp_info#8,查看常量池中为 Ljava/lang/String;基础类型有,可以看出L开头的为引用类型
4. attributes_count(属性数量): 表示与字段关联的属性的数量。属性数量为 00 00 ,也就是0,不用考虑属性表
5. attributes(属性表): 包含与字段关联的属性信息。因为没有属性表,所以不用考虑
综上上述,可以得到我们有个private java.lang.String类型的字段名为a的字段。因为字段个数为2,所以继续解析另外一个字段,得到public I b,I的含义为int类型。
8、方法表
方法表的前两位是方法计数器,方法表中每个成员都是method_info格式,method_info又由access_flags(访问标识)、name_index(方法名索引)、descriptor_index(字方法述符索引)、attributes_count(属性数量)和attributes(属性表)组成。
当前字节码文件中,我们的方法计数器为00 02,也就是有2个方法。我们逐个进行解析
8.1 方法表的解析
1. access_flags(访问标识): :表示方法的访问权限和特性。在字节码文件中为00 01,也就是public。
2. name_index(字段名索引): 指向常量池中存储方法名的常量项的索引。在字节码文件中为00 0B,对应着常量池中的cp_info#11,
3. descriptor_index(字段描述符索引): 指向常量池中存储方法描述符的常量项的索引。在字节码文件中为00 0C,对应着常量池中的cp_info#12, ()V
4. attributes_count(属性数量): 表示与方法关联的属性的数量。在字节码文件中为00 01,也就是有1个属性
5. attributes(属性表): 包含与方法关联的属性信息。具体解析详见 8.2 属性表的解析
8.2 属性表的解析
属性名索引为00 0D,对应着cp_info#13,也就是Code,含义为java代码编译成的字节码指令。
attribute_length为4字节,也就是00 00 00 44,对应着属性长度为:68。
info为属性表内容,长度为68字节。也就是从00000150行的7列到00000190行的B列。
属性名为Code的属性表属性如下图:
全部属性和说明地址
docs.oracle.com/javase/spec…
我们对具体的Code属性进行解析得到下表:
其中Code属性表属性中的存储字节码指定由于过多,不做详细介绍,可以参考字节码指令的详细说明:docs.oracle.com/javase/spec…
至此,我们方法表解析完毕,如果我们通过javap -v TestClass.class命令可以看出我们解析出来的内容和下图一致
9、类属性表
前两位为00 01,是方法表的结束标识,非类属性表内容。类属性表有三部分组成
解析出来的内容即为 SourceFile: "TestClass.java"。
总结
至此,我们全部的字节码解析完毕。可以看出,我们完全可以通过按照java语言规范,修改字节码文件,达到对现有代码进行增强和扩展的目的。