概念
字节码文件(.class)由JVM规范规定,主要包括魔数、JDK版本号(小版本和大版本)、常量池、类的访问标志、类的接口和父类信息、成员变量信息,方法信息等。
对于java中的数据类型,字节码中的对应关系为:
- byte->B,char->C,int->I,long->J,short->S,boolean->Z,folat->F,double->D
- 对于数组,以**[** 作为前缀,二位数组前缀为**[[,如:int[]表示为[I**
- 对于引用,以L开头,以分号结尾,将类的全限定名用**/**分割。如:Object类表示为Ljava/lang/Object;
- 对于方法签名,用**(方法参数类型列表)返回类型表示,其中方法参数列表要严格按照方法声明的顺序,返回类型如果是void,用V表示。如,Object getValue()表示为()Ljava/lang/Object;,Object mymethod(int i, double d, Object o)表示为(IDLjava/lang/Object;)Ljava/lang/Object;**
字节码中的数据类型包括两种,一种是无符号数的基本数据类型,其中字节长度u1表示一个字节,u2表示2个字节,u3表示3个字节。它们表示数值,索引和UTF8字面值。另一种是表,如常量池表,字段表,方法表,它们是基本数据类型的复合结构体。
描述符(Descriptor),指的是字段或者方法类型,它存放在constant pool中的CONSTANT_Utf8_info类型项中。分为字段描述符和方法描述符。
一个class文件本身就是一张有顺序的表,从上到下的整体结构为:
类型 | 名称 | 数量 |
---|---|---|
u4 | magic | 1 |
u2 | minor_version | 1 |
u2 | major_version | 1 |
u2 | constant_pool_count | 1 |
cp_info | constant_pool | constant_pool_count-1 |
u2 | access_flag | 1 |
u2 | this_class | 1 |
u2 | super_class | 1 |
u2 | interfaces_count | 1 |
u2 | interfaces | interfaces_count |
u2 | fields_count | 1 |
field_info | fields | fields_count |
u2 | methods_count | 1 |
method_info | methods | methods_count |
u2 | attributes_count | 1 |
attribute_info | attributes | attributes_count |
通过javap -v Cat.class命令可以解析class文件,得到反编译文件,本文以Cat.java为例,源码和反编译文件如下:
1package test.learn3;
2// Cat.java
3public class Cat {
4private int age = 2;
5
6public int getAge() {
7return age;
8}
9
10public void setAge(int age) {
11this.age = age;
12}
13}
反编译的内容如下:
1D:\develop\workspace\MyTest\target\classes>javap -v test.learn3.Cat
2Classfile /D:/develop/workspace/MyTest/target/classes/test/learn3/Cat.class
3Last modified 2020-1-7; size 463 bytes
4MD5 checksum 9c9b4378c58cb37e21cf1b918c3a64b2
5Compiled from "Cat.java"
6public class test.learn3.Cat
7minor version: 0
8major version: 52
9flags: ACC_PUBLIC, ACC_SUPER
10Constant pool:
11#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
12#2 = Fieldref #3.#21 // test/learn3/Cat.age:I
13#3 = Class #22 // test/learn3/Cat
14#4 = Class #23 // java/lang/Object
15#5 = Utf8 age
16#6 = Utf8 I
17#7 = Utf8 <init>
18#8 = Utf8 ()V
19#9 = Utf8 Code
20#10 = Utf8 LineNumberTable
21#11 = Utf8 LocalVariableTable
22#12 = Utf8 this
23#13 = Utf8 Ltest/learn3/Cat;
24#14 = Utf8 getAge
25#15 = Utf8 ()I
26#16 = Utf8 setAge
27#17 = Utf8 (I)V
28#18 = Utf8 SourceFile
29#19 = Utf8 Cat.java
30#20 = NameAndType #7:#8 // "<init>":()V
31#21 = NameAndType #5:#6 // age:I
32#22 = Utf8 test/learn3/Cat
33#23 = Utf8 java/lang/Object
34{
35public test.learn3.Cat();
36descriptor: ()V
37flags: ACC_PUBLIC
38Code:
39stack=2, locals=1, args_size=1
400: aload_0
411: invokespecial #1 // Method java/lang/Object."<init>":()V
424: aload_0
435: iconst_2
446: putfield #2 // Field age:I
459: return
46LineNumberTable:
47line 3: 0
48line 4: 4
49LocalVariableTable:
50Start Length Slot Name Signature
510 10 0 this Ltest/learn3/Cat;
52
53public int getAge();
54descriptor: ()I
55flags: ACC_PUBLIC
56Code:
57stack=1, locals=1, args_size=1
580: aload_0
591: getfield #2 // Field age:I
604: ireturn
61LineNumberTable:
62line 7: 0
63LocalVariableTable:
64Start Length Slot Name Signature
650 5 0 this Ltest/learn3/Cat;
66
67public void setAge(int);
68descriptor: (I)V
69flags: ACC_PUBLIC
70Code:
71stack=2, locals=2, args_size=2
720: aload_0
731: iload_1
742: putfield #2 // Field age:I
755: return
76LineNumberTable:
77line 11: 0
78line 12: 5
79LocalVariableTable:
80Start Length Slot Name Signature
810 6 0 this Ltest/learn3/Cat;
820 6 1 age I
83}
84SourceFile: "Cat.java"
对应的十六进制数据为:
1Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
200000000: CA FE BA BE 00 00 00 34 00 18 0A 00 04 00 14 09 J~:>...4........
300000010: 00 03 00 15 07 00 16 07 00 17 01 00 03 61 67 65 .............age
400000020: 01 00 01 49 01 00 06 3C 69 6E 69 74 3E 01 00 03 ...I...<init>...
500000030: 28 29 56 01 00 04 43 6F 64 65 01 00 0F 4C 69 6E ()V...Code...Lin
600000040: 65 4E 75 6D 62 65 72 54 61 62 6C 65 01 00 12 4C eNumberTable...L
700000050: 6F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C ocalVariableTabl
800000060: 65 01 00 04 74 68 69 73 01 00 11 4C 74 65 73 74 e...this...Ltest
900000070: 2F 6C 65 61 72 6E 33 2F 43 61 74 3B 01 00 06 67 /learn3/Cat;...g
1000000080: 65 74 41 67 65 01 00 03 28 29 49 01 00 06 73 65 etAge...()I...se
1100000090: 74 41 67 65 01 00 04 28 49 29 56 01 00 0A 53 6F tAge...(I)V...So
12000000a0: 75 72 63 65 46 69 6C 65 01 00 08 43 61 74 2E 6A urceFile...Cat.j
13000000b0: 61 76 61 0C 00 07 00 08 0C 00 05 00 06 01 00 0F ava.............
14000000c0: 74 65 73 74 2F 6C 65 61 72 6E 33 2F 43 61 74 01 test/learn3/Cat.
15000000d0: 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 ..java/lang/Obje
16000000e0: 63 74 00 21 00 03 00 04 00 00 00 01 00 02 00 05 ct.!............
17000000f0: 00 06 00 00 00 03 00 01 00 07 00 08 00 01 00 09 ................
1800000100: 00 00 00 38 00 02 00 01 00 00 00 0A 2A B7 00 01 ...8........*7..
1900000110: 2A 05 B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A *.5..1..........
2000000120: 00 02 00 00 00 03 00 04 00 04 00 0B 00 00 00 0C ................
2100000130: 00 01 00 00 00 0A 00 0C 00 0D 00 00 00 01 00 0E ................
2200000140: 00 0F 00 01 00 09 00 00 00 2F 00 01 00 01 00 00 ........./......
2300000150: 00 05 2A B4 00 02 AC 00 00 00 02 00 0A 00 00 00 ..*4..,.........
2400000160: 06 00 01 00 00 00 07 00 0B 00 00 00 0C 00 01 00 ................
2500000170: 00 00 05 00 0C 00 0D 00 00 00 01 00 10 00 11 00 ................
2600000180: 01 00 09 00 00 00 3E 00 02 00 02 00 00 00 06 2A ......>........*
2700000190: 1B B5 00 02 B1 00 00 00 02 00 0A 00 00 00 0A 00 .5..1...........
28000001a0: 02 00 00 00 0B 00 05 00 0C 00 0B 00 00 00 16 00 ................
29000001b0: 02 00 00 00 06 00 0C 00 0D 00 00 00 00 00 06 00 ................
30000001c0: 05 00 06 00 01 00 01 00 12 00 00 00 02 00 13 ...............
下面逐一拆解上面这段二进制数据。
魔数
最开头的4个字节,它是固定值:0xCAFEBABE
版本号
次版本号是:0000 主版本号是:0034,十进制是52,表示采用的是jdk1.8
常量池
紧接着版本号后面的部分是常量池,常量池是字节码的资源库,并不只是类中常量。常量池由一个u2(2个字节)长度的常量池项目个数和常量池表构成的。常量池表的字节长度是变长的。
常量池中的元素,即常量池项目(item)从1开始,而是不从0开始,第0项目表示空引用(null),类中的变量什么都不指向。常量池项目的数据类型主要包括以下14种,每一个都是以单个字节的tag打头,用于标志这种元素的数据类型。
本例中,常量池的长度是0018,也就是24项。紧接着是0A,对于十进制10,tag为10的类型是CONSTANT_Methodref_info,紧接着的4个字节是0004 0014,分别指向第4项和第20项索引
类访问标识
用于标识这个类的修饰符,是否为接口,是否为抽象类,是否为public等。访问标识说明表如下:
访问标志 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0X0001 | 是否为public类型 |
ACC_FINAL | 0X0010 | 是否被声明为final,只有类可设置 |
ACC_SUPER | 0X0020 | 是否允许使用invokespecial字节码指令 |
ACC_INTERFACE | 0X0200 | 标识为一个接口 |
ACC_ABSTRACT | 0X0400 | 是否为abstract类型,对于接口或抽象类而言,此标志值为真,其他类值为假 |
ACC_SYNTHETIC | 0X1000 | 标识这个类并非由用户代码产生的 |
ACC_ANNOTATION | 0X2000 | 标识这是一个注解 |
ACC_ENUM | 0X4000 | 标识这是一个枚举 |
在本例中,坐标为[000000e0, 02]和[000000e0, 03]这2个字节为0021,即为ACC_PUBLIC和ACC_SUPER的组合,代表为Public类型的类。
类、父类名称索引以及接口数量
紧接着的4个字节为0003 0004,分别标识this_class和super_class,分别引用了常量池中常量:
1#3 = Class #22 // test/learn3/Cat
2#4 = Class #23 // java/lang/Object
也就是当前类的全限定名称索引和父类的全限定名称索引。接着的两个字节00 00 (坐标为[000000e0, 08]和[000000e0, 09])表示接口数量为0个。
字段表
接口数量和接口信息之后的数据就是字段数量和字段表数组。字段数量(fields_count)表示声明的字段个数。字段表(fields)是字段表述信息结构体。
——**字段表**————
类型 | 名称 | 数量 | 备注 |
---|---|---|---|
u2 | access_flags | 1 | 字段访问标志 |
u2 | name_index | 1 | 字段名称索引(对常量池的引用) |
u2 | descriptor_index | 1 | 字段类型描述符(对常量池的引用) |
u2 | attributes_count | 1 | |
attribute_info | attributes | attributes_count |
字段访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否public |
ACC_PRIVATE | 0x0002 | 字段是否private |
ACC_PROTECTED | 0x0004 | 字段是否protected |
ACC_STATIC | 0x0008 | 字段是否static |
ACC_FINAL | 0x0010 | 字段是否final |
ACC_VOLATILE | 0x0040 | 字段是否volatile |
ACC_TRANSIENT | 0x0080 | 字段是否transient |
ACC_SYNTHETIC | 0x1000 | 字段是否由编译器自动产生的 |
ACC_ENMU | 0x4000 | 字段是否enmu |
本例中,字段长度为1,字段数据为 ++00 02 00 05 00 06 00++。0002对应了字段标识ACC_PRIVATE,标识该字段为private,0005和0006对应于:
1#5 = Utf8 age
2#6 = Utf8 I
标识字段名称是age,字段类型是I,即整型。最后的一个字节00表示没有属性表。
方法表
方法表用于描述方法的名称,类型和属性等信息的结构体。
方法表结构
类型 | 名称 | 数量 | 备注 |
---|---|---|---|
u2 | access_flags | 1 | 方法访问标志 |
u2 | name_index | 1 | 方法名称(对常量池的引用) |
u2 | descriptor_index | 1 | 方法的描述符(对常量池的引用) |
u2 | attributes_count | 1 | attributes包含的项目数 |
attribute_info | attributes[attributes_count] | attributes_count |
方法访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否为public |
ACC_PRIVATE | 0x0002 | 方法是否为private |
ACC_PROTECTED | 0x0004 | 方法是否为protected |
ACC_STATIC | 0x0008 | 方法是否为static |
ACC_FINAL | 0x0010 | 方法是否为final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否为synchronized |
ACC_BRIDGE | 0x0040 | 方法是否是由编译器产生的 桥接方法 |
ACC_VARARGS | 0x0080 | 方法是否接受不定参数 |
ACC_NATIVE | 0x0100 | 方法是否为native |
ACC_ABSTRACT | 0x0400 | 方法是否为abstract |
ACC_STRICTFP | 0x0800 | 方法是否为strictfp |
ACC_SYNTHETIC | 0x1000 | 方法是否是由编译器自动产生的 |
本例中,从第000000f0行第04列到第0F列即为第一个方法:
100 03 00 01 00 07 00 08 00 01 00 09
0003表示方法数量,该类共有3个方法,接下来就是第一个方法,0001对应于ACC_PUBLIC,表示public,0007和0008表示方法名称索引和类型索引:
1#7 = Utf8 <init>
2#8 = Utf8 ()V
也就是说该方法为构造方法。接着0001表示只有一个属性,0009表示code属性。
属性表
在Class文件、字段表、方法表中都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
属性名称 | 使用位置 | 含义 |
---|---|---|
Code | 方法表 | Java代码编译成的字节码指令 |
ConstantValue | 字段表 | final关键字定义的常量值 |
Deprecated | 类、方法表、字段表 | 被声明为deprecated的方法和字段 |
Exceptions | 方法表 | 方法抛出的异常 |
InnerClasses | 类文件 | 内部类列表 |
LineNumberTable | Code属性 | Java源码的行号与字节码指令的对应关系 |
LocalVariableTable | Code属性 | 方法的局部变量描述 |
SourceFile | 类文件 | 源文件名称 |
Synthetic | 类、方法表、字段表 | 标识方法或字段为编译器自动生成的 |
Code属性 Java方法里的代码被编译处理后,变为字节码指令存储在方法表的Code属性里,但并不是所有的方法表里都有Code属性,例如接口或抽象类中的方法就可能没有该属性。
Code属性数据结构:
类型 | 名称 | 含义 |
---|---|---|
u2 | attribute_name_index | 属性名称索引 |
u4 | attribute_length | 属性长度 |
u2 | max_stack | 操作数栈深度的最大值 |
u2 | max_locals | 局部变量表所需的存储空间 |
u4 | code_length | 字节码长度 |
u1 | code[code_length] | 存储字节码指令的一系列字节流 |
u2 | exception_table_length | 异常表长度 |
exception_info | exception_table | |
u2 | attributes_count | |
attribute_info | attributes[attributes_count] | |
可以看到Code属性数据结构里还包含有其他属性,主要有LineNumberTable、LocalVariableTable。 |
LineNumberTable
1LineNumberTable_attribute {
2 u2 attribute_name_index; //属性名称索引
3 u4 attribute_length; //属性长度
4 u2 line_number_table_length;
5 { u2 start_pc; //字节码行号
6 u2 line_number; //java源码行号
7 } line_number_table[line_number_table_length];
8}
上面方法中code属性字节:
1 00 00 00 38 00 02 00 01 00 00 00 0A 2A B7 00 01 2A 05 B5 00 02 B1 00 00
2 00 02 00 0A
00000038表示code的属性长度为56个字节,0002表示栈的深度为2,0001表示局部变量数为1,0000000A表示方法代码指令长度为10个字节,即2A B7 00 01 2A 05 B5 00 02 B1。接着0000表示异常表的长度为0。
接着0002表示有2个属性,其中000A引用常量池的第10项:
1#10 = Utf8 LineNumberTable
参考上面LineNumberTable的数据结构可以继续分析。
code中2个属性信息如下截图:
LineNumberTable:
LocalVaribalTable: