手把手带你读class文件字节码
这是 JVM 系列的第五篇,见 JVM 系列。
示例代码基于 jdk1.8 版本
准备
IDEA 里面如果想看 class 的二进制文件需要下载插件 BinEd ,也有 C 端的。另外查看编译后的 class 文件人类友好插件可以选用
jclasslib Bytecode Viewer ,或者用命令 javap -verbose className
来打印出 class 的文件格式。
class 文件格式
前面的文章(JVM 系列 —— class 文件格式 )讲过一个正确的 class 文件格式长什么样子,另外,再强调一点就是,这个 class 文件指的是 class 文件流,并不是单纯指磁盘上的 class 文件,也可能来自网络字节流。 ClassFile 格式:
1 | ClassFile { |
最简单的一个类
Demo 代码
我们就用最简单的一个类,再加上一个
int
类型的字段作为示例代码,我放在工程里面的,所以会带有包名,如下:demo code 1
2
3
4
5package top.caolizhi.example.jvm.bytecode;
public class Bytecode {
int s = 3;
}class 文件二进制字节码
装完插件后,选中类文件,然后点击菜单 View -> Show Bytecode With Jclasslib 。
右变就会出现友好可读的 class 文件的描述:
我们也可以用命令行来操作,先编译一遍,然后进入到目录target > classes
,在控制台运行命令:javap -verbose top.caolizhi.example.jvm.bytecode.Bytecode
,打印如下:javap -verbose 命令输出 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53E:\Workspaces\Git\caolizhi-personal\jvm\target\classes>javap -verbose top.caolizhi.example.jvm.bytecode.Bytecode
Classfile /E:/Workspaces/Git/caolizhi-personal/jvm/target/classes/top/caolizhi/example/jvm/bytecode/Bytecode.class
Last modified 2021-12-9; size 352 bytes
MD5 checksum e9c0623eb64cce65bad7f5f5b3e7678c
Compiled from "Bytecode.java"
public class top.caolizhi.example.jvm.bytecode.Bytecode
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#16 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#17 // top/caolizhi/example/jvm/bytecode/Bytecode.s:I
#3 = Class #18 // top/caolizhi/example/jvm/bytecode/Bytecode
#4 = Class #19 // java/lang/Object
#5 = Utf8 s
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Ltop/caolizhi/example/jvm/bytecode/Bytecode;
#14 = Utf8 SourceFile
#15 = Utf8 Bytecode.java
#16 = NameAndType #7:#8 // "<init>":()V
#17 = NameAndType #5:#6 // s:I
#18 = Utf8 top/caolizhi/example/jvm/bytecode/Bytecode
#19 = Utf8 java/lang/Object
{
int s;
descriptor: I
flags:
public top.caolizhi.example.jvm.bytecode.Bytecode();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_3
6: putfield #2 // Field s:I
9: return
LineNumberTable:
line 3: 0
line 5: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Ltop/caolizhi/example/jvm/bytecode/Bytecode;
}
SourceFile: "Bytecode.java"上面这些都是工具帮你把 class 美化了的,但是 JVM 没法这么读,只能一个一个字节的读,接下来,我们从 JVM 的角度去读一下字节码。在
target > classes
目录,找到Bytecode.class
的文件,右键,选中Open As Binary
,class 文件的二进制如下:
接下来,开始一个一个字节的去读,看看 JVM 到底是怎么解析的?
如果我是一个虚拟机,我怎么读 class 文件?
这个 ClassFormat 文件谁规定的,当然是JVM 规范啦~。参考 Java Virtual Machine Specification
读取魔数(
magic
)魔数 1
u4 magic;
就跟公司周三早餐铁打的紫菜饭团一样,魔数也是万年不变的
oxCAFEBABE
,主要就是看着这个文件流是不是 class 文件。关于这个魔数的由来,可以去看看维基百科上 Java 始祖 James Gosling 说过的话 。也就是说,前面的 4 个字节确定是CA
,FE
,BA
,BE
。16 进制里面的只有 A ~ F,cafe 刚好属于 16 进制里面的,又与 java 渊源已久,另外 babe 推测想用 baby 的,毕竟 babe 比较轻浮口语化,可是 y 不在 16 进制的字母里面,所以改成了 e。
class 文件字节的确也是这样:
读取版本号(
minor_version
,major_version
)版本号 1
2u2 minor_version;
u2 major_version;接下来的4个字节是关于版本号的,前面的两个字节表示副版本,后面的两个字节表示主版本,比如这里:
minor_version
值为0x0000
,十进制就是0
,major_version
值为0x0034
,十进制就是52
,而52
对应的是 jdk8,那么这个 class 文件的格式版本号就确定为:52.0
,如果在 jdk11 的环境里面解析这个 class 文件是不被支持的。jdk 版本对应关系:
jdk1.8 jdk9 jdk10 jdk11 52 53 54 55 class 文件字节:
解析常量池(
constant_pool
) 很重要!很重要!很重要!常量池 1
2u2 constant_pool_count;
cp_info constant_pool[constant_pool_count-1];接下来是非常重要的一部分了,后面的解析都会依赖常量池里面的值。先看版本号后面的两个字节,值为
0x0014
,十进制为20
,也就是说这个类的常量池表结构constant_pool
数组大小为 19,0 位置保留,数组的每一个项都是cp_info
结构 ,如下:常量池表中项的结构 cp_info 1
2
3
4cp_info {
u1 tag;
u1 info[];
}其中
tag
是个枚举值,info
根据tag
的值来确定结构,参考 JVM 规范文档 。常量池第 1 项:
先来看看常量池表的第一个项的tag
值0x0A
,十进制为 10,如下图:
该tag
的值在表中找到对应的info
结构为CONSTANT_Methodref_info
,表示,类中方法的符号引用,它的结构如下:tag 为 10 对应的 info 结构 1
2
3
4
5CONSTANT_Methodref_info {
u1 tag; // 10
u2 class_index; // 指向声明方法的类描述符 CONSTANT_Class_info 的索引项
u2 name_and_type_index; // 指向名称以及类型描述符CONSTANT_NameAndType_info 的索引项
}也就是说,在常量池表的第一项中,对应的就是
CONSTANT_Methodref_info
结构,已经知道tag
是 10 了,那么0x0A
后面的两个字节0x0004
,十进制 4,表示的是u2 class_index
,指向常量池的索引 4,我们看到常量池 4 位置的值为:再后面的两个字节
0x0010
,十进制是 16,表示的是u2 name_and_type_index
,指向常量池索引 16,看 16 位置的值为:常量池第 2 项:
按照上面的方法,接着看的tag
值0x09
对应CONSTANT_Fieldref_info
,
结构如下:tag 为 9 对应的 info 结构 1
2
3
4
5CONSTANT_Fieldref_info {
u1 tag; // 9
u2 class_index; // 指向声明字段的类或接口描述符 CONSTANT_Class_info 的索引项
u2 name_and_type_index; // 指向字段描述符CONSTANT_NameAndType_info 的索引项
}u2 class_index
->0x0003
,十进制 3,指向常量池第 3 个位置:<top/caolizhi/example/jvm/bytecode/Bytecode>
u2 name_and_type_index
->0x0011
,十进制 17,指向常量池 17 的位置:常量池第 3 项:
tag
->0x07
,十进制7,对应CONSTANT_Class_info
tag 为 7 对应的 info 结构 1
2
3
4CONSTANT_Class_info {
u1 tag; // 7
u2 name_index; // 全限定名常量项的索引
}u2 name_index
->0x0012
,十进制 18,指向常量池 18 的位置:常量池第 4 项:
tag
->0x07
,十进制7,对应CONSTANT_Class_info
u2 name_index
->0x0013
,十进制 19,指向常量池 19 的位置:常量池第 5 项:
tag
->0x01
,十进制1,对应CONSTANT_Utf8_info
,tag 为 1 对应的 info 结构 1
2
3
4
5CONSTANT_Utf8_info {
u1 tag; // 1
u2 length; // UTF-8 缩略编码字符串占用字节数
u1 bytes[length]; // 长度为 length 的 UTF-8 编码字符串
}0x01
后面的两个字节0x0001
表示length
,值为 1,也就是说接下来的后面 1 一个长度的字节是 UTF-8 编码字符串,即0x73
,ASCII 码表示s
,其实就是类中定义的字段s
,如图:常量池第 6 项:
tag
->0x01
,十进制1,对应CONSTANT_Utf8_info
length
->0x0001
,十进制1,长度为1bytes
->0x49
,对应 ASCII 码I
,字段s
的类型常量池第 7 项:
tag
->0x01
,十进制1,对应CONSTANT_Utf8_info
length
->0x0006
,十进制 6,长度为6bytes
-> 字节:3C 69 6E 69 74 3E
,对应 ASCII 码的<init>
常量池第 8 项:
tag
->0x01
,十进制1,对应CONSTANT_Utf8_info
length
->0x0003
,十进制 3,长度为3bytes
-> 字节:28 29 56
,对应 ASCII 码的()V
常量池第 9 项:
tag
->0x01
,十进制1,对应CONSTANT_Utf8_info
length
->0x0004
,十进制 4,长度为4bytes
-> 字节:43 6F 64 65
,对应 ASCII 码的Code
常量池第 10 项:
tag
->0x01
,十进制1,对应CONSTANT_Utf8_info
length
->0x000F
,十进制 15,长度为15bytes
-> 字节:4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65
,对应 ASCII 码的LineNumberTable
常量池第 11 项:
tag
->0x01
,十进制1,对应CONSTANT_Utf8_info
length
->0x0012
,十进制 18,长度为18bytes
-> 字节:4C 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65
,对应 ASCII 码的LocalVariableTable
常量池第 12 项:
tag
->0x01
,十进制1,对应CONSTANT_Utf8_info
length
->0x0004
,十进制 4,长度为 4bytes
-> 字节:74 68 69 73
,对应 ASCII 码的this
常量池第 13 项:
tag
->0x01
,十进制1,对应CONSTANT_Utf8_info
length
->0x002C
,十进制 44,长度为 44bytes
-> 字节:4C 74 6F 70 2F 63 61 6F 6C 69 7A 68 69 2F 65 78 61 6D 70 6C 65 2F 6A 76 6D 2F 62 79 74 65 63 6F 64 65 2F 42 79 74 65 63 6F 64 65 3B
,对应 ASCII 码的Ltop/caolizhi/example/jvm/bytecode/Bytecode;
常量池第 14 项:
tag
->0x01
,十进制1,对应CONSTANT_Utf8_info
length
->0x000A
,十进制 10,长度为 10bytes
-> 字节:0A 53 6F 75 72 63 65 46 69 6C 65
,对应 ASCII 码的SourceFile
常量池第 15 项:
tag
->0x01
,十进制1,对应CONSTANT_Utf8_info
length
->0x000D
,十进制 13,长度为 13bytes
-> 字节:42 79 74 65 63 6F 64 65 2E 6A 61 76 61
,对应 ASCII 码的Bytecode.java
常量池第 16 项:
tag
->0x0C
,十进制 12,对应CONSTANT_NameAndType_info
tag 为 12 对应的 info 结构 1
2
3
4
5CONSTANT_NameAndType_info {
u1 tag; // 12
u2 name_index; // 指向该字段或方法名称常量项的索引
u2 descriptor_index; // 指向该字段或方法描述符常量项的索引
}name_index
->0x0007
,十进制 7,指向常量池 7 的位置:
descriptor_index
-> 0x0008
,十进制 8,指向常量池 8 的位置:
常量池第 17 项:tag
-> 0x0C
,十进制 12,对应 CONSTANT_NameAndType_info
name_index
-> 0x0005
,十进制 5,指向常量池 5 的位置:
descriptor_index
-> 0x0006
,十进制 6,指向常量池 6 的位置:
常量池第 18 项:tag
-> 0x01
,十进制1,对应 CONSTANT_Utf8_info
length
-> 0x002A
,十进制 42,长度为 42bytes
-> 字节:74 6F 70 2F 63 61 6F 6C 69 7A 68 69 2F 65 78 61 6D 70 6C 65 2F 6A 76 6D 2F 62 79 74 65 63 6F 64 65 2F 42 79 74 65 63 6F 64 65
,对应 ASCII 码的 top/caolizhi/example/jvm/bytecode/Bytecode
常量池第 19 项:tag
-> 0x01
,十进制1,对应 CONSTANT_Utf8_info
,length
-> 0x0010
,十进制 16,长度为 16,bytes
-> 字节:6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74
,对应 ASCII 码的 java/lang/Object
以上就是常量池的解析。
解析类的访问权限(
access_flags
)access_flags 1
u2 access_flags;
接下来的 2 个字节表示的是
access_flags
,表示访问标志位,标注类或接口的访问信息,这里值为0x0021
,查文档可知,既包括ACC_PUBLIC
(0x0001) 又包括ACC_SUPER
(0x0020),jdk1.2以后都有ACC_SUPER
。现在这个类是public
类型的。解析当前类名称(
this_class
)this_class 1
u2 this_class;
访问标志位后面的 2 个字节
0x0003
表示当前类的全限定名,即包名+类名,指向常量池中的索引值。对应常量池 3 的位置:解析父类名称(
super_class
)super_class 1
u2 super_class;
this_class
访问标志位后面的 2 个字节0x0004
表示当前类的父类全限定名,指向常量池中的索引值。对应常量池 4 的位置:java.lang.Object
是所有类的父类。解析接口(
interfaces
)接口项 1
2u2 interfaces_count;
u2 interfaces[interfaces_count];接下来的 2 个字节
0x0000
,十进制 0,没有接口,也就没有interfaces[]
表结构。解析字段(
fields
)字段项 1
2u2 fields_count;
field_info fields[fields_count];字段项的前两个字节表示长度,
0x0001
,十进制 1,只有一个字段,和源码中定义一个字段匹配。field_info
结构如下:field_info 结构 1
2
3
4
5
6
7field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}在
fields_count
字节后面 2 个字节表示字段的访问标志,access_flags
->0x0001
表示public
,各个值对应表参考。name_index
->0x0005
,指向常量池 5 的位置:s
descriptor_index
->0x0006
,指向常量池 6 的位置:I
,有关字段类型参考链接。attributes_count
->0x0000
,没有属性值。至此,字段的解析完毕。
解析方法(
methods
)方法解析 1
2u2 methods_count;
method_info methods[methods_count];方法的解析和字段解析查不多,
method_info
结构如下:method_info 结构 1
2
3
4
5
6
7method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}先看
methods_count
值为:0x0001
,只有一个方法,这里奇怪了,我们没有定义方法,但是为什么会有一个方法呢?继续往下看:
access_flags
->0x0001
,表示public
,可以参考链接。name_index
->0x0007
,表示方法名称,指向常量池的第 7 个索引位置值,<init>
:descriptor_index
->0x0008
,方法描述符,指向常量池的第 8 个索引位置值,()V
attributes_count
->0x0001
,说明有一个属性,属性的结构:attribute_info 结构 1
2
3
4
5attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}接下来的 2 个字节
0x0009
表示attribute_name_index
,指向常量池的索引值 9。attribute_info
中的info
结构类型有很多,可以参考链接 。这里的类型为Code
。attribute_length
-> 占 4 个字节,0x00_00_00_38
,十进制为 56,表示属性长度为 56,也就是这个字节后面的 56 个字节表示的是Code
这个属性。不包括前面attribute_name_index
和attribute_length
占用的 6 个字节。接下来看一下
Code
属性的结构,Code 属性是一个 class 文件最重要的一个属性!Code 属性 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack; // 操作数栈深度最大值
u2 max_locals; // 局部变量表所需要存储的空间,单位是 slot,
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]; // 属性表
}max_stack
->0x0002
,表示操作数栈最大深度是 2,这里可以思考一下为什么是 2 ?max_locals
->0x0001
,表示局部变量表的大小是 1,占用一个 slot。可以看到编译后就已经确定了一个方法的大小。code_length
->00 00 00 0A
,十进制 10,也就是说后面的 10 个字节就是这个方法的字节码指令。code
->2A B7 00 01 2A 06 B5 00 02 B1
,这 10 个字节就是一组字节码指令,在 JVM 中一个字节表示一个指令,最大只有 255 个指令集。查看JVM的指令集表,上面的 10 个字节中的字节码指令如下:
操作码 HEX 含义 aload_0 0x2A 将局部变量表中的第 0 个 slot 中 reference 类型的本地变量推送到操作数栈顶 invokespecial 0xB7 调用类的实例构造方法、private 方法或父类方法,指令后面会带上一个 u2 类型的参数,指向常量池的索引值,表示调用哪个方法 iconst_3 0x06 将 int 类型 3 推到栈顶 putfield 0xB5 用栈顶的值为指定的实例域赋值,指令后面会带上一个 u2 类型的参数,表示给哪一个赋值,指向常量池的索引值 return 0xB1 从当前方法返回 上面的字节码执行流程如下:
exception_table_length
->0x0000
,异常表大小是 0:attributes_count
->0x0002
,Code
属性的属性表大小是 2 个,属性表结构都是
attribute_info
,如下:attribute_info 结构 1
2
3
4
5attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}Code 属性的第 1 个属性:
attribute_name_index
->0x000A
,十进制是 10,指向常量池的索引值LineNumberTable
:LineNumberTable
属性用于记录字节码指令与源代码行号对应的关系,异常抛出时显示行号,debug 调试时用到的行号,结构:LineNumberTable 结构 1
2
3
4
5
6
7
8
9
LineNumberTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 line_number_table_length;
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}attribute_length
->00 00 00 0A
,十进制 10,属性的长度为 10,即后面的 10 个字节描述这个属性。line_number_table_length
->0x0002
,行号表长度为 2,- 行号表第 1 行
start_pc
->0x0000
,字节码行号,也即字节码指令偏移位置line_number
->0x0003
,Java源码行号 - 行号表第 2 行
start_pc
->0x0004
line_number
->0x0005
Code 属性的第 2 个属性:
attribute_name_index
->0x000B
,十进制是 11,指向常量池的索引值LocalVariableTable
:LocalVariableTable
属性用于描述栈桢中局部变量表中的变量与 Java 源码中定义的变量之间的关系,结构:LocalVariableTable 结构 1
2
3
4
5
6
7
8
9
10
11LocalVariableTable_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 local_variable_table_length;
{ u2 start_pc; // 字节码偏移量
u2 length; // 作用范围覆盖的长度
u2 name_index; // 指向常量池的索引值
u2 descriptor_index; // 指向常量池的索引值
u2 index; // 栈桢中局部变量表中 slot 的位置
} local_variable_table[local_variable_table_length];
}attribute_length
->00 00 00 0C
,十进制 12,属性的长度为 12,即后面的 12 个字节描述这个属性。local_variable_table_length
->0x0001
,局部变量表长度为 1,start_pc
->0x0000
,字节码行号,即aload_0
length
->0x000A
,长度为 10,和上面的结合起来就是这个变量在字节码中的作用域范围,10个字节,从字节码偏移量 0 开始 到 10 结束,如下图,可以看拿到覆盖了整个<init>
方法。name_index
->0x000C
,指向常量池 12 的位置descriptor_index
->0x000D
,指向常量池 13 的位置也就是说
this
指向的对象类型是Bytecode
。index
->0x0000
,理所当然,在局部变量表的第 0 个 slot 位置。至此,整个方法解析已经完成了
- 行号表第 1 行
解析属性(
attributes
)属性 1
2u2 attributes_count;
attribute_info attributes[attributes_count];接下来的 2 个字节表示了这个类的属性大小,即
attributes_count
->0x0001
attribute_info
跟上面的方法的属性类型结构都是相同的,具体类型见链接 。attribute_name_index
->0x000E
,指向常量池索引 14 的位置SourceFile
:SourceFile
属性就是记录生成这个 Class 文件的源码文件名称。一般来说,类名和文件名是一致的,除了少数类比如内部类,抛出异常时可以显示错误所属的文件名。SourceFile 属性结构 1
2
3
4
5SourceFile_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 sourcefile_index;
}attribute_length
->00 00 00 02
,长度是 2,后面的 2 个字节描述的是该属性。sourcefile_index
->0x000F
,十进制 15,指向常量池的一个索引值Bytecode.java
至此,该类的属性解析完了
至此,该类解析完了!!!没有多余的字节!
啰嗦一句,解析完了我该干嘛?
解析完了,我把他存起来,下次用到的时候,我再开箱即用,放哪里好呢,放到内存里面,那这个只是我能用,java 应用他们咋用,嗯,我再复制一份给它用,我的是我的,它的还是我的,哈哈~!
手把手带你读class文件字节码