Yves

手撸 Class 文件

请准备好眼药水

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
offset : 0 1 2 3 4 5 6 7 8 9 a b c d e f
-------------------------------------------------------------------
00000000: cafe babe 0000 0033 0013 0a00 0400 0f09 .......3........
00000010: 0003 0010 0700 1107 0012 0100 016d 0100 .............m..
00000020: 0149 0100 063c 696e 6974 3e01 0003 2829 .I...<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 0466 756e umberTable...fun
00000050: 6301 0003 2829 4901 000a 536f 7572 6365 c...()I...Source
00000060: 4669 6c65 0100 0f48 656c 6c6f 576f 726c File...HelloWorl
00000070: 642e 6a61 7661 0c00 0700 080c 0005 0006 d.java..........
00000080: 0100 1263 632f 7976 6573 2f48 656c 6c6f ...cc/yves/Hello
00000090: 576f 726c 6401 0010 6a61 7661 2f6c 616e World...java/lan
000000a0: 672f 4f62 6a65 6374 0021 0003 0004 0000 g/Object.!......
000000b0: 0001 0002 0005 0006 0000 0002 0001 0007 ................
000000c0: 0008 0001 0009 0000 001d 0001 0001 0000 ................
000000d0: 0005 2ab7 0001 b100 0000 0100 0a00 0000 ..*.............
000000e0: 0600 0100 0000 0300 0100 0b00 0c00 0100 ................
000000f0: 0900 0000 1f00 0200 0100 0000 072a b400 .............*..
00000100: 0204 60ac 0000 0001 000a 0000 0006 0001 ..`.............
00000110: 0000 0007 0001 000d 0000 0002 000e ..............

准备

Java 源代码

1
2
3
4
5
6
7
8
9
package cc.yves;
public class HelloWorld{
private int m;
public int func(){
return m + 1;
}
}

字节码

编译之后的字节码打开之后长这样 ↓↓↓,为了方便查看,我在第一行标了行偏移量。用 vim 打开 Class 文件有个坑,命令一定要加上个参数 -b,即 vim -b HelloWorld.class,否则用 :%!xxd 命令打开之后在最后会多出来一个字节,分析到最后的时候差点以为前面搞错了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
offset : 0 1 2 3 4 5 6 7 8 9 a b c d e f
-------------------------------------------------------------------
00000000: cafe babe 0000 0033 0013 0a00 0400 0f09 .......3........
00000010: 0003 0010 0700 1107 0012 0100 016d 0100 .............m..
00000020: 0149 0100 063c 696e 6974 3e01 0003 2829 .I...<init>...()
00000030: 5601 0004 436f 6465 0100 0f4c 696e 654e V...Code...LineN
00000040: 756d 6265 7254 6162 6c65 0100 0466 756e umberTable...fun
00000050: 6301 0003 2829 4901 000a 536f 7572 6365 c...()I...Source
00000060: 4669 6c65 0100 0f48 656c 6c6f 576f 726c File...HelloWorl
00000070: 642e 6a61 7661 0c00 0700 080c 0005 0006 d.java..........
00000080: 0100 1263 632f 7976 6573 2f48 656c 6c6f ...cc/yves/Hello
00000090: 576f 726c 6401 0010 6a61 7661 2f6c 616e World...java/lan
000000a0: 672f 4f62 6a65 6374 0021 0003 0004 0000 g/Object.!......
000000b0: 0001 0002 0005 0006 0000 0002 0001 0007 ................
000000c0: 0008 0001 0009 0000 001d 0001 0001 0000 ................
000000d0: 0005 2ab7 0001 b100 0000 0100 0a00 0000 ..*.............
000000e0: 0600 0100 0000 0300 0100 0b00 0c00 0100 ................
000000f0: 0900 0000 1f00 0200 0100 0000 072a b400 .............*..
00000100: 0204 60ac 0000 0001 000a 0000 0006 0001 ..`.............
00000110: 0000 0007 0001 000d 0000 0002 000e ..............

javap -verbose HelloWorld

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
53
Classfile /home/yves/Test/java/cc/yves/HelloWorld.class
Last modified 2017-7-24; size 286 bytes
MD5 checksum 10879c1e1c71c8091fccc11f5a0278e5
Compiled from "HelloWorld.java"
public class cc.yves.HelloWorld
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // cc/yves/HelloWorld.m:I
#3 = Class #17 // cc/yves/HelloWorld
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 func
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 HelloWorld.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 cc/yves/HelloWorld
#18 = Utf8 java/lang/Object
{
public cc.yves.HelloWorld();
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 int func();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 7: 0
}
SourceFile: "HelloWorld.java"

