背景介绍
Java源代码被编译为Class文件之后,Class文件结构是JVM加载Class,实例化对象,和进行方法调用的重要依据.
每个 Class 文件都是由 8 字节为单位的字节流组成,所有的 16 位、32 位和 64 位长度的数 据将被构造成 2 个、4 个和 8 个 8 字节单位来表示。多字节数据项总是按照 Big-Endian1的顺 序进行存储。在Java SDK中,访问这种格式的数据可以使用java.io.DataInput、 java.io.DataOutput 等接口和 java.io.DataInputStream 和 java.io.DataOutputStream 等类来实现。
Class 文件的内容可用一组私有数据类型来表示,它们包括 u1,u2 和 u4,分别代 表了1、2和4个字节的无符号数。在 Java SDK 中这些类型的数据可以通过实现接口 java.io.DataInput 中的 readUnsignedByte、readUnsignedShort 和 readInt 方法进 行读取。
ClassFile 结构 每一个 Class 文件对应于一个如下所示的 ClassFile 结构体,其包含的属性如下表:
几个问题
- 运行时常量池和静态常量池有什么区别?
- Class文件里面都有什么内容?
- Class文件反汇编之后的格式里面分别有什么,尝试解读里面方法中的汇编指令
- 本地变量表和操作数栈是如何工作?
查看Class文件
以十六进制查看Class文件
技巧:
vim + xxd = 十六进制编辑器vim + xxd = 十六进制编辑器
- vim -b xxx.class 可以以二进制将class文件打开;
- vim内调用::%!xxd 以十六进制显示当前文件;
- 修改完成之后,如果想保存,执行以下命令把十六进制转换回二进制- :%!xxd -r
输出包括行号,本地变量反汇编等信息
javap
- -v -verbose:输出附加信息(包括行号、本地变量表、反汇编等信息)
- -c:对代码进行反汇编
- 如:
javap -c xxx.class
javap -verbose Test.class
更多关于javap的介绍:docs.oracle.com/javase/7/docs/tech...
关于反汇编:
反汇编(Disassembly):把目标代码转为汇编代码的过程,也可以说是把机器语言转换为汇编语言代码、低级转高级的意思。软件一切神秘的运行机制全在反汇编代码里面。
Class文件解读
JVM规范中的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]; // 属性表 | |
} |
Class文件是一组以8位字节为基础单位的二进制流。类结构有两种数据类型:
- 无符号数:无符号数属于基本属性类型,用u1, u2, u4, u8分别代表1个字节,2个字节,4个字节和8个字节的无符号数,可以用它描述数字、索引引用、数量值或者utf8编码的字符串值;
- 表:由多个无符号数或者其他表作为数据项构成的复合数据类型,以命名_info结尾。
cafe babe 0000 0034 001d 0a00 0600 0f09 | |
0010 0011 0800 120a 0013 0014 0700 1507 | |
0016 0100 063c 696e 6974 3e01 0003 2829 | |
5601 0004 436f 6465 0100 0f4c 696e 654e | |
756d 6265 7254 6162 6c65 0100 046d 6169 | |
6e01 0016 285b 4c6a 6176 612f 6c61 6e67 | |
2f53 7472 696e 673b 2956 0100 0a53 6f75 | |
7263 6546 696c 6501 0008 4c6f 672e 6a61 | |
7661 0c00 0700 0807 0017 0c00 1800 1901 | |
000c 6865 6c6c 6f20 776f 726c 6421 0700 | |
1a0c 001b 001c 0100 1263 6f6d 2f68 656c | |
6c6f 2f74 6573 742f 4c6f 6701 0010 6a61 | |
7661 2f6c 616e 672f 4f62 6a65 6374 0100 | |
106a 6176 612f 6c61 6e67 2f53 7973 7465 | |
6d01 0003 6f75 7401 0015 4c6a 6176 612f | |
696f 2f50 7269 6e74 5374 7265 616d 3b01 | |
0013 6a61 7661 2f69 6f2f 5072 696e 7453 | |
7472 6561 6d01 0007 7072 696e 746c 6e01 | |
0015 284c 6a61 7661 2f6c 616e 672f 5374 | |
7269 6e67 3b29 5600 2100 0500 0600 0000 | |
0000 0200 0100 0700 0800 0100 0900 0000 | |
1d00 0100 0100 0000 052a b700 01b1 0000 | |
0001 000a 0000 0006 0001 0000 0003 0009 | |
000b 000c 0001 0009 0000 0025 0002 0001 | |
0000 0009 b200 0212 03b6 0004 b100 0000 | |
0100 0a00 0000 0a00 0200 0000 0500 0800 | |
0600 0100 0d00 0000 0200 0e |
- 根据以上的Class文件结构,我们可以梳理出以下的Class文件结构图:
魔数 magic
用于标识这个文件的格式,Class文件格式的魔数为 0xCAFEBABE。 | |
Class 文件的第 1 - 4 个字节代表了该文件的魔数。 |
魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的 Class 文件。魔数值固定为 0xCAFEBABE,不会改变。魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的 Class 文件。魔数值固定为 0xCAFEBABE,不会改变。
副版本号 minor_version,主版本号 major_version
- Class 文件的第 5 - 6 个字节代表了 Class 文件的副版本号。
- Class 文件的第 7 - 8 个字节代表了 Class 文件的主版本号。
主版本号和次版本号共同决定了类文件格式的版本。 如果类文件的主版本号为M,次版本号为m,则将其类文件格式的版本表示为M.m。
因此,可以按字典顺序对类文件格式版本进行排序,例如1.5 <2.0 <2.1。minor_version和major_version项目的值是此类文件的次要版本号和主要版本号。
- Java 虚拟机实例只能支持特定范围内的主版本号(Mi 至 Mj)和 0 至特定范围 内(0 至 m)的副版本号。
- 假设一个 Class 文件的格式版本号为 V,仅当 Mi.0 ≤ v ≤ Mj.m 成立时,这个 Class 文件才可以被此 Java 虚拟机支持。不同版本的 Java 虚拟机实现 支持的版本号也不同,高版本号的 Java 虚拟机实现可以支持低版本号的 Class 文件。
下表列出了各个版本 JDK 的十六进制版本号信息:
上述 class 文件 0000 0034 对应的就是表格中的 JDK1.8。
常量池计数器 constant_pool_count
紧跟版本信息之后的是常量池信息,其中前 2 个字节表示常量池计数器,其后的不定长数据则表示常量池的具体信息。constant_pool表的索引[1,constant_pool_count-1]
- 常量池描述着整个Class文件中所有的字面量信息。常量池计数器(constant_pool_count)的值等于常量池(constant_pool)表中的条目数加一。
- 如果constant_pool索引大于零且小于constant_pool_count,则该索引被视为有效。对于 long 和 double 类型有例外情况。
- 在 Class 文件的常量池中,所有的 8 字节的常量都占两个表成员(项)的空间。如果一个 CONSTANT_Long_info 或 CONSTANT_Double_info 结构的项在常量池中的索引为 n,则常量池中下一个有效的项的索引为 n+2,此时常量池中索引为 n+1 的项有效但必须被认为不可用。
class 文件字节码对应的内容是:001d,其值为 29,表示一共有 29 - 1 = 28 个常量。
常量池表 constant_pool[]
紧跟着常量池计数器后面就是 28 个常量了,因为每个常量都对应不同的类型,需要一个个具体分析。
constant_pool[]是一个结构表,表示各种字符串常量,类和接口名称,字段名称以及在ClassFile结构及其子结构中引用的其他常量。 每个constant_pool表条目的格式由其第一个“标签”字节指示。 所有类型的常量池表项目有以下通用的格式:
cp_info { | |
u1 tag; | |
u1 info[]; | |
} |
常量池中,每个 cp_info 项的格式必须相同,它们都以一个表示 cp_info 类型的单字节 “tag”项开头。后面 info[]项的内容 tag 由的类型所决定。tag 有效的类型和对应的取值在下图表示。每个 tag 项必须跟随 2 个或更多的字节,这些字节用于给定这个常量的信息,附加字节的信息格式由 tag 的值决定。
常量池中的14种常量结构
" class="reference-link">
这些 cp_info 表结构又有不同的数据结构,其对应的数据结构如下图所示。
" class="reference-link">
接下来我们开始分析上述 Log.class 文件每个字节的含义,前面第一句话已经说了,紧跟着常量池计数器后面的就是常量池了。下面开始分析:
第 1 个常量
紧接着 001d 的后一个字节为 0A,为十进制数字 10,查表可知其为方法引用类型(CONSTANT_Methodref_info)的常量。在 cp_info 中结构如下所示:
查找的方式是先确定 tag 值,tag 值判断当前属于哪一个常量。这里 tag 为 10。
然后看其结构显示还有两个 U2 的index,说明后面 4 个字节都是属于第一个常量,其中第 2 - 3 个字节表示类信息,第 4 - 5 个字节表示名称及类描述符。
接下来我们取出这部分的数据:0a 0600 000f :
该常量项:
第 2 - 3 个字节,其值为 00 06,表示指向常量池第 6 个常量所表示的信息。根据后面我们分析的结果知道第 6 个常量是 java/lang/Object。
第 4 - 5 个字节,其值为 000f,表示指向常量池第 15 个常量所表示的信息,根据 javap 反编译出来的信息可知第 15 个常量是 :()V。
将两者组合起来:java/lang/Object.:V,即 Object 的 init 初始化方法。
javap -v Log.class | |
Classfile /Users/xxx/Desktop/Log.class | |
Last modified 2020-1-8; size 427 bytes | |
MD5 checksum 745be5a6df4d9554e783dbbcecaf9b6d | |
Compiled from "Log.java" | |
public class com.hello.test.Log | |
minor version: 0 | |
major version: 52 | |
flags: ACC_PUBLIC, ACC_SUPER | |
Constant pool: | |
#1 = Methodref #6.#15 // java/lang/Object."<init>":()V | |
#2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream; | |
#3 = String #18 // hello world! | |
#4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V | |
#5 = Class #21 // com/hello/test/Log | |
#6 = Class #22 // java/lang/Object | |
#7 = Utf8 <init> | |
#8 = Utf8 ()V | |
#9 = Utf8 Code | |
#10 = Utf8 LineNumberTable | |
#11 = Utf8 main | |
#12 = Utf8 ([Ljava/lang/String;)V | |
#13 = Utf8 SourceFile | |
#14 = Utf8 Log.java | |
#15 = NameAndType #7:#8 // "<init>":()V | |
#16 = Class #23 // java/lang/System | |
#17 = NameAndType #24:#25 // out:Ljava/io/PrintStream; | |
#18 = Utf8 hello world! | |
#19 = Class #26 // java/io/PrintStream | |
#20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V | |
#21 = Utf8 com/hello/test/Log | |
#22 = Utf8 java/lang/Object | |
#23 = Utf8 java/lang/System | |
#24 = Utf8 out | |
#25 = Utf8 Ljava/io/PrintStream; | |
#26 = Utf8 java/io/PrintStream | |
#27 = Utf8 println | |
#28 = Utf8 (Ljava/lang/String;)V | |
{ | |
public com.hello.test.Log(); | |
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 | |
public static void main(java.lang.String[]); | |
descriptor: ([Ljava/lang/String;)V | |
flags: ACC_PUBLIC, ACC_STATIC | |
Code: | |
stack=2, locals=1, args_size=1 | |
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream; | |
3: ldc #3 // String hello world! | |
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V | |
8: return | |
LineNumberTable: | |
line 5: 0 | |
line 6: 8 | |
} | |
SourceFile: "Log.java" |
其实从上面的结果也可以看出来,第一个常量对应的是第6,15个常量,组合起来的含义后面注释也写着了。
其他很多常量都是类似的,接下来我们看看字符串是怎么来得。
第 21 个常量
第 21 个常量,数据为
0100 1263 6f6d 2f68 656c 6c6f 2f74 6573 742f 4c6f 67
这里 tag 值是 01,对应的结构如下:
length 是 u2,对应着 0012,说明后面跟着 18 个字节:63 6f6d 2f68 656c 6c6f 2f74 6573 742f 4c6f 67;查 ASCII 表可得 63-c, 6f-o, 6d-m, 2f-/ ··· 4c-L,6f-o, 67-g,
组合起来就是:com/hello/test/Log 。
相信通过上面两个例子,大家就知道如何去分析常量池里面的索引了。但很多时候我们可以借助 JDK 提供的 javap 命令直接查看 Class 文件的常量池信息,但是手动分析能够让你更加了解结果为啥是这样的。其实 javap 出来的就是人家分析总结好的。
access_flags 访问标志
在常量池结束之后,紧接着的两个字节代表类或接口的访问标(access_flags)。这里的数据为 00 21。
access_flags 是一种掩码标志,用于表示某个类或者接口的访问权限及基础属性。access_flags 的取值范围和相应含义见下表:
- 第一列是标记名;
- 第二列是对应的值;
- 第三列是对应的说明。
带有 ACC_SYNTHETIC 标志的类,意味着它是由编译器自己产生的而不是由程序员 编写的源代码生成的。
带有 ACC_ENUM 标志的类,意味着它或它的父类被声明为枚举类型。
带有 ACC_INTERFACE 标志的类,意味着它是接口而不是类,反之是类而不是接口。
如果一个 Class 文件被设置了 ACC_INTERFACE 标志,那么同时也得设置 ACC_ABSTRACT 标志。同时它不能再设置ACC_FINAL、 ACC_SUPER 和 ACC_ENUM 标志。
ANNOTATION注解类型必定带有 ACC_ANNOTATION 标记,如果设置了 ANNOTATION 标记, ACC_INTERFACE 也必须被同时设置。如果没有同时设置 ACC_INTERFACE 标记, 那么这个 Class 文件可以具有表 4.1 中的除 ACC_ANNOTATION 外的所有其它标记。 当然 ACC_FINAL 和 ACC_ABSTRACT 这类互斥的标记除外。
ACC_SUPER 标志用于确定该 Class 文件里面的 invokespecial 指令使用的是哪 一种执行语义。目前 Java 虚拟机的编译器都应当设置这个标志。ACC_SUPER 标记 是为了向后兼容旧编译器编译的 Class 文件而存在的,在 JDK1.0.2 版本以前的编 译器产生的 Class 文件中,access_flag 里面没有 ACC_SUPER 标志。同时, JDK1.0.2 前的 Java 虚拟机遇到 ACC_SUPER 标记会自动忽略它。
在表中没有使用的 access_flags 标志位是为未来扩充而预留的,这些预留的标志为在编译器中会被设置为 0, Java 虚拟机实现也会自动忽略它们。
类索引、父类索引、接口索引
在访问标记后,则是类索引、父类索引、接口索引的数据,这里数据为:00 05 、00 06 、00 00。
类索引和父类索引都是一个 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合,这个可以由前面 Class 文件的构成可以得到。 Class 文件中由这三项数据来确定这个类的继承关系。
this_class 类索引
类索引,this_class 的值必须是对 constant_pool 表中项目的一个有效索引值。
constant_pool 表在这个索引处的项必须为 CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类或接口。这里的类索引是 00 05 表示其指向了常量池中第 5 个常量,通过我们之前的分析,我们知道第 5 个常量其最终的信息是 Log 类。
super_class 父类索引
super_class 值必须为 0 或者对 constant_pool 表中项目的一个有效索引值。
如果它的值不为 0,那 constant_pool 表在这个索引处的项 必须为 CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的 类的直接父类。
当前类的直接父类,以及它所有间接父类的 access_flag 中都不能有 ACC_FINAL 标记。对于接口来说,它的 Class 文件的 super_class 项的值必须是 对 constant_pool 表中项目的一个有效索引值。
constant_pool 表在这个索引处的 项必须为代表 java.lang.Object 的 CONSTANT_Class_info 类型常量。
如果 Class 文件的 super_class 的值为 0,那这个 Class 文件只可能是定义的是 java.lang.Object 类,只有它是唯一没有父类的类。这里的父类索引是 00 06 表示其指向了常量池中第 6 个常量,通过我们之前的分析,我们知道第 6 个常量其最终的信息是 Object 类。
因为其并没有继承任何类,所以 Demo 类的父类就是默认的 Object 类。
interfaces_count 接口计数器
interfaces_count 的值表示当前类或接口的直接父接口数量。
interfaces[] 接口表
interfaces[] 数组中的每个成员的值必须是一个对 constant_pool 表中项 目的一个有效索引值,它的长度为 interfaces_count。每个成员 interfaces[i] 必 须为CONSTANT_Class_info类型常量,其中0 ≤ i < interfaces_count。
在 interfaces[]数组中,成员所表示的接口顺序和对应的源 代码中给定的接口顺序(从左至右)一样,即 interfaces[0]对应的是源代码中最左 边的接口。
这里 Log 类的字节码文件中,因为并没有实现任何接口,所以紧跟着父类索引后的两个字节是0x0000,这表示该类没有实现任何接口。因此后面的接口索引表为空。
未完待续……