以這段Java代碼為例候醒,反匯編分析一下對(duì)應(yīng)的Java字節(jié)碼铡溪。將該文件保存為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)
執(zhí)行javac BooleanTest.java; javap -v BooleanTest.class
,會(huì)輸出完整的反匯編代碼苔咪,大致可以分為幾部分:元數(shù)據(jù)+常量池锰悼、一系列方法,我們分開(kāi)來(lái)看团赏。
常量池
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;
根據(jù)《Java虛擬機(jī)規(guī)范》箕般,每一個(gè)class文件對(duì)應(yīng)下面這樣一個(gè)ClassFile
結(jié)構(gòu)。
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
:唯一作用是確定這個(gè)文件是否為一個(gè)class文件舔清,值固定為0xCAFEBABE
丝里。 -
minor_version
和major_version
用來(lái)表示class文件的版本號(hào)曲初,不同版本的JDK編譯出的版本號(hào)不同,例如主版本號(hào)為52表示是Java 8版本的class杯聚、55表示是Java 11版本的class臼婆,參考。 -
constant_pool_count
常量池大小幌绍。 -
constant_pool
常量池颁褂。 -
access_flags
訪問(wèn)標(biāo)志,ACCESS_PUBLIC
表示這是個(gè)public類(lèi)傀广,ACCESS_SUPER
沒(méi)什么意義颁独,所有jdk 1.0.2之后編譯出的class都帶有這個(gè)標(biāo)志。 -
this_class
本類(lèi)在常量池中的一個(gè)索引伪冰。 - ......
常量池中的每一項(xiàng)都具有如下通用格式誓酒,單字節(jié)的tag
表示cp_info
的實(shí)際類(lèi)型,后面info
數(shù)組的內(nèi)容由類(lèi)型決定贮聂。tag
可以表示Class
, Fieldref
, Methodref
, InterfaceMethodref
, String
, Integer
, Float
, Long
, Double
, NameAndType
, Utf8
, MethodHandle
, MethodType
, InvokeDynamic
靠柑。
cp_info {
u1 tag;
u1 info[];
}
Class_info
的結(jié)構(gòu)如下,表示一個(gè)類(lèi)或接口吓懈,name_index
是常量池中一個(gè)Utf8_info
項(xiàng)的下標(biāo)歼冰,表示類(lèi)或接口名。
CONSTANT_Class_info {
u1 tag;
u2 name_index;
}
Fieldref_info
, Methodref_info
的結(jié)構(gòu)如下骄瓣,分別表示字段引用和方法引用停巷,他們包含兩個(gè)字段,class_index
表示字段榕栏、方法所在的類(lèi)在常量池中的索引畔勤,name_and_type_index
表示當(dāng)前字段或方法的名字和描述符。
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
的結(jié)構(gòu)如下扒磁,用于表示一個(gè)String類(lèi)型的常量對(duì)象庆揪,string_index
是常量池中一個(gè)Utf8_info
項(xiàng)的下標(biāo)。
CONSTANT_String_info {
u1 tag;
u2 string_index;
}
Utf8_info
的結(jié)構(gòu)如下妨托,用于表示一個(gè)Utf8字符串值常量缸榛。
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}
NameAndType_info
結(jié)構(gòu)如下,用于表示一個(gè)方法或字段兰伤,不包含類(lèi)或接口的信息(無(wú)法得知它屬于的類(lèi)或接口)内颗。name_index
是常量池中一個(gè)Utf8_info
的下標(biāo),表示方法名稱(chēng)敦腔;descriptor_index
也是一個(gè)Utf8_info
的下標(biāo)均澳,表示一個(gè)字段描述符(變量類(lèi)型)或方法描述符(方法參數(shù)和返回值類(lèi)型)。
CONSTANT_NameAndType_info {
u1 tag;
u2 name_index;
u2 descriptor_index;
}
字段描述符:例如Ljava/lang/Object;
表示一個(gè)Object
實(shí)例,[[[D
表示double[][][]
找前。
方法描述符:例如Object m(int i, double d, Thread t)
為(IDLjava/lang/Thread;)Ljava/lang/Object;
糟袁。
現(xiàn)在來(lái)看反匯編代碼中常量池的內(nèi)容,結(jié)構(gòu)為#<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行是一個(gè)
Methodref
類(lèi)型的项戴,它的class_index
為6,name_and_type_index
為19槽惫,后面的注釋表明這個(gè)是Object
類(lèi)的構(gòu)造方法周叮。 - 第6行,是
Class
類(lèi)型界斜,它的name_index
為26则吟。 - 第26行,是
Utf8
類(lèi)型锄蹂,它的值為java/lang/Object
。 - 第19行水慨,是
NameAndType
類(lèi)型得糜,它的name_index
為10,descriptor_index
為11晰洒。 - 第10行朝抖,是
Utf8
類(lèi)型,它的值為<init>
谍珊,因此它表示的方法名稱(chēng)為<init>
治宣,這是一個(gè)特殊方法名稱(chēng),是構(gòu)造方法砌滞。 - 第11行侮邀,是
Utf8
類(lèi)型,它的值為()V
贝润,表明方法沒(méi)有參數(shù)绊茧,返回值為void。
從第1行開(kāi)始打掘,經(jīng)過(guò)一系列的遞歸查表华畏,才能確定這個(gè)方法所屬的類(lèi)、方法名尊蚁、參數(shù)亡笑、返回值。javap將這個(gè)值寫(xiě)在了行末的注釋里横朋,方便閱讀仑乌,但實(shí)際的字節(jié)碼里是沒(méi)有這些的。Methodref
這些也是助記符,class文件里只有一條條的字節(jié)碼绝骚。
#42 = Utf8 (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
再來(lái)看一個(gè)例子耐版,這是一個(gè)Utf8
類(lèi)型的記錄,可以看出它是一個(gè)方法描述符压汪,有兩個(gè)參數(shù)粪牲,分別為String
類(lèi)型和Object[]
類(lèi)型,返回值為PrintStream
類(lèi)型止剖,這就是System.out.printf
的方法描述符腺阳。
虛擬機(jī)指令
研究方法的字節(jié)碼之前需要了解一些基本的字節(jié)碼指令。
在匯編中穿香,一條指令分為操作碼和操作數(shù)亭引,常見(jiàn)的CPU的操作數(shù)都是放在寄存器中的,例如mov eax ecx
皮获,將一個(gè)寄存器中的值賦予另一個(gè)寄存器焙蚓。
對(duì)于虛擬機(jī)來(lái)說(shuō),如果把操作數(shù)放在寄存器中洒宝,就稱(chēng)為基于寄存器的虛擬機(jī)购公,如果把操作數(shù)放在棧中,就稱(chēng)為基于棧的虛擬機(jī)雁歌。一般來(lái)說(shuō)宏浩,基于寄存器的虛擬機(jī)更為復(fù)雜,因?yàn)樗cCPU相關(guān)靠瞎,性能會(huì)更好比庄;基于棧的虛擬機(jī)更為簡(jiǎn)單,但性能會(huì)更差乏盐。
JVM是一個(gè)基于棧的虛擬機(jī)佳窑,它的大多指令,都涉及到對(duì)棧的操作父能。例如load
系列指令华嘹,會(huì)將變量值壓入棧頂;而store
系列指令法竞,會(huì)將棧頂元素出棧耙厚,存入變量;還有的指令如newarray
岔霸,它會(huì)將棧上的操作數(shù)出棧薛躬,然后將指令執(zhí)行的結(jié)果(返回值)入棧。
本文中涉及到的指令:
-
iconst_<i>
將int
常量i
入棧呆细,i
的范圍為-1,0,1,2,3,4,5型宝。 -
bipush <b>
將byte
常量b
入棧八匠,在棧中被擴(kuò)展成有符號(hào)整型。iconst_<i>
指令與對(duì)應(yīng)的bipush <i>
等價(jià)趴酣。 -
astore <index>
將棧頂存入下標(biāo)為index
的局部變量梨树,引用類(lèi)型必須為reference
或returnAddress
。 -
astore_<n>
與astore <n>
等價(jià)岖寞,n
的范圍為0,1,2,3抡四。 -
newarray <type>
創(chuàng)建type
類(lèi)型的數(shù)組,數(shù)組的長(zhǎng)度count
由棧頂?shù)恼椭付ㄕ套唬瑒?chuàng)建完成后數(shù)組的引用會(huì)被push到棧頂指巡。type
只能為基本數(shù)據(jù)類(lèi)型。 -
bastore
為byte
或boolean
類(lèi)型的數(shù)組賦值隶垮,棧結(jié)構(gòu)為arrayref, index, value
藻雪,相當(dāng)于arrayref[index]=value
。 -
getstatic <byte1> <byte2>
獲取類(lèi)的靜態(tài)字段值并壓入棧中狸吞。(byte1<<8)|byte2
為該字段在運(yùn)行時(shí)常量池中的下標(biāo)勉耀。 -
iload <index>
將index表示的int
局部變量加載到棧頂。 -
iload_<n>
等價(jià)于iload <n>
蹋偏,n
的范圍為0,1,2,3瑰排。 -
invokevirtual <byte1> <byte2>
調(diào)用實(shí)例方法。操作數(shù)棧中結(jié)構(gòu)是這樣的..., objectref, arg1, arg2 ...
暖侨,使用(byte1<<8)|byte2
來(lái)索引方法,運(yùn)行時(shí)常量池中的符號(hào)引用包含了方法描述崇渗、方法參數(shù)個(gè)數(shù)等信息字逗。將方法所需的nargs出棧,并被設(shè)置成方法的局部變量宅广,然后切換到方法的棧幀中葫掉,修改PC(程序計(jì)數(shù)器),開(kāi)始執(zhí)行方法中的指令跟狱。 -
ldc <index>
將運(yùn)行時(shí)常量池中下標(biāo)為index
的entry入棧俭厚,該entry必須為一個(gè)運(yùn)行時(shí)常量,類(lèi)型為int
驶臊、long
挪挤、字符串字面量引用、符號(hào)引用关翎、方法句柄扛门。 -
anewarray <byte1> <byte2>
創(chuàng)建數(shù)組,通過(guò)(byte1<<8)|byte2
在常量池中索引類(lèi)纵寝、數(shù)組论寨、接口,由此確定數(shù)組類(lèi)型,棧頂參數(shù)指定數(shù)組長(zhǎng)度葬凳,創(chuàng)建好的數(shù)組引用入棧绰垂。 -
aastore
將值存入數(shù)組,棧結(jié)構(gòu)為arrayref, index, value
火焰,相當(dāng)于arrayref[index]=value
劲装,執(zhí)行完后3個(gè)操作數(shù)全部出棧。 -
pop
彈出一個(gè)棧頂操作數(shù)荐健。 -
return
返回void
酱畅。
create方法
以下代碼是通過(guò)javap -c BooleanTest.class
反匯編得來(lái)的,-c
不會(huì)輸出方法中的局部變量表等信息江场,更為簡(jiǎn)潔清晰纺酸。
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
對(duì)應(yīng)2條指令,iconst_1
將整型常量1入棧址否,istore_0
將棧頂?shù)囊粋€(gè)整型常量出棧并存入局部變量0餐蔬,也就是變量a
。
第二行boolean b = false
也對(duì)應(yīng)2條指令佑附,iconst_0
將0入棧樊诺,istore_1
將0出棧存入局部變量1,也就是b
音同。
第三行boolean[] arr = new boolean[100]
對(duì)應(yīng)3條指令词爬,bipush 100
將100入棧,newarray boolean
权均,創(chuàng)建一個(gè)長(zhǎng)度為100的boolean
類(lèi)型的數(shù)組顿膨,數(shù)組引用放到棧頂,astore_2
將棧頂存入局部變量2叽赊,即arr
恋沃。
第四行arr[5] = true
對(duì)應(yīng)4條指令,aload_2
將arr
引用入棧必指,iconst_5
將5入棧囊咏,iconst_1
將1入棧,bastore
將數(shù)組下標(biāo)為5的位置賦值為1塔橡。
第五行System.out.println(a)
對(duì)應(yīng)3條指令梅割,getstatic
獲得System.out
對(duì)象的引用并入棧,iload_0
將局部變量1入棧葛家,invokevirtual
索引println
方法炮捧,以棧頂為參數(shù)進(jìn)行調(diào)用。
print方法
以下代碼是通過(guò)javap -v BooleanTest.class
反匯編得來(lái)的惦银,因?yàn)槲覀冃枰^察局部變量表咆课。
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
通過(guò)LocalVariableTable
可以看出末誓,函數(shù)參數(shù)a
也是當(dāng)做局部變量來(lái)處理的,下標(biāo)為0书蚪,局部變量b
下標(biāo)為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
創(chuàng)建一個(gè)Object[2]
數(shù)組并入棧; -
dup
復(fù)制棧頂?shù)臄?shù)組引用并入棧敬察,iconst_0
將int
常量0入棧秀睛,iload_0
將變量a
入棧,invokestatic
調(diào)用Integer.valueOf
將棧頂裝箱成Integer
對(duì)象莲祸,aastore
將裝箱后的a
設(shè)置到數(shù)組下標(biāo)0的位置蹂安; -
dup
復(fù)制棧頂?shù)臄?shù)組引用并入棧,iconst_1
將int
常量1入棧锐帜,iload_1
將變量b
入棧田盈,invokestatic
調(diào)用Integer.valueOf
將棧頂裝箱,aastore
將裝箱后的b
設(shè)置到數(shù)組下標(biāo)1的位置缴阎; -
invokevirtual
調(diào)用printf
方法允瞧; -
pop
指令將printf
的返回值出棧
總結(jié)
本文實(shí)踐探索了一個(gè)class文件大致的結(jié)構(gòu):
- 常量池就是一個(gè)數(shù)組,數(shù)組的每一項(xiàng)都具有指令的類(lèi)型蛮拔,類(lèi)型后面是它的值述暂。常量池中的項(xiàng)目通過(guò)下標(biāo)引用。
- class中的常量池稱(chēng)為靜態(tài)常量池语泽,在類(lèi)加載的過(guò)程中會(huì)被合并到運(yùn)行時(shí)常量池中。
-
boolean
類(lèi)型是當(dāng)做int
來(lái)處理的视卢,1
表示true
踱卵,0
表示false
。 -
boolean[]
類(lèi)型不是byte[]
來(lái)處理的据过,雖然在使用newarray
指令創(chuàng)建的時(shí)候指定的是boolean
類(lèi)型惋砂,但是在賦值的時(shí)候使用的是字節(jié)數(shù)組操作指令bastore
。 - 在字節(jié)碼層面觀察到了
Integer
的裝箱绳锅。 - 局部變量表按照局部變量的聲明順序依次編號(hào)的西饵。
- 方法參數(shù)也當(dāng)做局部變量來(lái)處理,并且率先編號(hào)鳞芙。
- 方法參數(shù)的值是由
invokevirtual
指令設(shè)置好的眷柔,在方法中可以直接使用期虾。