Class 文件格式

Class 文件是一组以 8 位字节为基础单位的二进制流,各项数据严格按照顺序紧密地排列在 Class 文件中,中间没有多余的分隔符号。当遇到需要占用 8 位(即 1 字节)以上空间的数据项时,按照 Big-Endian 方式分割成若干个字节进行存储。

u1、u2、u4 分别代表 1 个字节、2 个字节、4 个字节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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];
}

魔数(Magic Number) 与 Class 文件的版本

首先看前面 8 个字节,即偏移量从 0x0000 到 0x0007 范围

类型 字节数 偏移量
magic 4 0xcafebabe 0x0000
minor_version 2 0x0000 0x0004
major_version 2 0x0034 0x0006

每个 Class 文件的头 4 个字节称为魔数,其唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。Class 文件的魔数值为 0xCAFEBABE

紧接着的 4 个字节分别是次版本号主版本号,各占 2 个字节。主版本号从 45 开始算,每个 Java 大版本发布主版本号加一。0x0034 转换成十进制是 51,说明该字节码由 JDK 1.8 编译生成。高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过其版本号的 Class 文件。

常量池

版本号紧接着的是常量池,因为常量池中常量的数量是不固定的,所以在常量池的入口放置一项 u2 类型数据表示常量池容量计数值。这样的方式在 Java 字节码中很常见。

constant_pool_count

字节数 偏移量
2 0x0013 0x0008

其值为 0x0013,也就是 19,而常量池是从 1 开始计数的,0 作为保留值用于后面某些指向常量池的索引值的数据在特定情况下需要表达”不引用任何一个常量池项目”的含义。那么常量池中常量的数量就是 constant_pool_count - 1,即在这个 Class 文件中有 18 个常量。

constant_pool

常量池中主要存放两大类常量: 字面量(Literal)和符号引用(Symbolic References)。字面量比较接近 Java 语言层面的常量,如文本字符串,声明为 final 的常量值等。而符号引用则属于编译原理方面的概念,包括三类常量:

  • 类和接口的全限定名(Fully Qualified Name)
  • 字段的名称和描述符(Descriptor)
  • 方法的名称和描述符

常量池中每一个常量都是一个表,在 JDK 1.7 之前有 11 种结构各不相同的表结构数据,JDK 1.7 中增加了 3 种(CONSTANT_MethodHandle_infoCONSTANT_MethodType_infoCONSTANT_InvokeDynamic_info) 用于更好地支持动态语言调用。
表开始的第一位是一个 u1 类型的标志位(tag),用于代表当前常量属于哪种常量类型。

常量池中的 14 种常量项的结构总表

对照这张表,结合前面 javap 的输出数据,对 Class 文件中的常量进行分析。

序号 字节数 类型 偏移量
1 5 0x0a 0004 000f #4,#15 Methodref 0x000a
2 5 0x09 0003 0010 #03,#16 Fieldref 0x000f
3 3 0x07 0011 #17 Class 0x0014
4 3 0x07 0012 #18 Class 0x0017
5 4 0x01 0001 6d m Utf8 0x001a
6 4 0x01 0001 49 I Utf8 0x001e
7 9 0x01 0006 3c696e69743e <init> Utf8 0x0022
8 6 0x01 0003 383956 ()V Utf8 0x002b
9 7 0x01 0004 436f 6465 Code Utf8 0x0031
10 18 0x01 000f 4c 696e 654e 756d 6265 7254 6162 6c65 LineNumberTable Utf8 0x0038
11 7 0x01 0004 6675 6e63 func Utf8 0x004a
12 6 0x01 0003 2829 49 ()I Utf8 0x0051
13 13 0x01 000a 536f 7572 6365 4669 6c65 SourceFile Utf8 0x0057
14 18 0x01 000f 48 656c 6c6f 576f 726c 642e 6a61 7661 HelloWorld.java Utf8 0x0064
15 5 0x0c 0007 0008 #7:#8 // "<init>":()V NameAndType 0x0076
16 5 0x0c 0005 0006 #5:#6 // m:I NameAndType 0x007b
17 21 0x01 0012 63 632f 7976 6573 2f48 656c 6c6f 576f 726c 64 cc/yves/HelloWorld Utf8 0x0080
18 19 0x01 0010 6a61 7661 2f6c 616e 672f 4f62 6a65 6374 java/lang/Object Utf8 0x0095

