Java 反汇编分析(一)
以这段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指令设置好的,在方法中可以直接使用。