题目
带你分析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/do…
关于反汇编:
反汇编(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种常量结构
这些 cp_info 表结构又有不同的数据结构,其对应的数据结构如下图所示。
接下来我们开始分析上述 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,这表示该类没有实现任何接口。因此后面的接口索引表为空。