以第一个常量为例, tag = 0x0a 表示该常量为 CONSTANT_Methodref_info 类型的常量,查看该类型的结构表可知,第二项数据占 2 个字节,指向常量池中的第 4 个常量,第三项数据也占 2 个字节,指向常量池中的第 15 个常量。

接着看常量池的第 4 个常量,tag = 0x07,表示这是一个 CONSTANT_Class_info 类型的常量,第二项数据 0x0012 指向常量池中的第 18 个常量。

第 18 个常量为 CONSTANT_Utf8_info 类型,其第二项数据表示后面接着多少字节属于本类型的内容,即后面有多少个字节的字符串(回想一下与前面的 constant_pool_count )。0x6a61 7661 2f6c 616e 672f 4f62 6a65 6374 这 16 个字节分别对应一个 unicode 字符,javap 已经帮我们算出来是哪些字符了。

以此类推,其他的常量也是类似的,对照着结构表就可以推出常量池中所有的常量表示什么意思。

访问标志(access_flags)

常量池之后的 2 个字节表示访问标志,这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口、是否定义为 public 类型、是否定义为 abstract类型,如果是类的话,是否被声明为 final 等。

访问标志

访问标志中一共有 16 个标志位可用(2 个字节) ,目前只定义了其中 8 个,没有使用到的标志位要求一律为 0。

字节数 偏移量
2 0x0021 public、super 0x00a8

HelloWorld 是一个普通的 Java 类,被 public 修饰,所以 ACC_PUBLICACC_SUPER 这两个标志为真,其它的全为假,即 0x0021 = 0x0020 | 0x0001

类索引(this_class)、父类索引(super_class)、接口索引集合(interfaces)

类索引和父类索引都是 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合。Class 文件中由这三项数据来确定这个类的继承关系。

类索引和父类索引中各自指向一个类型为 CONSTANT_Class_info 的类描述符常量,通过 CONSTANT_Class_info 类型常量中的索引值可以找到定义在 CONSTANT_Utf8_info 类型常量中的全限定名字符串。

接口索引集合的第一项为 u2 类型的 接口计数器(interfaces_count),表示索引表的容量。示例中的类没有实现任何接口,所以计数器值为 0 ,后面接口的索引表不再占用任何字节。

类型 字节数 偏移量
this_class 2 0x0003 0x00aa
super_class 2 0x0004 0x00ac
interfaces_count 2 0x0000 ox00ae
interfaces 0 - -

字段表集合(field_info)

fields_count

字段表的入口是一个 u2 类型,与前面出现过的类似,用来表示字段的个数

字节数 偏移量
2 0x0001 ox00b0

0x0001 表示后面只有一个变量

fields_info

序号 字节数 偏移量
1 4 0x0002 0005 0006 0000 0x00b2

字段表用于描述接口或类中声明的变量。字段包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。可以包括的信息有:

  • 字段的作用域(public、private、protected、default)
  • 是实例变量还是类变量(static)
  • 可变性(final)
  • 并发可见性(volatile,是否强制从主内存读写)
  • 可否被序列化(transient)
  • 字段数据类型(基本类型、对象、数组)
  • 字段名称

由修饰符决定的都是布尔值,要么有修饰符要么没有,因此使用标志位来表示,而字段名,数据类型,只能引用常量池中的常量来描述。

字段表结构

字段修饰符放在 access_flags 中,与类中的 access_flags 类似

字段访问标志

0x0002 对应了表中的 ACC_PRIVATE ,java 代码中确实是声明唯一的变量为 private。

接着是两项索引name_indexdescriptor_index ,都是对常量池的引用,分别代表字段的简单名称以及字段和方法描述符。

描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型、和顺序)和返回值。基本数据类型和 void 类型都使用首字母大写字符来表示,而对象类型则用字符 L 加对喜爱那个的全限定名表示。如下表所示

描述符标识字符含义

