Write once, run everywhere!
,咱們都知道這是 Java
著名的宣傳口號。不一樣的操做系統,不一樣的 CPU 具備不一樣的指令集,如何作到平臺無關性,依靠的就是 Java 虛擬機。計算機永遠只能識別 0
和 1
組成的二進制文件,虛擬機就是咱們編寫的代碼和計算機之間的橋樑。虛擬機將咱們編寫的 .java
源程序文件編譯爲 字節碼
格式的 .class
文件,字節碼是各類虛擬機與全部平臺統一使用的程序存儲格式,這是平臺無關性的本質,虛擬機在操做系統的應用層實現了平臺無關。實際上不只僅是平臺無關,JVM 也是 語言無關
的。常見的 JVM 語言,如 Scala
,Groovy
,再到最近的 Android 官方開發語言 Kotlin
,通過各自的語言編譯器最終都會編譯爲 .class
文件。適當的瞭解 Class 文件格式,對咱們開發,逆向都是大有裨益的。java
class 文件的結構很清晰,以下所示:android
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];
}
複製代碼
其中的 u2
u4
分別表明 2 和 4 字節的無符號數。另外須要注意的是 classs 文件的多字節數據是按照大端表示法(big-endian)進行存儲的,在解析的時候須要注意。git
瞭解一種文件結構最好的方法就是去解析它,包括以後的 AndroidManifest.xml
、dex
等等,都會經過代碼直接解析來學習它們的文件結構。下面就以最簡單的 Hello.java
程序進行解析:github
public class Hello {
private static String HELLO_WORLD = "Hello World!";
public static void main(String[] args) {
System.out.println(HELLO_WORLD);
}
}
複製代碼
javac
命令編譯生成 Hello.class
文件。這裏推薦一個利器 010Editor
,查看分析各類二進制文件結構十分方便,相比 Winhex
或者 Ghex
更加智能。下面是經過 010Editor
打開 Hello.class
文件的截圖:數組
文件結構一目瞭然。點擊各個結構也會自動標記處上半部分文件內容中對應的十六進制數據,至關方便。下面就對照着結構目錄逐項解析。bash
class 文件的魔數頗有意思, 0xCAFEBABE
,也許 Java 創始人真的很熱衷於咖啡吧,包括 Java 的圖標也是一杯咖啡。微信
minor_version
是次版本號,major_version
是主版本號。每一個版本的 JDK 都有本身特定的版本號。高版本的 JDK 向下兼容低版本的 Class 文件,但低版本不能運行高版本的 Class 文件,即便文件格式沒有發生任何變化,虛擬機也拒絕執行高於其版本號的 Class 文件。上面圖中主版本號爲 52,表明 JDK 1.8,在 JDK 1.8 如下的版本是沒法執行的。數據結構
常量池是 Class 文件中的重中之重,存放着各類數據類型,與其餘項目關聯甚多。在解析的時候,咱們能夠把常量池當作一個數組或者集合,既然是數組或者集合,就要先肯定它的長度。首先看一下 Hello.class
文件的常量池部分的截圖:post
常量池部分以一個 u2 類型開頭,表明常量池中的容量,上例中爲 34
。須要注意的是,常量池的下標是從 1
開始的,也就表明該 Class 文件具備 33 個常量。那麼,爲何下標要從 1 開始呢?目的是爲了表示在特定狀況下 不引用任何一個常量池項
,這時候下標就用 0
表示。學習
下表是常量池的一些常見數據類型:
類 型 | 標 志 | 描 述 |
---|---|---|
CONSTANT_Utf8_info | 1 | UTF-8 編碼的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮點型字面量 |
CONSTANT_Long_info | 5 | 長整型字面量 |
CONSTANT_Double_info | 6 | 雙精度浮點型字面量 |
CONSTANT_Class_info | 7 | 類或接口的符號引用 |
CONSTANT_String_info | 8 | 字符串類型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符號引用 |
CONSTANT_Methodref_info | 10 | 類中方法的符號引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符號引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的部分符號引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 標識方法類型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一個動態方法調用點 |
常量池的數據類型有十幾種,各自都有本身的數據結構,可是他們都有一個共有屬性 tag
。tag
是標誌位,標記是哪種數據結構。咱們在這裏不針對每種數據結構進行分析,就按照 Hello.class
文件的常量池結構粗略分析一下。
首先看 Hello.class 文件常量池的第一項:
這是一個 CONSTANT_Methodref_info
, 表示類中方法的一些信息,它的數據結構是 tag
class_index
name_and_type_index
。tag
標識爲 10。class_index
的值是 7,這是一個常量池索引,指向常量池中的某一項數據。注意,常量池的索引是從 1 開始的,因此這裏指向的實際上是第 6 個數據項:
CONSTANT_Methodref_info
的 class_index
指向的數據項永遠是 CONSTANT_Class_info
,tag
標識爲 7,表明的是類或者接口,它的 name_index
也是常量池索引,上圖中能夠看到是第 26 項:
這是一個 CONSTANT_Utf8_info
,從名稱就能夠看出來這是一個字符串,length
屬性標識長度,後面的 byte[]
表明字符串內容。從 010Editor 解析內容能夠看到這個字符串是 java/lang/Object
,表示類的全限定名。
接着回到常量池第一項 CONSTANT_Methodref_info
,剛纔看了 name_index
屬性,另外一個屬性是 name_and_type_index
,它永遠指向 CONSTANT_NameAndType_info
,表示字段或者方法,它的值爲 19,咱們來看一下常量池的第 18 項:
CONSTANT_NameAndType_info
的 tag 標識爲 12,具備兩個屬性, name_index
和 descriptor_index
,它們指向的均是 CONSTANT_Utf8_info
。name_index
表示字段或者方法的非限定名,這裏的值是 <init>
。descriptor_index
表示字段描述符或者方法描述符,這裏的值是 ()V
。
到這裏,常量池的第一個數據項就分析完了,後面的每個數據項均可以按照這樣分析。到這裏就能夠看到常量池的重要性了,包含了 Class 文件的大部分信息。
接着繼續分析常量池以後的文件結構,先整體瀏覽一下:
訪問表示,表示類或接口的訪問權限和屬性,下圖爲一些訪問標誌的取值和含義:
標誌名稱 | 標 志 值 | 含 義 |
---|---|---|
ACC_PUBIC | 0x0001 | 是否爲 public 類型 |
ACC_FINAL | 0x0010 | 是否聲明爲 final |
ACC_SUPER | 0x0020 | JDK1.0.2 以後編譯出來的類這個標誌都必須爲真 |
ACC_INTERFACE | 0x0200 | 是否爲接口 |
ACC_ABSTRACT | 0x0400 | 是否爲 abstract 類型 |
ACC_SYNTHETIC | 0x1000 | 標記這個類並不是由用戶代碼產生 |
ACC_ANNOTATION | 0x2000 | 是否爲註解 |
ACC_ENUM | 0x4000 | 是否爲枚舉類型 |
Hello.class
文件的訪問標記爲十進制 33,Hello.java
就是一個普通的類,由 public
修飾,因此應該具備 ACC_PUBIC
和 ACC_SUPER
這兩個標記, 0x0001 + 0x0010
正好爲十進制 33。
爲何把這幾項放在同一節進行解釋?由於這幾項數據共同肯定了該類的繼承關係。 this_class
表明類索引,用於肯定該類的全限定名,圖中能夠看到索引值爲 6 ,指向常量池中第 5 個數據項,這個數據項必須是 CONSTANT_Class_info
,查找常量池能夠看到類名爲 Hello
,表明當前類的名字。super_class
表示父類索引, 一樣也是指向 CONSTANT_Class_info
,值爲 java/lang/Object
。咱們都知道,Object 是 java 中惟一一個沒有父類的類,所以它的父類索引爲 0 。
super_class
以後緊接的兩個字符是 interfaces_count
,表示的是該類實現的接口數量。因爲 Hello.java
未實現任何接口,因此該值爲 0。若是實現了若干接口,這些接口信息將存儲在以後的 interfaces[]
之中。
字段表集合,表示該類中聲明的變量。fields_count
指明變量的個數,fields[]
存儲變量的信息。注意,這裏的變量指的是成員變量,並不包括方法中的局部變量。再回憶一下 Hello.java
文件,僅有一個變量:
private static String HELLO_WORLD = "Hello World!";
複製代碼
上面這行變量聲明告訴咱們,有一個叫 HELLO_WORLD
的 String
類型變量,且是 private static
修飾的。因此 fields[]
所需存儲的也正是這些信息。先來看下 filed_info
的結構:
access_flags
是訪問標誌,表示字段的訪問權限和基本屬性,和以前分析過的類的訪問標誌是很類似的。下表是一些常見的訪問標誌的名稱和含義:
標誌名稱 | 標 志 值 | 含 義 |
---|---|---|
ACC_PUBIC | 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_ENUM | 0x4000 | 是否爲 enum |
private static
即爲 0x0002 + 0x0008
,等於十進制的 10。
name_index
爲常量池索引,表示字段的名稱,查看常量池第 7 項,是一個 CONSTANT_Utf8_info
,值爲 HELLO_WORLD
。
descriptor_index
也是常量池索引,表示字段的描述,查看常量池第 8 項,是一個 CONSTANT_Utf8_info
,值爲 Ljava/lang/String;
。
這樣便獲得了這個字段的完整信息。在圖中還能夠看到,descriptor_index
後面還跟着 attributes_count
,這裏的值爲 0,不然後面還會跟着 attributes[]
。關於屬性表後面還會專門分析到,這裏先不作分析。
緊接着字段表集合的是方法表集合,表示類中的方法。方法表集合和字段表集合的結構很類似,以下圖所示:
access_flags
表示訪問標誌,其標誌值和字段表略有不一樣,以下所示:
標誌名稱 | 標 志 值 | 含 義 |
---|---|---|
ACC_PUBIC | 0x0001 | 是否爲 public |
ACC_PRIVATE | 0x0002 | 是否爲 private |
ACC_PROTECTED | 0x0004 | 是否爲 protected |
ACC_STATIC | 0x0008 | 是否爲 static |
ACC_FINAL | 0x0010 | 是否爲 final |
ACC_SYNCHRONIZED | 0x0020 | 是否爲 sychronized |
ACC_BRIDGE | 0x0040 | 是否由編譯器產生的橋接方法 |
ACC_VARARGS | 0x0080 | 是否接受不定參數 |
ACC_NATIVE | 0x0100 | 是否爲 native |
ACC_ABSTRACT | 0x0400 | 是否爲 abstract |
ACC_STRICTFP | 0x0800 | 是否爲 strictfp |
ACC_SYNTHETIC | 0x1000 | 是否由編譯器自動產生 |
name_index
和 descriptor_index
與字段表同樣,分別表示方法的名稱和方法的描述,指向常量池中的 CONSTANT_Utf8_info
項。方法中的具體代碼存儲在以後的屬性表中,經編譯器編譯爲字節碼格式存儲。屬性表在下一節進行具體分析。
屬性表在以前已經出現過好幾回,包括字段表,方法表都包含了屬性表。屬性表的種類不少,能夠表示源文件名稱,編譯生成的字節碼指令,final 定義的常量值,方法拋出的異常等等。在 《Java虛擬機規範(Java SE 7)》中已經預約義了 21 項屬性。這裏僅對 Hello.class
文件中出現的屬性進行分析。
首先來看下 Hello.class
文件中緊跟在方法表以後的最後兩項。
attributes_count
聲明後面的屬性表長度,這裏爲 1,後面跟了一個屬性。由上圖該屬性結構可知,這是一個定長的屬性,可是大部分屬性類型實際上是不定長的。
attribute_name_index
是屬性名稱索引,指向常量池中的 CONSTANT_Utf8_info
,表明的是屬性的類型,該屬性爲 17,因此指向常量池的第 16 項,查閱常量池,其值爲 SourceFile
,表示這是一個 SourceFile
屬性,其屬性值爲源文件的名稱。
attribute_length
是屬性的長度,但不包含 attribute_name_index
和其自己,因此整個屬性的長度應該是 attribute_length + 6
。
sourcefile_index
是源文件名稱索引,指向常量池中的 CONSTANT_Utf8_info
,索引值爲 18,即指向第 17 項,不難猜想,該項表示的字符串就是源文件名稱 Hello.java
。
這個屬性比較簡單,下面來看 main
方法表中的屬性表,該屬性表所表明的是 main
方法中的代碼通過編譯編譯生成的字節碼:
能夠看到 main
方法的方法表中包含了一個屬性,其結構仍是比較複雜的,下面進行逐項分析。
attribute_name_index
指向常量池第 11 項,其字符串爲 Code
,表示這是一個 Code
屬性。Code
屬性是 Class 文件中最重要的屬性,它存儲的是 Java 代碼編譯生成的字節碼。
attribute_length
爲 38,表示後面 38 個字節都是該屬性的內容。
max_stack
表明了操做數棧深度的最大值。在方法執行的任意時刻,操做數棧都不會超過這個深度。虛擬機運行的時候須要根據這個值來分配棧幀中的操做棧深度。
max_locals
表明了局部變量表所需的存儲空間,以 slot
爲單位。Slot
是虛擬機爲局部變量分配內存所使用的最小單位。
code_length
指的是編譯生成的字節碼的長度, 緊接着的 code
就是用來存儲字節碼的。上圖中能夠看到這裏的字節碼長度是 10 個字節,咱們來看一下這 10 個字節:
B2 00 02 B2 00 03 B6 00 04 B1
複製代碼
字節碼指令是由操做碼(Opcode)以及跟隨其後的所需參數構成的。操做碼是單字節,表明某種特定操做,參數個數可能爲 0。關於字節碼指令集的指令描述,在 《Java 虛擬機規範》 一書中有詳細介紹。
這裏咱們接着分析上述字節碼。首個操做符爲 0xb2
,查表可得表明的操做是 getstatic
,獲取類的靜態字段。其後跟着兩字節的索引值,指向常量池中的第 2 項數據,這是一個 CONSTANT_Fieldref_info
,表示的是一個字段的符號引用。按照上面的分析方式分析一下這個字段,它的類名是 java/lang/System
,名稱是 out
,描述符是 Ljava/io/PrintStream;
。由此可知,0xb20002
這段字節碼的含義是獲取類 System
中類型爲 Ljava/io/PrintStream
的靜態字段 out
。
接着看下一個操做符,仍舊是 0xb2
,同上,分析可得這個字段是類 Hello
中類型爲 Ljava/lang/String
的靜態字段 HELLO_WORLD
。
第三個操做符是 0xb6
,查表其表明的操做爲 invokevirtual
,其含義是調用實例方法。後面緊跟兩個字節,指向常量池中的 CONSTANT_Methodref_info
。查看常量池中的第 4 項數據,分析可得其類名爲 java/io/PrintStream
,方法名爲 println
,方法描述符爲 (Ljava/lang/String;)V
。這三個字節的字節碼執行的操做就是咱們的打印語句了。
最後一個操做符是 0xb1
,表明的操做爲 return
,表示方法返回 void
。到這裏,該方法就執行完畢了。
到這裏, Hello.class
的文件結構就基本分析完了。咱們再回顧一下 Class 文件的基本結構:
魔數 | 副版本號 | 主版本號 | 常量池數量 | 常量 | 訪問標誌 | 類索引 | 父類索引 | 接口數量 | 接口表 | 字段數量 | 字段表 | 方法數量 | 方法表 | 屬性數量 | 屬性表
各個項目嚴格按照順序緊湊地排列在 Class 文件之中,中間沒有任何分隔符。
另外咱們也能夠經過 javap
命令快速查看 Class 文件內容:
javap -verbose Hello.class
結果以下圖所示:
Class 文件格式的代碼解析相對比較簡單,讀到文件流逐項解析便可。
魔數和主副版本解析:
private void parseHeader() {
try {
String magic = reader.readHexString(4);
log("magic: %s", magic);
int minor_version = reader.readUnsignedShort();
log("minor_version: %d", minor_version);
int major_version = reader.readUnsignedShort();
log("major_version: %d", major_version);
} catch (IOException e) {
log("Parser header error:%s", e.getMessage());
}
}
複製代碼
常量池解析:
private void parseConstantPool() {
try {
int constant_pool_count = reader.readUnsignedShort();
log("constant_pool_count: %d", constant_pool_count);
for (int i = 0; i < constant_pool_count - 1; i++) {
int tag = reader.readUnsignedByte();
switch (tag) {
case ConstantTag.METHOD_REF:
ConstantMethodref methodRef = new ConstantMethodref();
methodRef.read(reader);
log("%s", methodRef.toString());
break;
case ConstantTag.FIELD_REF:
ConstantFieldRef fieldRef = new ConstantFieldRef();
fieldRef.read(reader);
log("%s", fieldRef.toString());
break;
case ConstantTag.STRING:
ConstantString string = new ConstantString();
string.read(reader);
log("%s", string.toString());
break;
case ConstantTag.CLASS:
ConstantClass clazz = new ConstantClass();
clazz.read(reader);
log("%s", clazz.toString());
break;
case ConstantTag.UTF8:
ConstantUtf8 utf8 = new ConstantUtf8();
utf8.read(reader);
log("%s", utf8.toString());
break;
case ConstantTag.NAME_AND_TYPE:
ConstantNameAndType nameAndType = new ConstantNameAndType();
nameAndType.read(reader);
log("%s", nameAndType.toString());
break;
}
}
} catch (IOException e) {
log("Parser constant pool error:%s", e.getMessage());
}
}
複製代碼
剩餘信息解析:
private void parseOther() {
try {
int access_flags = reader.readUnsignedShort();
log("access_flags: %d", access_flags);
int this_class = reader.readUnsignedShort();
log("this_class: %d", this_class);
int super_class = reader.readUnsignedShort();
log("super_class: %d", super_class);
int interfaces_count = reader.readUnsignedShort();
log("interfaces_count: %d", interfaces_count);
// TODO parse interfaces[]
int fields_count = reader.readUnsignedShort();
log("fields_count: %d", fields_count);
List<Field> fieldList=new ArrayList<>();
for (int i = 0; i < fields_count; i++) {
Field field=new Field();
field.read(reader);
fieldList.add(field);
log(field.toString());
}
int method_count=reader.readUnsignedShort();
log("method_count: %d", method_count);
List<Method> methodList=new ArrayList<>();
for (int i=0;i<method_count;i++){
Method method=new Method();
method.read(reader);
methodList.add(method);
log(method.toString());
}
int attribute_count=reader.readUnsignedShort();
log("attribute_count: %d", attribute_count);
List<Attribute> attributeList = new ArrayList<>();
for (int i = 0; i < attribute_count; i++) {
Attribute attribute=new Attribute();
attribute.read(reader);
attributeList.add(attribute);
log(attribute.toString());
}
} catch (IOException e) {
e.printStackTrace();
}
}
複製代碼
因爲屬性種類衆多,這裏未對屬性就行詳細解析,僅爲了加深對 Class 文件結構的瞭解,至關於一個低配版的 javap 。
Class 文件結構的基本瞭解就到這裏,文中相關文件和 Class 文件解析工程源碼都在這裏, android-reverse。
下一篇開始學習 smali
語言,Smali 語法解析——Hello World
文章同步更新於微信公衆號:
秉心說
, 專一 Java 、 Android 原創知識分享,LeetCode 題解,歡迎關注!