如何看懂字节码文件
测试使用的类
public class Test {
public int i;
public long y;
}
基础概念
每个Class文件的头4个字节被称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的Class文件。不仅是Class文件,很多文件格式标准中都有使用魔数来进行身份识别的习惯,譬如图片格式,如GIF或者JPEG等在文件头中都存有魔数。使用魔数而不是扩展名来进行识别主要是基于安全考虑,因为文件扩展名可以随意改动。
紧接着魔数的4个字节存储的是Class文件的版本号:第5和第6个字节是次版本号(Minor Version),第7和第8个字节是主版本号(Major Version)。Java的版本号是从45开始的,JDK 1.1之后的每个JDK大版本发布主版本号向上加1(JDK 1.0~1.1使用了45.0~45.3的版本号),高版本的JDK能向下兼容以前版本的Class文件,但不能运行以后版本的Class文件,因为《Java虚拟机规范》在Class文件校验部分明确要求了即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的Class文件。
这里使用nodepad++安装HEX插件【可以用任意具有HEX查看功能的工具】打开class文件观察数据
首先是魔数cafe babe
,代表次版本号的第5个和第6个字节值为0x0000
,而主版本号的值为0x003d
,也即是十进制的61
,即JDK17
编译的class文件。
常量池
紧接着主、次版本号之后的是常量池入口,常量池可以比喻为Class文件里的资源仓库,它是Class文件结构中与其他项目关联最多的数据,通常也是占用Class文件空间最大的数据项目之一,另外,它还是在Class文件中第一个出现的表类型数据项目。
常量池中主要存放两大类常量:字面量(Literal
)和符号引用(Symbolic References
)。字面量比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。而符号引用则属于编译原理方面的概念,主要包括下面几类常量:·被模块导出或者开放的包(Package
)·类和接口的全限定名(Fully Qualified Name
)·字段的名称和描述符(Descriptor
)·方法的名称和描述符·方法句柄和方法类型(Method Handle、Method Type、Invoke Dynamic
)·动态调用点和动态常量(Dynamically-Computed Call Site、Dynamically-Computed Constant
)。
常量池中的每一项常量都是一个表,在 JDK 7 之前共有 11 种结构不同的表,在 JDK 7 中为了更好的支持动态语言调用,又增加了 3 种。都是CONSTANT
开头,info
结尾。我们以下表为基础来解析class文件。
其中 CONSTANT_Utf8_info
类型的常量,它的 length
值说明了这个 UTF-8 编码的字符串长度是多少字节,bytes
的值为长度为 length
字节的 UTF-8 缩略编码表示的字符串
由于 Class 文件中方法、字段等都需要引用 CONSTANT_Utf8_info
型常量来描述名称,所以 CONSTANT_Utf8_info
型常量的最大长度也就是 Java 中方法、字段名的最大长度,即 u2
的 65535,也就是说最大 65535 字节,即 64KB。
由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count)。与Java中语言习惯不同,这个容量计数是从1而不是0开始的,本例图中第8,9位便是我们常量池的大小。(偏移地址:0x00000008
)为十六进制数0x0014
,即十进制的20,这就代表常量池中有19项常量,索引值范围为1~19;
表结构起始的第一位是个u1类型的标志位,代表着当前常量属于哪种常量类型,即图中a位置;这里就从图中a开始解读:
常量池的第一项常量,它的标志位(偏移地址:0x0000000A
)是0x0a
,查表获取类型如下图,此处是CONSTANT_Methodref_info
类型的tag
,得知tag
后跟2个u2
类型的index
索引,分别是(偏移地址:0x0000000C
)0x02
和(偏移地址:0x0000000E
)0x03
[这两个索引具体是什么我们在下面解析]。
第一个常量池数据结束,目前得到的常量池:
[01] CONSTANT_Methodref_info #2,#3
接着是第二个常量池:它的标志位(偏移地址:0x0000000F
)是0x07
,依然是查类型表07;此处是CONSTANT_Class_info
类型的tag
后跟1个u2
类型的index
索引(偏移地址:0x0000000C
)0x04
第二个常量池数据结束,目前得到的常量池:
[01] CONSTANT_Methodref_info #2,#3
[02] CONSTANT_Class_info #4
第三个常量池:它的标志位(偏移地址:0x00000012
)是0x0c
,依然是查类型表12;此处是CONSTANT_NameAndType
类型的tag
,后跟2个u2
类型的index
索引,分别是(偏移地址:0x00000014
)0x05
和(偏移地址:0x00000016
)0x06
第三个常量池数据结束,目前得到的常量池:
[01] CONSTANT_Methodref_info #2,#3
[02] CONSTANT_Class_info #4
[03] CONSTANT_NameAndType #5,#6
第四个常量池:它的标志位(偏移地址:0x00000017
)是0x01
,查类型表01;此处是CONSTANT_Utf8_info
类型的tag
;
跟1个u2
类型的长度(偏移地址:0x00000019
)0x10
,长度是16【length值说明了这个UTF-8编码的字符串长度是多少字节,它后面紧跟着的长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。UTF-8缩略编码与普通UTF-8编码的区别是:从’\u0001’到’\u007f’之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示,从’\u0080’到’\u07ff’之间的所有字符的缩略编码用两个字节表示,从’\u0800’开始到’\uffff’之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示】,这里后面数据是(偏移地址:0x00000029
) java/lang/object
第四个常量池数据结束,目前得到的常量池:
[01] CONSTANT_Methodref_info #2,#3
[02] CONSTANT_Class_info #4
[03] CONSTANT_NameAndType #5,#6
[04] CONSTANT_Utf8_info java/lang/Object
从上面的第二个常量池[02] CONSTANT_Class_info #4
知道它的索引是第四个常量池,而第四个常量池便是我们的[04] CONSTANT_Utf8_info java/lang/Object
.
jclasslib
以此类推,便可以获得所有的常量池数据;这里虽然抽象但是具体的步骤便是如此,为了让大家能明白,我们使用IntelliJ IDEA
的jclasslib
插件来验证上面的解析。
第四个常量池长度16,字符串便是我们解析的 java/lang/Object
javap
我们再使用jdk提供的命令来验证javap -c -v Test.class
:
Classfile /D:/project/java/blog/target/classes/com/mystic/ycc/blog/test/Test.class
Last modified 2024-5-22; size 319 bytes
MD5 checksum d41bbe1c5d1a6eb390ca59cadaeb48d3
Compiled from "Test.java"
public class com.mystic.ycc.blog.test.Test
minor version: 0
major version: 61
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #2.#3 // java/lang/Object."<init>":()V
#2 = Class #4 // java/lang/Object
#3 = NameAndType #5:#6 // "<init>":()V
#4 = Utf8 java/lang/Object
#5 = Utf8 <init>
#6 = Utf8 ()V
#7 = Class #8 // com/mystic/ycc/blog/test/Test
#8 = Utf8 com/mystic/ycc/blog/test/Test
#9 = Utf8 i
#10 = Utf8 I
#11 = Utf8 y
#12 = Utf8 J
#13 = Utf8 Code
#14 = Utf8 LineNumberTable
#15 = Utf8 LocalVariableTable
#16 = Utf8 this
#17 = Utf8 Lcom/mystic/ycc/blog/test/Test;
#18 = Utf8 SourceFile
#19 = Utf8 Test.java
{
public int i;
descriptor: I
flags: ACC_PUBLIC
public long y;
descriptor: J
flags: ACC_PUBLIC
public com.mystic.ycc.blog.test.Test();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/mystic/ycc/blog/test/Test;
}
SourceFile: "Test.java"
这里的1到4的常量池便是上面一步步解析的数据。