前言
为什么要学习Java字节码呢,因为我们学的是插桩字节码技术,这块技术的根底就是字节码,要学会字节码的阅读和字节码的编写,虽然现在很多工具可以帮我们阅读和编写,但最根本的知识还是要理解的。万层楼高从地起,打好基础是关键。
字节码和ClassFile关系
ClassFile是以.class
结尾的二进制文件,而该二进制文件中存储的内容就是16进制的Java字节码,在我们学习的插桩技术中,本质就是修改Java字节码文件,也就是要修改ClassFile,读懂Java字节码的基础就是要读懂ClassFile的意思
ClassFile解读
ClassFile是字节码存储文件,规定了规范,我们只需要按照规范的内容进行解析和解读即可知道表达的意思,我们通过简单的例子来演示
1、Hello Word
简单的写一段HelloWord代码
package hensen;
public class Demo {
public static void main(String[] args) {
System.out.println("Hello World.");
}
}
通过javac生成class文件,通过Editor010读取里面的十六进制格式的内容
通过javap -v 查看class
文件的结构,这个接口是java帮我们打印出来的,我们这次目的是手动解析成下面的格式
文章会逐步通过字节码的规范,将下面十六进制内容按照顺序,如下图那样进行逐步解析
在解析之前可以先大概看看整个Class文件的可视化,方便后面的理解
2、ClassFile结构解析
Class文件的格式,解析出来的结构如下
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];
}
- u1: 表示占用1个字节,2个16进制字符
- u2: 表示占用2个字节,4个16进制字符
- u4: 表示占用4个字节,8个16进制字符
- u8: 表示占用8个字节,16个16进制字符
3、字节码解析案例
(一)魔数
从案例中看到:
CAFE BABE
第1 - 4个字节表示magic
魔数,魔数的作用是Class文件的标识,虚拟机会判断这个文件是否为一个能被虚拟机接受的Class文件
(二)文件版本
从案例中看到:
0000 0034
- 第5 - 6个字节表示
minor_version
JDK的次版本号 - 第7 - 8个字节表示
major_version
JDK的主版本号
JDK版本 | 次版本号 | 主版本号 | 十进制 |
---|---|---|---|
JDK1.7 | 0000 | 0033 | 51 |
JDK1.8 | 0000 | 0034 | 52 |
(三)常量池
常量池中包含多种定义的常量属性和字段,其中官方给出定义好的常量表的规范,我们按照对应的说明进行解析即可
从案例中看到:
001D
- 前2个字节表示常量池个数,其值为29,表示一共有29 - 1 = 28个常量
- 后N个字节表示常量池的具体内容
确定常量池数量后,要确定常量池内容
cp_info {
u1 tag;
u1 info[];
}
常量池的内容都会有个统一个格式,前1个字节表示类型,后面的字节表示常量池的值
①第1个常量
0A 00 06 00 0F
类型 | 长度 | 字段值 | 说明 |
---|---|---|---|
CONSTANT_Methodref_info | u1 | tag(0x0A) | 类中方法的符号引用 |
— | u2 | class_index | 指向声明方法的类描述符CONSTANT_Class_info的索引项 |
— | u2 | name_and_type_index | 指向名称及类型描述符CONSTANT_NameAndType的索引项 |
通过常量池寻表,可以发现,该常量项是方法引用
- class_index:0x0006(#6)
- name_and_type_index:0x000F(#15)
②第2个常量
09 00 10 00 11
类型 | 长度 | 字段值 | 说明 |
---|---|---|---|
CONSTANT_Fieldref_info | u1 | tag(0x09) | 字段的符号引用 |
— | u2 | class_index | 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项 |
— | u2 | name_and_type_index | 指向字段描述符CONSTANT_NameAndType的索引项 |
通过常量池寻表,可以发现,该常量项是变量引用
- class_index:0x0010(#16)
- name_and_type_index:0x0011(#17)
③第3个变量
08 00 12
类型 | 长度 | 字段值 | 说明 |
---|---|---|---|
CONSTANT_String_info | u1 | tag(0x08) | 字符串类型字面量 |
— | u2 | string_index | 指向字符串字面量的索引 |
通过常量池寻表,可以发现,该常量项是字符串常量
- string_index:0x0012(#18)
④第4个变量
0A 00 13 00 14
通过常量池寻表,可以发现,该常量项是方法引用
- class_index:0x0013(#19)
- name_and_type_index:0x0014(#20)
⑤第5个变量
07 00 15
类型 | 长度 | 字段值 | 说明 |
---|---|---|---|
CONSTANT_Class_info | u1 | tag(0x07) | 类或接口的符号引用 |
— | u2 | name_index | 指向全限定名常量项的索引 |
通过常量池寻表,可以发现,该常量项是类信息
- name_index:0x0015(#21)
⑥第6个变量
07 00 16
通过常量池寻表,可以发现,该常量项是类信息
- name_index:0x0016(#22)
⑦第7个变量
01 00 06 3C 69 6E 69 74 3E
类型 | 长度 | 字段值 | 说明 |
---|---|---|---|
CONSTANT_Utf8_info | u1 | tag(0x01) | UTF-8编码的字符串 |
— | u2 | length | UTF-8编码的字符串占用的字节数 |
— | u1 | byte[length] | 长度为length的UTF-8编码的字符串 |
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x0006(6个字节)
- byte[length]:0x3C 69 6E 69 74 3E()
⑧第8个变量
01 00 03 28 29 56
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x0003(3个字节)
- byte[length]:0x28 29 56(()V)
⑨第9个变量
01 00 04 43 6F 64 65
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x0004(4个字节)
- byte[length]:0x43 6F 64 65(Code)
⑩第10个变量
01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x000F(15个字节)
- byte[length]:0x4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65(LineNumberTable)
①①第11个变量
01 00 04 6D 61 69 6E
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x0004(4个字节)
- byte[length]:0x01 00 04 6D 61 69 6E(main)
①②第12个变量
01 00 16 28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x0016(22个字节)
- byte[length]:0x28 5B 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56((Ljava/lang/String;)V)
①③第13个变量
01 00 0A 53 6F 75 72 63 65 46 69 6C 65
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x000A(10个字节)
- byte[length]:0x53 6F 75 72 63 65 46 69 6C 65(SourceFile)
①④第14个变量
01 00 09 44 65 6D 6F 2E 6A 61 76 61
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x0009(9个字节)
- byte[length]:0x44 65 6D 6F 2E 6A 61 76 61(Demo.java)
①⑤第15个变量
0C 00 07 00 08
类型 | 长度 | 字段值 | 说明 |
---|---|---|---|
CONSTANT_NameAndType_info | u1 | tag(0x0C) | 字段或方法的部分符号引用 |
— | u2 | name_index | 指向该字段或方法名称常量项的索引 |
— | u2 | descriptor_index | 指向该字段或方法描述符常量项的索引 |
通过常量池寻表,可以发现,该常量项是方法名的索引
- name_index:0x0007(#7)
- descriptor_index:0x0008(#8)
①⑥第16个变量
07 00 17
通过常量池寻表,可以发现,该常量项是类信息
- name_index:0x0017(#23)
①⑦第17个变量
0C 00 18 00 19
通过常量池寻表,可以发现,该常量项是方法名的索引
- name_index:0x0018(#24)
- descriptor_index:0x0019(#25)
①⑧第18个变量
01 00 0C 48 65 6C 6C 6F 20 57 6F 72 6C 64 2E
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x000C(12个字节)
- byte[length]:0x48 65 6C 6C 6F 20 57 6F 72 6C 64 2E(Hello World)
①⑨第19个变量
07 00 1A
通过常量池寻表,可以发现,该常量项是类信息
- name_index:0x001A(#26)
②⑩第20个变量
0C 00 1B 00 1C
通过常量池寻表,可以发现,该常量项是方法名的索引
- name_index:0x001B(#27)
- descriptor_index:0x001C(#28)
②①第21个变量
01 00 0B 68 65 6E 73 65 6E 2F 44 65 6D 6F
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x000B(11个字节)
- byte[length]:0x68 65 6E 73 65 6E 2F 44 65 6D 6F(hensen/Demo)
②②第22个变量
01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x0010(16个字节)
- byte[length]:0x6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74(java/lang/Object)
②③第23个变量
01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x0010(16个字节)
- byte[length]:0x6A 61 76 61 2F 6C 61 6E 67 2F 53 79 73 74 65 6D(java/lang/System)
②④第24个变量
01 00 03 6F 75 74
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x0003(3个字节)
- byte[length]:0x6F 75 74(out)
②⑤第25个变量
01 00 15 4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x0015(21个字节)
- byte[length]:0x4C 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D 3B(Ljava/io/PrintStream;)
②⑥第26个变量
01 00 13 6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x0013(19个字节)
- byte[length]:0x6A 61 76 61 2F 69 6F 2F 50 72 69 6E 74 53 74 72 65 61 6D(java/io/PrintStream)
②⑦第27个变量
01 00 07 70 72 69 6E 74 6C 6E
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x0007(7个字节)
- byte[length]:0x70 72 69 6E 74 6C 6E(println)
②⑧第28个变量
01 00 15 28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56
通过常量池寻表,可以发现,该常量项是UTF8
- length:0x0015(21个字节)
- byte[length]:0x28 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 29 56(Ljava/lang/String;)V
(四)访问标志access_flags
access_flags志是一种掩码标志,用于表示对该类或接口的访问权限
标志名称 | 值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 标记public |
ACC_FINAL | 0x0010 | 标记final |
ACC_SUPER | 0x0020 | 当调用到invokespecial指令时,需要特殊处理的父类方法 |
ACC_INTERFACE | 0x0200 | 标记接口 |
ACC_ABSTRACT | 0x0400 | 标记抽象类 |
ACC_SYNTHETIC | 0x1000 | 标记由编译器产生的,不存在于源码中 |
ACC_ANNOTATION | 0x2000 | 标记注解类型 |
ACC_ENUM | 0x4000 | 标记枚举类型 |
案例中为
00 21
- access_flags:0x0021(由0x0001和0x0020进行或运算得来)也就是说该类的访问标志是public并且允许使用invokespecial字节码指令的新语义
(五)类索引this_class
类索引指的是当前类的名字,指向当前常量池的Class类型,案例中为
00 05
- this_class:0x0005(#5) 表示第5个常量池的位置,即为hensen/Demo
(六)父类索引super_class
父类索引指的是当前类的父类信息,指向当前常量池的Class类型,案例中为
00 06
- super_class:0x0006(#6) 表示第6个常量池的位置,即为java/lang/Object
(七)接口计数器interfaces_count
接口计数器表示当前类实现的接口数量,案例中为
00 00
- interfaces_count:0x0000,表示当前接口数量为0,如果是0的情况,则表示当前的接口表并没有数据,则不需要解析接口表的数据了
(八)字段计数器fields_count
字段计数器表示当前类声明的所有字段,包括类变量和实例变量,案例中为
00 00
- fields_count:0x0000,表示当前字段数量为0,如果是0的情况,则表示当前的字段表并没有数据,则不需要解析字段表的数据了
(九)方法计数器methods_count
方法计数器表示当前类声明的所有方法,案例中为
00 02
- methods_count:0x0002,表示当前方法数量为2
(十)方法表methods[]
方法表包含此类声明的所有方法,包括实例方法,类方法,实例初始化方法以及任何类或接口初始化的方法,在方法表中也会有特定的结构要去解析
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
根据对应的字段结构,映射到案例中为
①第1个方法
00 01 00 07 00 08 00 01 00 09 00 00 00 1D 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00 00 00 06 00 01 00 00 00 03
- access_flags:0x0001(表示方法访问标识,表示ACC_PUBLIC标识)
- name_index:0x0007(指向了常量池第7个常量)
- descriptor_index:0x0008(指向了常量池第8个常量()V)
- attributes_count:0x0001(该方法的属性表一共有1个属性)
- attribute_info
- attribute_name_index:0x0009(指向了常量池第9个常量Code)
- attribute_length:0x0000 001D (29个属性长度)
- info:0x00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00 00 00 06 00 01 00 00 00 03
- 前2个字节为max_stack属性,表示操作数栈深度的最大值为1(0x0001)
- 紧接着2个字节为max_locals属性,表示局部变量表所需的存储空间为1个Slot(0x0001)
- 紧接着4个字节为code_length属性,表示生成字节码指令的长度为5(0x0000 0005)
- 紧接着5个字节为0x2AB70001B1,表示字节码指令,通过字节码查表
- 0x2A对应的指令为aload_0
- 0xB7对应的指令为invokespecial,这个方法有一个u2类型的参数说明具体调用哪一个方法
- 0x0001对应的指令为invokespecial的参数,查常量池得0x0001对应的常量为实例构造方法
- 0xB1对应的指令为return,返回值为void
- 紧接着2个字节为异常表长度,这里表示没有异常表数据(0x0000)
- 紧接着2个字节为属性表的长度,这里会有个属性值(0x0001)
- 紧接着为attribute_info属性,表示属性字段为0x00 0A 00 00 00 06 00 01 00 00 00 03
- 0x000A对应attribute_name_index指向了第10个常量池为LineNumberTable
- 0x00000006对应attribute_length为6个字节长度的字节数据
- 0x0001对应line_number_table_length属性,表示长度为1
- 0x00000003对应line_number_info属性,其包含了start_pc和line_number两个u2类型的数据项
- 0x0000是字节码行号
- 0x0003是Java源码行号
在分析完成后,我们可以找到之前javap帮我们生成的代码,对应上图片的内容,就是我们解析的内容
在属性表中,解析过程中涉及到一些数据结构,这里提供出来
比如在刚开始解析的时候,attribute_name_index指向Code类型的时候,其实是需要参考Code的属性表结构
再往下解析的
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{ u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
再比如解析到attribute_info的时候,attribute的属性表结构
如下
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
再比如解析到attribute_name_index指向LineNumberTable的时候,其LineNumberTable的属性表结构
如下
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
line_number_info line_number_table[line_number_table_length];
}
line_number_info {
u2 start_pc;
u2 line_number;
}
②第2个方法
00 09 00 0B 00 0C 00 01 00 09 00 00 00 25 00 02 00 01 00 00 00 09 B2 00 02 12 03 B6 00 04 B1 00 00 00 01 00 0A 00 00 00 0A 00 02 00 00 00 05 00 08 00 06
- access_flags:0x0009(表示方法访问标识,表示ACC_PUBLIC和ACC_STATIC标识)
- name_index:0x000B(指向了常量池第11个常量main)
- descriptor_index:0x000C(指向了常量池第12个常量([Ljava/lang/String;)V)
- attributes_count:0x0001(该方法的属性表一共有1个属性)
- attribute_info
- attribute_name_index:0x0009(指向了常量池第9个常量Code)
- attribute_length:0x0000 0025 (37个属性长度)
- info:0x00 02 00 01 00 00 00 09 B2 00 02 12 03 B6 00 04 B1 00 00 00 01 00 0A 00 00 00 0A 00 02 00 00 00 05 00 08 00 06
- 前2个字节为max_stack属性,表示操作数栈深度的最大值为2(0x0002)
- 紧接着2个字节为max_locals属性,表示局部变量表所需的存储空间为1个Slot(0x0001)
- 紧接着4个字节为code_length属性,表示生成字节码指令的长度为9(0x0000 0009)
- 紧接着9个字节为0xB200021203B60004B1,表示字节码指令,通过字节码查表可知,这里就不再赘述了
- 紧接着2个字节为异常表长度,这里表示没有异常表数据(0x0000)
- 紧接着2个字节为属性表的长度,这里会有个属性值(0x0001)
- 紧接着为attribute_info属性,表示属性字段为0x00 0A 00 00 00 0A 00 02 00 00 00 05 00 08 00 06
- 0x000A对应attribute_name_index指向了第10个常量池为LineNumberTable
- 0x0000000A对应attribute_length为10个字节长度的字节数据
- 0x0002对应line_number_table_length属性,表示长度为2
- 0x00000005对应line_number_info属性,其包含了start_pc和line_number两个u2类型的数据项
- 0x0000是字节码行号
- 0x0005是Java源码行号
- 0x00080006对应line_number_info属性,其包含了start_pc和line_number两个u2类型的数据项
- 0x0008是字节码行号
- 0x0006是Java源码行号
在分析完成后,我们可以找到之前javap帮我们生成的代码,对应上图片的内容,就是我们解析的内容
(十一)属性计数器attributes_count
属性计数器的值表示当前类的属性表中的属性数量,案例中为
00 01
- attributes_count:0x0001,表示当前属性数量为1
(十二)属性表attributes[]
属性表对应的数据结构
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
案例中为
00 0D 00 00 00 02 00 0E
- attribute_name_index:0x000D(指向了常量池第13个常量SourceFile)
- attribute_length:0x0000 0002 (2个)
- info:0x00 0E(指向了常量池第14个常量Demo.java)
SourceFile属性的表结构如下图所示
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index;
}
在分析完成后,我们可以找到之前javap帮我们生成的代码,对应上图片的内容,就是我们解析的内容
总结
我们到这里就完全分析完整个Class文件的内容,对字节码又进一步的认识,能坚持得下来的同学应该也是不多,只有沉下心来耐心解析完才会有所进步和收获吧。