最近由于項(xiàng)目要求,需要對(duì) Java Class 文件進(jìn)行更改。因此必須先了解 Java Class 文件的結(jié)構(gòu)。下面是對(duì)?JVMS(Java Virtual Machine Specification)?和一些博客內(nèi)容的總結(jié)。
每個(gè) class 文件包括了一個(gè)類或者接口的定義。盡管并不是每個(gè)類或者接口都要在一個(gè)文件中有外部表示(例如通過類加載器生成的類),我們一般認(rèn)為 class 文件格式是一個(gè)類或接口的有效表示。
一個(gè) class 文件由 8位字節(jié)流構(gòu)成。所有16位、32位以及64位的屬性都通過讀取2個(gè)、4個(gè)或者8個(gè)連續(xù)的8位字節(jié)構(gòu)造出來,并以此類推。多字節(jié)字段用大端法存儲(chǔ),也就是說高位優(yōu)先。在 Java SE 平臺(tái)中,這種格式由 接口 java.io.DataInput 和 java.io.DataOutput 以及 java.io.DataInputStream 和 java.io.DataOutputStream 等類支持。
Java Class 文件結(jié)構(gòu)
一個(gè) Java Class 文件包括 10 個(gè)基本組成部分:
魔數(shù): 0xCAFEBABE
Class 文件格式版本號(hào):class 文件的主次版本號(hào)(the minor and major versions)
常量池(Constant Pool):包含 class 中的所有常量
訪問標(biāo)記(Access Flags):例如該 class 是否為抽象類、靜態(tài)類,等等。
這里有一些可變長度部分,例如常量池、方法、以及屬性,因此在加載之前無法知道 Java Class 文件的長度。在這些部分的前面都有長度信息。這樣 JVM 在真正加載這些部分之前就可以知道可變長度部分的大小。
Class 文件中的數(shù)據(jù)都是按照單字節(jié)對(duì)齊并且緊密壓縮。這使得 Class 文件能盡可能小。
Java Class 文件中不同部分的順序是嚴(yán)格定義的,因此 JVM 知道 Class 文件中每個(gè)部分分別是什么、要按照什么順序加載。
下面來詳細(xì)看看一個(gè) Class 文件中的每個(gè)部分。
魔數(shù)(Magic number)
魔數(shù)(Magic number)用來唯一確定格式并和其它格式區(qū)別開來。 Class 文件的頭四個(gè)字節(jié)是0xCAFEBABE。
Class 文件版本號(hào)
Class 文件接下來的 4 個(gè)字節(jié)表示主次版本號(hào)。這個(gè)數(shù)字使得 JVM 可以識(shí)別和驗(yàn)證 class 文件。如果數(shù)字比 JVM 能夠加載的還要大,就會(huì)拒接加載該 class 文件并拋出?java.lang.UnsupportedClassVersionError?異常。
你可以使用?javap?命令行工具查看任意 Java Class 文件的版本號(hào)。例如:
1
javap -verbose MyClass
假設(shè)我們有如下一個(gè) Java 類:
1
public?class?HelloWorld {
2
??private?String msg;
3
??public?HelloWorld(String msg) {
4
????this.msg = msg;
5
??}
6
??public?HelloWorld() {
7
????this.msg =?"Default message";
8
??}
9
??public?String getMsg() {
10
????return?msg;
11
??}
12
??public?void?setMsg(String msg) {
13
????this.msg = msg;
14
??}
15
??public?void?printMsg() {
16
????System.out.println(msg);
17
??}
18
??public?static?void?main(String args[]) {
19
????HelloWorld hw =?new?HelloWorld("Hello world from Java");
20
????hw.printMsg();
21
??}
22
}
我們用命令?javac HelloWorld.java?編譯創(chuàng)建 class 文件。然后執(zhí)行?javap -verbose HelloWorld命令查看 class 文件的版本號(hào):
下面是一個(gè)主版本號(hào)(Major version)和 class 文件對(duì)應(yīng) JDK 版本號(hào)的列表。
下面是單字節(jié)標(biāo)記對(duì)應(yīng)的值及其解釋,對(duì)于每個(gè)類型對(duì)應(yīng)的結(jié)構(gòu)體,可以參考?JVMS The Constant Pool。
常量類型
值
CONSTANT_Class
7
CONSTANT_Fieldref
9
CONSTANT_Methodref
10
CONSTANT_InterfaceMethodref
11
CONSTANT_String
8
CONSTANT_Integer
3
CONSTANT_Float
4
CONSTANT_Long
5
CONSTANT_Double
6
CONSTANT_NameAndType
12
CONSTANT_Utf8
1
CONSTANT_MethodHandle
15
CONSTANT_MethodType
16
CONSTANT_InvokeDynamic
18
訪問標(biāo)記(Access flags)
常量池后面的就是訪問標(biāo)記。它由兩個(gè)字節(jié)組成,表示該文件定義的是類還是接口、如果是個(gè)類,是 public、abstract還是 final 等。下面是訪問標(biāo)記列表及其對(duì)應(yīng)的解釋:
標(biāo)記名稱值解釋
ACC_PUBLIC
0x0001
表示public/strong>;包外的類也可以訪問。
ACC_FINAL
0x0010
表示?final;不允許有任何子類。
ACC_SUPER
0x0020
通過 invokespecial 指令調(diào)用時(shí)調(diào)用父類的方法。
ACC_INTERFACE
0x0200
是一個(gè)接口而不是類
ACC_ABSTRACT
0x0400
表示?抽象類,不能被實(shí)例化。
this Class
This class 是一個(gè)兩個(gè)字節(jié)的條目,它的值是一個(gè)常量池索引。例如對(duì)于 HelloWorld.class 文件,該處的值是0x0006。在常量池中這個(gè)索引指向的條目包括兩個(gè)部分,第一個(gè)部分是單字節(jié)標(biāo)記,表示這是一個(gè)類或是接口,第二部分又是一個(gè)兩個(gè)字節(jié)的常量池索引,指向表示該類或接口的字符串字面值。例如在這個(gè)例子中,0x0006?索引所在的條目是一個(gè)Class_info,它指向索引值為?0x0021,也就是 33 的?Utf8_info,這個(gè) utf8_info 的值為 HelloWorld,也就是實(shí)際的類名。可以查看上面 Java Class 文件常量池示意圖對(duì)應(yīng) #6 和 #33部分。
super Class
接下來的 2 個(gè)字節(jié)是該類的父類(Super Class)。和 this class 類似,兩個(gè)字節(jié)的值是常量池的一個(gè)索引,該索引處的常量值是該類的父類。
接口(Interfaces)
該類(或接口)定義的所有接口都在 class 文件的這個(gè)部分。起始的兩個(gè)字節(jié)表示接口的數(shù)目,接下來是一個(gè)數(shù)組,每個(gè)數(shù)據(jù)包括兩個(gè)字節(jié),這兩個(gè)字節(jié)的值又是一個(gè)常量池索引,指向具體的接口名稱。
字段(Fields)
一個(gè)字段是類或者接口在實(shí)例或類層面的變量(屬性)。字段(Fields)部分只包括 class 文件中類或接口定義的字段,而不包括從父類或父接口中繼承而來的字段。
????????HelloWorld helloWorld =?new?HelloWorld("Hello world from Java");
31
????????helloWorld.printMsg();
32
????}
33
}
看起來和上面的 HelloWorld.java 完全一樣,這時(shí)候我們?cè)傩薷?.java 文件,更改類名,然后再編譯得到新的類。這對(duì)于一個(gè) Java 新手來說都是輕而易舉。但問題是,對(duì)于一個(gè)復(fù)雜的類或者有很多 .class 文件的 jar 包,反編譯的結(jié)果仍然正確嗎?
答案顯然是否定的,我嘗試了Decompilers online?上面的所有方法去反編譯一個(gè) JDBC Jar 包,得到的結(jié)果存在一大堆錯(cuò)誤,從顯而易見的到人肉眼都難以發(fā)現(xiàn)的錯(cuò)誤都有。如果這時(shí)候再去一一修正,顯然比較困難。一方面反編譯出來的源碼比較晦澀難懂,例如它里面使用了非常多的 switch case 語句,或者對(duì)于無法簡單判斷出來的類型,反編譯器使用了 Object 類代替;另一方面,反編譯出來的源碼是沒有注釋的,一個(gè)有上千個(gè)文件但卻沒有一行注釋的源碼,單只是想想就令人恐懼。
是什么原因呢?這里我們只替換了字符串,但沒有替換字符串前面的長度。那么如果替換前后字符串長度相同是不是就可以了呢?例如我想替換為 MycppWorld。再來嘗試一次,結(jié)果在上面的截圖中。可以看出,對(duì)于相同長度的字符串,簡單地進(jìn)行字符串替換是可以達(dá)到 Hack Class File 目的的。同樣,對(duì)于字符串長度不一樣的情況,我們只需要同時(shí)修改字符串前面的長度即可。通過閱讀?JVMS?中的?Class File Format?章節(jié),發(fā)現(xiàn)其實(shí)只需要修改?Constant Pool?部分、其余保持不變即可。例如說下面這個(gè)簡單的事例程序,它實(shí)現(xiàn)了 Class 文件 Constant Pool 部分的字符串替換:
1
import?java.io.BufferedReader;
2
import?java.io.File;
3
import?java.io.FileInputStream;
4
import?java.io.FileOutputStream;
5
import?java.io.IOException;
6
import?java.io.InputStreamReader;
7
import?java.nio.ByteBuffer;
8
import?java.nio.ByteOrder;
9
?
10
/**
11
?* String replace in Java .class file.
12
?* Reference: Java Virtual Machine Specification CLASS file format