以这段Java代码为例,反汇编分析一下对应的Java字节码。将该文件保存为BooleanTest.java
。
package ex3;
public class BooleanTest {
public static void create() {
boolean a = true;
boolean b = false;
boolean[] arr = new boolean[100];
arr[5] = true;
System.out.println(a);
System.out.println(b);
System.out.println(arr);
}
public static void print(int a) {
int b = a;
System.out.printf("%d %d\n", a, b);
}
}
使用的Java版本为OpenJDK 1.8.0_171。
Picked up _JAVA_OPTIONS: -Dawt.useSystemAAFontSettings=gasp
openjdk version "1.8.0_171"
OpenJDK Runtime Environment (build 1.8.0_171-8u171-b11-2-b11)
OpenJDK 64-Bit Server VM (build 25.171-b11, mixed mode)
执行javac BooleanTest.java; javap -v BooleanTest.class
,会输出完整的反汇编代码,大致可以分为几部分:元数据+常量池、一系列方法,我们分开来看。
常量池
public class ex3.BooleanTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #6.#19 // java/lang/Object."<init>":()V
#2 = Fieldref #20.#21 // java/lang/System.out:Ljava/io/PrintStream;
#3 = Methodref #22.#23 // java/io/PrintStream.println:(Z)V
#4 = Methodref #22.#24 // java/io/PrintStream.println:(Ljava/lang/Object;)V
#5 = String #25 // %d %d\n
#6 = Class #26 // java/lang/Object
#7 = Methodref #27.#28 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#8 = Methodref #22.#29 // java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
#9 = Class #30 // ex3/BooleanTest
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 create
#15 = Utf8 print
#16 = Utf8 (I)V
#17 = Utf8 SourceFile
#18 = Utf8 BooleanTest.java
#19 = NameAndType #10:#11 // "<init>":()V
#20 = Class #31 // java/lang/System
#21 = NameAndType #32:#33 // out:Ljava/io/PrintStream;
#22 = Class #34 // java/io/PrintStream
#23 = NameAndType #35:#36 // println:(Z)V
#24 = NameAndType #35:#37 // println:(Ljava/lang/Object;)V
#25 = Utf8 %d %d\n
#26 = Utf8 java/lang/Object
#27 = Class #38 // java/lang/Integer
#28 = NameAndType #39:#40 // valueOf:(I)Ljava/lang/Integer;
#29 = NameAndType #41:#42 // printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
#30 = Utf8 ex3/BooleanTest
#31 = Utf8 java/lang/System
#32 = Utf8 out
#33 = Utf8 Ljava/io/PrintStream;
#34 = Utf8 java/io/PrintStream
#35 = Utf8 println
#36 = Utf8 (Z)V
#37 = Utf8 (Ljava/lang/Object;)V
#38 = Utf8 java/lang/Integer
#39 = Utf8 valueOf
#40 = Utf8 (I)Ljava/lang/Integer;
#41 = Utf8 printf
#42 = Utf8 (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
根据《Java虚拟机规范》,每一个class文件对应下面这样一个ClassFile
结构。
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
:唯一作用是确定这个文件是否为一个class文件,值固定为0xCAFEBABE
。 -
minor_version
和major_version
用来表示class文件的版本号,不同版本的JDK编译出的版本号不同,例如主版本号为52表示是Java 8版本的class、55表示是Java 11版本的class,参考。 -
constant_pool_count
常量池大小。 -
constant_pool
常量池。 -
access_flags
访问标志,ACCESS_PUBLIC
表示这是个public类,ACCESS_SUPER
没什么意义,所有jdk 1.0.2之后编译出的class都带有这个标志。 -
this_class
本类在常量池中的一个索引。 - ......
常量池中的每一项都具有如下通用格式,单字节的tag
表示cp_info
的实际类型,后面info
数组的内容由类型决定。tag
可以表示Class
, Fieldref
, Methodref
, InterfaceMethodref
, String
, Integer
, Float
, Long
, Double
, NameAndType
, Utf8
, MethodHandle
, MethodType
, InvokeDynamic
。
cp_info {
u1 tag;
u1 info[];
}
Class_info
的结构如下,表示一个类或接口,name_index
是常量池中一个Utf8_info
项的下标,表示类或接口名。
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
Fieldref_info
, Methodref_info
的结构如下,分别表示字段引用和方法引用,他们包含两个字段,class_index
表示字段、方法所在的类在常量池中的索引,name_and_type_index
表示当前字段或方法的名字和描述符。
CONSTANT_Fieldref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
String_info
的结构如下,用于表示一个String类型的常量对象,string_index
是常量池中一个Utf8_info
项的下标。
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
Utf8_info
的结构如下,用于表示一个Utf8字符串值常量。
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
NameAndType_info
结构如下,用于表示一个方法或字段,不包含类或接口的信息(无法得知它属于的类或接口)。name_index
是常量池中一个Utf8_info
的下标,表示方法名称;descriptor_index
也是一个Utf8_info
的下标,表示一个字段描述符(变量类型)或方法描述符(方法参数和返回值类型)。
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
字段描述符:例如Ljava/lang/Object;
表示一个Object
实例,[[[D
表示double[][][]
。
方法描述符:例如Object m(int i, double d, Thread t)
为(IDLjava/lang/Thread;)Ljava/lang/Object;
。
现在来看反汇编代码中常量池的内容,结构为#<index> = cp_info
。
#1 = Methodref #6.#19 // java/lang/Object."<init>":()V
#6 = Class #26 // java/lang/Object
#10 = Utf8 <init>
#11 = Utf8 ()V
#19 = NameAndType #10:#11 // "<init>":()V
#26 = Utf8 java/lang/Object
- 第1行是一个
Methodref
类型的,它的class_index
为6,name_and_type_index
为19,后面的注释表明这个是Object
类的构造方法。 - 第6行,是
Class
类型,它的name_index
为26。 - 第26行,是
Utf8
类型,它的值为java/lang/Object
。 - 第19行,是
NameAndType
类型,它的name_index
为10,descriptor_index
为11。 - 第10行,是
Utf8
类型,它的值为<init>
,因此它表示的方法名称为<init>
,这是一个特殊方法名称,是构造方法。 - 第11行,是
Utf8
类型,它的值为()V
,表明方法没有参数,返回值为void。
从第1行开始,经过一系列的递归查表,才能确定这个方法所属的类、方法名、参数、返回值。javap将这个值写在了行末的注释里,方便阅读,但实际的字节码里是没有这些的。Methodref
这些也是助记符,class文件里只有一条条的字节码。
#42 = Utf8 (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
再来看一个例子,这是一个Utf8
类型的记录,可以看出它是一个方法描述符,有两个参数,分别为String
类型和Object[]
类型,返回值为PrintStream
类型,这就是System.out.printf
的方法描述符。
虚拟机指令
研究方法的字节码之前需要了解一些基本的字节码指令。
在汇编中,一条指令分为操作码和操作数,常见的CPU的操作数都是放在寄存器中的,例如mov eax ecx
,将一个寄存器中的值赋予另一个寄存器。
对于虚拟机来说,如果把操作数放在寄存器中,就称为基于寄存器的虚拟机,如果把操作数放在栈中,就称为基于栈的虚拟机。一般来说,基于寄存器的虚拟机更为复杂,因为它往往与CPU相关,性能会更好;基于栈的虚拟机更为简单,但性能会更差。
JVM是一个基于栈的虚拟机,它的大多指令,都涉及到对栈的操作。例如load
系列指令,会将变量值压入栈顶;而store
系列指令,会将栈顶元素出栈,存入变量;还有的指令如newarray
,它会将栈上的操作数出栈,然后将指令执行的结果(返回值)入栈。
本文中涉及到的指令:
-
iconst_<i>
将int
常量i
入栈,i
的范围为-1,0,1,2,3,4,5。 -
bipush <b>
将byte
常量b
入栈,在栈中被扩展成有符号整型。iconst_<i>
指令与对应的bipush <i>
等价。 -
astore <index>
将栈顶存入下标为index
的局部变量,引用类型必须为reference
或returnAddress
。 -
astore_<n>
与astore <n>
等价,n
的范围为0,1,2,3。 -
newarray <type>
创建type
类型的数组,数组的长度count
由栈顶的整型指定,创建完成后数组的引用会被push到栈顶。type
只能为基本数据类型。 -
bastore
为byte
或boolean
类型的数组赋值,栈结构为arrayref, index, value
,相当于arrayref[index]=value
。 -
getstatic <byte1> <byte2>
获取类的静态字段值并压入栈中。(byte1<<8)|byte2
为该字段在运行时常量池中的下标。 -
iload <index>
将index表示的int
局部变量加载到栈顶。 -
iload_<n>
等价于iload <n>
,n
的范围为0,1,2,3。 -
invokevirtual <byte1> <byte2>
调用实例方法。操作数栈中结构是这样的..., objectref, arg1, arg2 ...
,使用(byte1<<8)|byte2
来索引方法,运行时常量池中的符号引用包含了方法描述、方法参数个数等信息。将方法所需的nargs出栈,并被设置成方法的局部变量,然后切换到方法的栈帧中,修改PC(程序计数器),开始执行方法中的指令。 -
ldc <index>
将运行时常量池中下标为index
的entry入栈,该entry必须为一个运行时常量,类型为int
、long
、字符串字面量引用、符号引用、方法句柄。 -
anewarray <byte1> <byte2>
创建数组,通过(byte1<<8)|byte2
在常量池中索引类、数组、接口,由此确定数组类型,栈顶参数指定数组长度,创建好的数组引用入栈。 -
aastore
将值存入数组,栈结构为arrayref, index, value
,相当于arrayref[index]=value
,执行完后3个操作数全部出栈。 -
pop
弹出一个栈顶操作数。 -
return
返回void
。
create方法
以下代码是通过javap -c BooleanTest.class
反汇编得来的,-c
不会输出方法中的局部变量表等信息,更为简洁清晰。
public static void create();
Code:
0: iconst_1
1: istore_0
2: iconst_0
3: istore_1
4: bipush 100
6: newarray boolean
8: astore_2
9: aload_2
10: iconst_5
11: iconst_1
12: bastore
13: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
16: iload_0
17: invokevirtual #3 // Method java/io/PrintStream.println:(Z)V
20: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
23: iload_1
24: invokevirtual #3 // Method java/io/PrintStream.println:(Z)V
27: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
30: aload_2
31: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
34: return
第一行boolean a = true
对应2条指令,iconst_1
将整型常量1入栈,istore_0
将栈顶的一个整型常量出栈并存入局部变量0,也就是变量a
。
第二行boolean b = false
也对应2条指令,iconst_0
将0入栈,istore_1
将0出栈存入局部变量1,也就是b
。
第三行boolean[] arr = new boolean[100]
对应3条指令,bipush 100
将100入栈,newarray boolean
,创建一个长度为100的boolean
类型的数组,数组引用放到栈顶,astore_2
将栈顶存入局部变量2,即arr
。
第四行arr[5] = true
对应4条指令,aload_2
将arr
引用入栈,iconst_5
将5入栈,iconst_1
将1入栈,bastore
将数组下标为5的位置赋值为1。
第五行System.out.println(a)
对应3条指令,getstatic
获得System.out
对象的引用并入栈,iload_0
将局部变量1入栈,invokevirtual
索引println
方法,以栈顶为参数进行调用。
print方法
以下代码是通过javap -v BooleanTest.class
反汇编得来的,因为我们需要观察局部变量表。
public static void print(int);
descriptor: (I)V
flags: (0x0009) ACC_PUBLIC, ACC_STATIC
Code:
stack=6, locals=2, args_size=1
0: iload_0
1: istore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: ldc #5 // String %d %d\n
7: iconst_2
8: anewarray #6 // class java/lang/Object
11: dup
12: iconst_0
13: iload_0
14: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
17: aastore
18: dup
19: iconst_1
20: iload_1
21: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
24: aastore
25: invokevirtual #8 // Method java/io/PrintStream.printf:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
28: pop
29: return
LineNumberTable:
line 13: 0
line 14: 2
line 15: 29
LocalVariableTable:
Start Length Slot Name Signature
0 30 0 a I
2 28 1 b I
通过LocalVariableTable
可以看出,函数参数a
也是当做局部变量来处理的,下标为0,局部变量b
下标为1。
第一行int b = a
包含两条指令,iload_0
将a
的值入栈,istore_1
将栈顶出栈到b
。
第二行System.out.printf("%d %d\n", a, b)
包含多条指令
-
getstatic
入栈System.out
的引用,ldc
将常量池中%d %d\n
的引用入栈,iconst_2
将int
常量2入栈,anewarray
创建一个Object[2]
数组并入栈; -
dup
复制栈顶的数组引用并入栈,iconst_0
将int
常量0入栈,iload_0
将变量a
入栈,invokestatic
调用Integer.valueOf
将栈顶装箱成Integer
对象,aastore
将装箱后的a
设置到数组下标0的位置; -
dup
复制栈顶的数组引用并入栈,iconst_1
将int
常量1入栈,iload_1
将变量b
入栈,invokestatic
调用Integer.valueOf
将栈顶装箱,aastore
将装箱后的b
设置到数组下标1的位置; -
invokevirtual
调用printf
方法; -
pop
指令将printf
的返回值出栈
总结
本文实践探索了一个class文件大致的结构:
- 常量池就是一个数组,数组的每一项都具有指令的类型,类型后面是它的值。常量池中的项目通过下标引用。
- class中的常量池称为静态常量池,在类加载的过程中会被合并到运行时常量池中。
-
boolean
类型是当做int
来处理的,1
表示true
,0
表示false
。 -
boolean[]
类型不是byte[]
来处理的,虽然在使用newarray
指令创建的时候指定的是boolean
类型,但是在赋值的时候使用的是字节数组操作指令bastore
。 - 在字节码层面观察到了
Integer
的装箱。 - 局部变量表按照局部变量的声明顺序依次编号的。
- 方法参数也当做局部变量来处理,并且率先编号。
- 方法参数的值是由
invokevirtual
指令设置好的,在方法中可以直接使用。