DEX 文件結構思惟導圖及解析源碼見文末。java
往期目錄:android
Class 文件格式詳解c++
Smali —— 數學運算,條件判斷,循環github
系列第一篇文章就分析過 Class 文件格式,咱們都知道 .java
源文件通過編譯器編譯會生成 JVM 可識別的 .class
文件。在 Android 中,不論是 Dalvik 仍是 Art,和 JVM 的區別仍是很大的。Android 系統並不直接使用 Class 文件,而是將全部的 Class 文件聚合打包成 DEX 文件,DEX 文件相比單個單個的 Class 文件更加緊湊,能夠直接在 Android Runtime 下執行。bash
對於學習熱修復框架,加固和逆向相關知識,瞭解 DEX 文件結構是頗有必要的。再以前解析過 Class 文件和 AndroidManifest.xml 文件結構以後,發現看二進制文件看上癮了。。後面會繼續對 Apk 文件中的其餘文件結構進行分析,例如 so 文件,resources.arsc 文件等。微信
在解析 DEX 文件結構以前,先來看看如何生成 DEX 文件。爲了方便解析,本篇文章中就不從市場上的 App 裏拿 DEX 文件過來解析了,而是手動生成一個最簡單的 DEX 文件。仍是以 Class 文件解析時候用的例子:數據結構
public class Hello {
private static String HELLO_WORLD = "Hello World!";
public static void main(String[] args) {
System.out.println(HELLO_WORLD);
}
}
複製代碼
首先 javac
編譯成 Hello.class
文件,而後利用 Sdk 自帶的 dx
工具生成 DEX 文件:
dx --dex --output=Hello.dex Hello.class
複製代碼
dx 工具位於 Sdk 的 build-tools 目錄下,可添加至環境變量方便調用。dx 也支持多 Class 文件生成 dex。
關於 DEX 文件結構的學習,給你們推薦兩個資料。
第一個是看雪神圖,出自非蟲,
第二個是 Android 源碼中對 DEX 文件格式的定義,dalvik/libdex/DexFile.h,其中詳細定義了 DEX 文件中的各個部分。
第三個是 010 Editor
,在以前解析 AndroidManifest.xml 文件格式解析 也介紹過,它提供了豐富的文件模板,支持常見文件格式的解析,能夠很方便的查看文件結構中的各個部分及其對應的十六進制。通常我在代碼解析文件結構的時候都是對照着 010 Editor 來進行分析。下面貼一張 010 Editor 打開以前生成的 Hello.dex 文件的截圖:
咱們能夠一目瞭然的看到 DEX 的文件結構,着實是一個利器。在詳細解析以前,咱們先來大概給 DEX 文件分個層,以下圖所示:
文末我放了一張詳細的思惟導圖,也能夠對着思惟導圖來閱讀文章。
依次解釋一下:
header :
DEX 文件頭,記錄了一些當前文件的信息以及其餘數據結構在文件中的偏移量string_ids :
字符串的偏移量type_ids :
類型信息的偏移量proto_ids :
方法聲明的偏移量field_ids :
字段信息的偏移量method_ids :
方法信息(所在類,方法聲明以及方法名)的偏移量class_def :
類信息的偏移量data :
: 數據區link_data :
靜態連接數據區從 header
到 data
之間都是偏移量數組,並不存儲真實數據,全部數據都存在 data
數據區,根據其偏移量區查找。對 DEX 文件有了一個大概的認識以後,咱們就來詳細分析一下各個部分。
DEX 文件頭部分的具體格式能夠參考 DexFile.h 中的定義:
struct DexHeader {
u1 magic[8]; // 魔數
u4 checksum; // adler 校驗值
u1 signature[kSHA1DigestLen]; // sha1 校驗值
u4 fileSize; // DEX 文件大小
u4 headerSize; // DEX 文件頭大小
u4 endianTag; // 字節序
u4 linkSize; // 連接段大小
u4 linkOff; // 連接段的偏移量
u4 mapOff; // DexMapList 偏移量
u4 stringIdsSize; // DexStringId 個數
u4 stringIdsOff; // DexStringId 偏移量
u4 typeIdsSize; // DexTypeId 個數
u4 typeIdsOff; // DexTypeId 偏移量
u4 protoIdsSize; // DexProtoId 個數
u4 protoIdsOff; // DexProtoId 偏移量
u4 fieldIdsSize; // DexFieldId 個數
u4 fieldIdsOff; // DexFieldId 偏移量
u4 methodIdsSize; // DexMethodId 個數
u4 methodIdsOff; // DexMethodId 偏移量
u4 classDefsSize; // DexCLassDef 個數
u4 classDefsOff; // DexClassDef 偏移量
u4 dataSize; // 數據段大小
u4 dataOff; // 數據段偏移量
};
複製代碼
其中的 u
表示無符號數,u1
就是 8 位無符號數,u4
就是 32 位無符號數。
magic
通常是常量,用來標記 DEX 文件,它能夠分解爲:
文件標識 dex + 換行符 + DEX 版本 + 0
複製代碼
字符串格式爲 dex\n035\0
,十六進制爲 0x6465780A30333500
。
checksum
是對去除 magic
、 checksum
之外的文件部分做 alder32 算法獲得的校驗值,用於判斷 DEX 文件是否被篡改。
signature
是對除去 magic
、 checksum
、 signature
之外的文件部分做 sha1 獲得的文件哈希值。
endianTag
用於標記 DEX 文件是大端表示仍是小端表示。因爲 DEX 文件是運行在 Android 系統中的,因此通常都是小端表示,這個值也是恆定值 0x12345678
。
其他部分分別標記了 DEX 文件中其餘各個數據結構的個數和其在數據區的偏移量。根據偏移量咱們就能夠輕鬆的得到各個數據結構的內容。下面順着上面的 DEX 文件結構來認識第一個數據結構 string_ids
。
struct DexStringId {
u4 stringDataOff;
};
複製代碼
string_ids
是一個偏移量數組,stringDataOff
表示每一個字符串在 data 區的偏移量。根據偏移量在 data 區拿到的數據中,第一個字節表示的是字符串長度,後面跟着的纔是字符串數據。這塊邏輯比較簡單,直接看一下代碼:
private void parseDexString() {
log("\nparse DexString");
try {
int stringIdsSize = dex.getDexHeader().string_ids__size;
for (int i = 0; i < stringIdsSize; i++) {
int string_data_off = reader.readInt();
byte size = dexData[string_data_off]; // 第一個字節表示該字符串的長度,以後是字符串內容
String string_data = new String(Utils.copy(dexData, string_data_off + 1, size));
DexString string = new DexString(string_data_off, string_data);
dexStrings.add(string);
log("string[%d] data: %s", i, string.string_data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
複製代碼
打印結果以下:
parse DexString
string[0] data: <clinit>
string[1] data: <init>
string[2] data: HELLO_WORLD
string[3] data: Hello World!
string[4] data: Hello.java
string[5] data: LHello;
string[6] data: Ljava/io/PrintStream;
string[7] data: Ljava/lang/Object;
string[8] data: Ljava/lang/String;
string[9] data: Ljava/lang/System;
string[10] data: V
string[11] data: VL
string[12] data: [Ljava/lang/String;
string[13] data: main
string[14] data: out
string[15] data: println
複製代碼
其中包含了變量名,方法名,文件名等等,這個字符串池在後面其餘結構的解析中也會常常遇到。
struct DexTypeId {
u4 descriptorIdx;
};
複製代碼
type_ids
表示的是類型信息,descriptorIdx
指向 string_ids
中元素。根據索引直接在上一步讀取到的字符串池便可解析對應的類型信息,代碼以下:
private void parseDexType() {
log("\nparse DexTypeId");
try {
int typeIdsSize = dex.getDexHeader().type_ids__size;
for (int i = 0; i < typeIdsSize; i++) {
int descriptor_idx = reader.readInt();
DexTypeId dexTypeId = new DexTypeId(descriptor_idx, dexStringIds.get(descriptor_idx).string_data);
dexTypeIds.add(dexTypeId);
log("type[%d] data: %s", i, dexTypeId.string_data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
複製代碼
解析結果:
parse DexType
type[0] data: LHello;
type[1] data: Ljava/io/PrintStream;
type[2] data: Ljava/lang/Object;
type[3] data: Ljava/lang/String;
type[4] data: Ljava/lang/System;
type[5] data: V
type[6] data: [Ljava/lang/String;
複製代碼
struct DexProtoId {
u4 shortyIdx; /* index into stringIds for shorty descriptor */
u4 returnTypeIdx; /* index into typeIds list for return type */
u4 parametersOff; /* file offset to type_list for parameter types */
};
複製代碼
proto_ids
表示方法聲明信息,它包含如下三個變量:
方法參數列表的數據結構在 DexFile.h 中用 DexTypeList
來表示:
struct DexTypeList {
u4 size; /* #of entries in list */
DexTypeItem list[1]; /* entries */
};
struct DexTypeItem {
u2 typeIdx; /* index into typeIds */
};
複製代碼
size
表示方法參數的個數,參數用 DexTypeItem
表示,它只有一個屬性 typeIdx
,指向 type_ids
中對應項。具體的解析代碼以下:
private void parseDexProto() {
log("\nparse DexProto");
try {
int protoIdsSize = dex.getDexHeader().proto_ids__size;
for (int i = 0; i < protoIdsSize; i++) {
int shorty_idx = reader.readInt();
int return_type_idx = reader.readInt();
int parameters_off = reader.readInt();
DexProtoId dexProtoId = new DexProtoId(shorty_idx, return_type_idx, parameters_off);
log("proto[%d]: %s %s %d", i, dexStringIds.get(shorty_idx).string_data,
dexTypeIds.get(return_type_idx).string_data, parameters_off);
if (parameters_off > 0) {
parseDexProtoParameters(parameters_off);
}
dexProtos.add(dexProtoId);
}
} catch (IOException e) {
e.printStackTrace();
}
}
複製代碼
解析結果:
parse DexProto
proto[0]: V V 0
proto[1]: VL V 412
parameters[0]: Ljava/lang/String;
proto[2]: VL V 420
parameters[0]: [Ljava/lang/String;
複製代碼
struct DexFieldId {
u2 classIdx; /* index into typeIds list for defining class */
u2 typeIdx; /* index into typeIds for field type */
u4 nameIdx; /* index into stringIds for field name */
};
複製代碼
field_ids
表示的是字段信息,指明瞭字段所在的類,字段的類型以及字段名稱,在 DexFile.h
中定義爲 DexFieldId
, 其各個字段含義以下:
代碼解析很簡單,就不貼出來了,直接看一下解析結果:
parse DexField
field[0]: LHello;->HELLO_WORLD;Ljava/lang/String;
field[1]: Ljava/lang/System;->out;Ljava/io/PrintStream;
複製代碼
struct DexMethodId {
u2 classIdx; /* index into typeIds list for defining class */
u2 protoIdx; /* index into protoIds for method prototype */
u4 nameIdx; /* index into stringIds for method name */
};
複製代碼
method_ids
指明瞭方法所在的類、方法聲明以及方法名。在 DexFile.h 中用 DexMethodId
表示該項,其屬性含義以下:
解析結果:
parse DexMethod
method[0]: LHello; proto[0] <clinit>
method[1]: LHello; proto[0] <init>
method[2]: LHello; proto[2] main
method[3]: Ljava/io/PrintStream; proto[1] println
method[4]: Ljava/lang/Object; proto[0] <init>
複製代碼
struct DexClassDef {
u4 classIdx; /* index into typeIds for this class */
u4 accessFlags;
u4 superclassIdx; /* index into typeIds for superclass */
u4 interfacesOff; /* file offset to DexTypeList */
u4 sourceFileIdx; /* index into stringIds for source file name */
u4 annotationsOff; /* file offset to annotations_directory_item */
u4 classDataOff; /* file offset to class_data_item */
u4 staticValuesOff; /* file offset to DexEncodedArray */
};
複製代碼
class_def
是 DEX 文件結構中最複雜也是最核心的部分,它表示了類的全部信息,對應 DexFile.h
中的 DexClassDef
:
重點是 classDataOff
這個字段,它包含了一個類的核心數據,在 Android 源碼中定義爲 DexClassData
,它不在 DexFile.h 中了,而是在 DexClass.h 中:
struct DexClassData {
DexClassDataHeader header;
DexField* staticFields;
DexField* instanceFields;
DexMethod* directMethods;
DexMethod* virtualMethods;
};
複製代碼
DexClassDataHeader
定義了類中字段和方法的數目,它也定義在 DexClass.h 中:
struct DexClassDataHeader {
u4 staticFieldsSize;
u4 instanceFieldsSize;
u4 directMethodsSize;
u4 virtualMethodsSize;
};
複製代碼
在讀取的時候要注意這裏的數據是 LEB128 類型。它是一種可變長度類型,每一個 LEB128 由 1~5 個字節組成,每一個字節只有 7 個有效位。若是第一個字節的最高位爲 1,表示須要繼續使用第 2 個字節,若是第二個字節最高位爲 1,表示須要繼續使用第三個字節,依此類推,直到最後一個字節的最高位爲 0,至多 5 個字節。除了 LEB128 之外,還有無符號類型 ULEB128。
那麼爲何要使用這種數據結構呢?咱們都知道 Java 中 int 類型都是 4 字節,32 位的,可是不少時候根本用不到 4 個字節,用這種可變長度的結構,能夠節省空間。對於運行在 Android 系統上來講,能多省一點空間確定是好的。下面給出了 Java 讀取 ULEB128 的代碼:
public static int readUnsignedLeb128(byte[] src, int offset) {
int result = 0;
int count = 0;
int cur;
do {
cur = copy(src, offset, 1)[0];
cur &= 0xff;
result |= (cur & 0x7f) << count * 7;
count++;
offset++;
DexParser.POSITION++;
} while ((cur & 0x80) == 128 && count < 5);
return result;
}
複製代碼
繼續回到 DexClassData 中來。header
部分定義了各類字段和方法的個數,後面跟着的分別就是 靜態字段 、實例字段 、直接方法 、虛方法 的具體數據了。字段用 DexField
表示,方法用 DexMethod
表示。
struct DexField {
u4 fieldIdx; /* index to a field_id_item */
u4 accessFlags;
};
複製代碼
struct DexMethod {
u4 methodIdx; /* index to a method_id_item */
u4 accessFlags;
u4 codeOff; /* file offset to a code_item */
46};
複製代碼
method_idx
是指向 method_ids 的索引,表示方法信息。accessFlags
是該方法的訪問標識符。codeOff
是結構體 DexCode
的偏移量。若是你堅持看到了這裏,是否是發現說到如今還沒說到最重要的東西,DEX 包含的代碼,或者說指令,對應的就是 Hello.java 中的 main 方法。沒錯,DexCode
就是用來存儲方法的詳細信息以及其中的指令的。
struct DexCode {
u2 registersSize; // 寄存器個數
u2 insSize; // 參數的個數
u2 outsSize; // 調用其餘方法時使用的寄存器個數
u2 triesSize; // try/catch 語句個數
u4 debugInfoOff; // debug 信息的偏移量
u4 insnsSize; // 指令集的個數
u2 insns[1]; // 指令集
/* followed by optional u2 padding */ // 2 字節,用於對齊
/* followed by try_item[triesSize] */
/* followed by uleb128 handlersSize */
/* followed by catch_handler_item[handlersSize] */
};
複製代碼
咱們打開 010 Editor,定位到 main() 方法對應的 DexCode
,對照進行分析:
public class Hello {
private static String HELLO_WORLD = "Hello World!";
public static void main(String[] args) {
System.out.println(HELLO_WORLD);
}
}
複製代碼
main()
方法對應的 DexCode 十六進制表示爲 :
03 00 01 00 02 00 00 00 79 02 00 00 08 00 00 00
62 00 01 00 62 01 00 00 6E 20 03 00 10 00 0E 00
複製代碼
使用的寄存器個數是 3 個。參數個數是 1 個,就是 main()
方法中的 String[] args
。調用外部方法時使用的寄存器個數爲 2 個。指令個數是 8 。
終於說到指令了,main() 函數中有 8 條指令,就是上面十六進制中的第二行。嘗試來解析一下這段指令。Android 官網就有 Dalvik 指令的相關介紹,連接。
第一個指令 62 00 01 00
,查詢文檔 62
對應指令爲 sget-object vAA, field@BBBB
,AA
對應 00
, 表示 v0
寄存器。BBBB
對應 01 00
,表示 field_ids
中索引爲 1 的字段,根據前面的解析結果該字段爲 Ljava/lang/System;->out;Ljava/io/PrintStream
,整理一下,62 00 01 00
表示的就是:
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
複製代碼
接着是 62 01 00 00
。仍是 sget-object vAA, field@BBBB
, AA
對應 01
,BBBB
對應 0000
, 使用的是 v1
寄存器,field 位 field_ids 中索引爲 0 的字段,即 LHello;->HELLO_WORLD;Ljava/lang/String
,該句完整指令爲:
sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;
複製代碼
接着是 6E 20 03 00
, 查看文檔 6E
指令爲 invoke-virtual {vC, vD, vE, vF, vG}, meth@BBBB
。6E
後面一個十六位 2
表示調用方法是兩個參數,那麼 BBBB
就是 03 00
,指向 method_ids 中索引爲 3 方法。根據前面的解析結果,該方法就是 Ljava/io/PrintStream;->println(Ljava/lang/String;)V
。完整指令爲:
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
複製代碼
最後的 0E
,查看文檔該指令爲 return-void
,到這 main() 方法就結束了。
將上面幾句指令放在一塊兒:
62 00 01 00 : sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
62 01 00 00 : sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;
6E 20 03 00 : invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
OE OO : return-void
複製代碼
這就是 main() 方法的完整指令了。還記得我以前的一篇文章 Smali 語法解析——Hello World,其實這個解析結果和 Hello.java 對應的 smali 代碼是一致的:
.method public static main([Ljava/lang/String;)V .registers 3 .prologue .line 6 sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
sget-object v1, LHello;->HELLO_WORLD:Ljava/lang/String;
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
.line 7
return-void
.end method
複製代碼
這種文章真的是又臭又長,可是耐下心去看,仍是會有很大收貨的。最後來一張思惟導圖總結一下:
Java 版本 DEX 文件格式解析源碼,點我 DexParser
文章首發微信公衆號:
秉心說
, 專一 Java 、 Android 原創知識分享,LeetCode 題解。更多 JDK 源碼解析,掃碼關注我吧!