对于数组类型,每一维度使用一个前置的[ 字符描述。如定义 String[][] 这个二维数组,则记录为 [[Ljava/lang/String;

用描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的顺序放在一组圆括号 () 内。如方法 int func(int i) 的描述符为 (I)I

0x00050x0006 对应常量池中的第 5 和第 6 个常量,分别是 mI ,表明这是名为 m 的 int 类型变量。

字段表中最后一个 u2 类型的值是属性的个数,这里没有属性所以其值为 0x0000 ,后面不再占字节数。

属性表集合用于存储一些额外的信息。如果将字段声明为 final static int m = 123; ,就会存在一项名称为 ConstantValue 的属性,其值指向常量 123。

字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能列出原本 Java 代码中不存在的字段,如内部类中会自动添加指向外部类实例的字段。

在 Java 语言中字段是无法重载的,但是对于字节码来说,如果两个字段的描述符不一致,字段重名就是合法的。

方法表集合

methods_count

与字段表类似,入口依旧是一个指明数量的 u2 类型数据

字节数 偏移量
2 0x0002 0x00ba

这里 methods_count 的值是 2,是因为隐式的无参数构造方法也包含在内。

methods_info

方法表的结构与字段表非常类似

方法表结构

方法访问标志

而方法内部的代码则是放在一个名为 Code 的属性里面。

序号 字节数 偏移量
1 43 0x0001 0007 0008 0001, 0009 0000 001d 0001 0001 0000 0005 2ab7 0001 b1 0000 0001, 000a 0000 0006 0001 0000 0003 (, 之后为属性) 0x00bc
2 45 0x0001 000b 000c 0001, 0009 0000 001f 0002 0001 0000 0007 2a b400 0204 60ac 0000 0001, 000a 0000 0006 0001 0000 0007(, 之后为属性) 0x00e7

这里有两个方法,主要分析第一个,第二个也是类似的。

0x0001 表示这是一个 pubic 方法

0x00070x0008 分别是简单名称和描述符,从 javap 中的结果可以快速确定他们所指向的常量分别是 <init>()V

接着的 0x0001 表示这个方法只有一个属性,后面是接着的就是属性的内容。

code属性表结构

参照上表 0x0009 指向常量池中的第 9 个常量 Code ,这个常量值固定为 Code

0x0000001d 是属性的长度,由于属性名称索引与属性长度一共为 6 字节,所以属性值长度固定为整个属性表长度减去 6 个字节,也就是属性长度后面紧跟的所有内容的长度。

max_stackmax_local 的值都是 0x0001 ,分别表示操作数栈深度的最大值为 1 和局部变量表所需的储存空间都为 1 个 Slot。

0x00000005 表示后续的字节码指令长度 code_length,每个字节码指令占一个字节,即 0x2a b7 00 01 b1 都表示字节码指令。

接着的 2 个字节 0x0000 说明没有异常属性,后面不再占用字节数。

最后 0x0001 是 Code 属性的属性数量,后面接着的是一个属性。接着看这个属性,0x000a 应用的常量是 LineNumberTable

LineNumberTable属性表结构

LineNumberTable 属性中,属性名索引之后的依旧是属性长度,其值为 0x00000006 ,后面还有 6 个字节是属于这个属性的。line_number_lable_length 占了 2 个字节。然后是 line_number_table ,这是数量为 line_number_lable_length 、类型为 line_number_info 的集合。line_number_info 表包括了 start_pcline_number 两个 u2 类型的数据项,前者是字节码行号,后者是 Java 源码行号。用代码说明如下:

1
2
3
4
5
6
7
8
9
LineNumberTable:{
u2 attribute_name_index;
u4 attribute_length; //以上两个是所有属性都有的数据项
u2 line_number_table_length;
line_number_info:{
u2 start_pc; //字节码行号
u2 line_number; //Java 源码行号
} line_number_table[line_number_table_length];//这是一个长度为 line_number_table_length 的集合
}

对应的第一个方法的最后 6 个字节分别是

0x0001 表明只有一个 line_number_tablestart_pc0x0000line_number0x0003

到这里,第一个方法的分析就完了,第二个方法也是类似的,不再赘述。

类属性表

方法之后是类的属性表,跟字段和方法的属性类似。

attributes_count

首先还是属性的数量,占 2 个字节,值为0x0001 ,说明这个类这里只有一个属性

字节数 偏移量
2 0x0001 0x0114

attributes_info

序号 字节数 偏移量
1 0x000d 0000 0002 000e 0x0115

0x000d 引用的常量是 SourceFile ,这个属性用于记录生成这个 Class 文件的源码文件名称,这个属性是可选的。

SourceFile属性表结构

sourcefile_index 是指向常量池的一个索引,值为 0x000e ,对应的常量是 cc/yves/HelloWorld

总结

到这里,整个 HelloWorld.class 文件就分析完了。只要知道对应的表结构,再复杂的 Class 文件也是差不多的分析过程。在了解了 Class 文件的组成之后,再看 javap 输出的信息,就一目了然了。