Dex檔案是Android虛拟機下的可執行檔案,包含了應用程式所用到所有操作指令和運作時資料。在程式編譯過程中,java源檔案先被編譯成class檔案,然後通過dx工具将多個class檔案整合為一個dex檔案。這樣的檔案結構使得各個類能夠共享資料,充分減少了存儲空間,提升了運作效率。
Java源檔案生成Dex檔案的映射關系。
dex檔案的結構如下圖:
我們可以在android源碼中的/dalvik/libdex/DexFile.h找到關于dexfile的定義。
Andre
這裡定義的DexFile是dex檔案被映射到記憶體中的結構,出了基本的dex檔案結構外,還包含了DexOptHead和尾部附加的資料,這些資料是Android系統為了結合目前平台特性對dex檔案的結構進行了優化和擴充,是運作效率更高。
/*
* Structure representing a DEX file.
*
* Code should regard DexFile as opaque, using the API calls provided here
* to access specific structures.
*/
typedef struct DexFile {
/* directly-mapped "opt" header */
const DexOptHeader* pOptHeader;
/* pointers to directly-mapped structs and arrays in base DEX */
const DexHeader* pHeader;
const DexStringId* pStringIds;
const DexTypeId* pTypeIds;
const DexFieldId* pFieldIds;
const DexMethodId* pMethodIds;
const DexProtoId* pProtoIds;
const DexClassDef* pClassDefs;
const DexLink* pLinkData;
/* mapped in "auxillary" section */
const DexClassLookup* pClassLookup;
/* points to start of DEX file data */
const u1* baseAddr;
/* track memory overhead for auxillary structures */
int overhead;
/* additional app-specific data structures associated with the DEX */
void* auxData;
} DexFile;
名稱 | 格式 | 描述 |
---|---|---|
header | header_item | 檔案頭。 |
string_ids | string_id_item[] | 字元串索引表,記錄了各個字元所在的偏移值,使用UTF-16編碼。 |
type_ids | type_id_item[] | 類型資料索引,記錄了各個類型的字元串索引。 |
proto_id | proto_id_item[] | 函數原型資料索引,記錄了方法聲明的字元串,傳回類型和參數清單。 |
field_ids | field_id_item[] | 字段資料索引,記錄了所屬類,聲明類型和方法名等資訊。 |
method_ids | method_id_item[] | 類方法索引,記錄了方法所屬類,聲明類型以及方法名等資訊。 |
class_defs | class_def_item[] | 類定義資料,記錄了指定類的各類資訊,包括接口,超類,類資料偏移量等。 |
data | type_id_item[] | 類型資料索引,記錄了各個類型的字元串索引。 |
type_ids | ubyte[] | 資料區,儲存着各個類的資料。 |
link_data | ubyte[] | 靜态連接配接資料。 |
Header
header是dex檔案的檔案頭,簡單的記錄了dex檔案的一些基本資訊和大緻的資料分布。header的大小固定為0x70,其中每一項資訊所占用的大小也是固定的。同樣在/dalvik/libdex/DexFile.h可以看到header_item的定義:
/*
* Direct-mapped "header_item" struct.
*/
typedef struct DexHeader {
u1 magic[8]; /* includes version number */
u4 checksum; /* adler32 checksum */
u1 signature[kSHA1DigestLen]; /* SHA-1 hash */
u4 fileSize; /* length of entire file */
u4 headerSize; /* offset to start of next section */
u4 endianTag; /*位元組序标号*/
u4 linkSize;
u4 linkOff;
u4 mapOff;
u4 stringIdsSize;
u4 stringIdsOff;
u4 typeIdsSize;
u4 typeIdsOff;
u4 protoIdsSize;
u4 protoIdsOff;
u4 fieldIdsSize;
u4 fieldIdsOff;
u4 methodIdsSize;
u4 methodIdsOff;
u4 classDefsSize;
u4 classDefsOff;
u4 dataSize;
u4 dataOff;
} DexHeader;
下表是每一項的基本資訊:
名稱 | 格式 | 描述 |
---|---|---|
magic | ubyte[4] | dex\n |
version | Ubyte[4] | 035\0 或 036\0 |
checksum | uint | 使用zlib 的adler32 所計算的32-bitsCheckSum . 計算的範圍為DEX 檔案的長度(Header->fileSize) 減去8bytes Magic Code 與4bytes CheckSum 的範圍. 用來確定DEX 檔案内容沒有損毀. |
signature | ubyte[20] | SHA-1signature (hash) 用來識别原本的DEX 檔案( 被優化以前的DEX).SHA-1 計算的範圍為DEX 檔案的長度(Header->fileSize) 減去8bytes Magic Code,4 bytes CheckSum 與20bytesSHA-1 的範圍. 用來識别最原本的DEX 檔案的唯一性.( 是以被優化過後, 這個SHA-1 盡能用來識别原本的DEX 檔案, 而無法 通過ODEX 檔案計算回最原本的DEX 檔案SHA-1 值了). |
file_size | uint | 檔案的總大小。 |
header_size | uint= 0x70 | 用來記錄目前DEXHeader 的大小( 現有版本為0x70bytes), 可用來做為後續Header 如果有改版時, 最基本的頭欄位向前, 向後相容的依據. |
endian_tag | uint= ENDIAN_CONSTANT | 預設值為Little-Endian, 在這欄位會顯示32bits 值”0x12345678”.( 應該在Big-Endian 處理器上, 會轉為“ 0x78563412”) |
link_size | uint | LinkSection 的大小, 如果為0 表示該DEX 檔案不是靜态連結. |
link_off | uint | 用來表示LinkSection 距離Dex 頭的Offset. 如果LinkSize 為0, 此值也會為0. 資料格式可以參考struct DexLink. |
map_off | uint | 用來表示MapItem 距離Dex 頭的Offset. 如果為0, 表示這DEX 檔案沒有MapItem. 資料格式可以參考struct map_list. |
string_ids_size | uint | 字元串位址清單中的元素個數。 |
string_ids_off | uint | 字元串位址清單的偏移。 |
type_ids_size | uint | 用來表示TypeIDs List 中的元素個數. |
type_ids_off | uint | TypeIDs List 的檔案偏移。如果type_ids_size 為0 ,則這個值亦為0。 |
proto_ids_size | uint | 用來表示PrototypeIDs List 中的元素個數。 |
proto_ids_off | uint | Prototype IDs 的檔案偏移。如果proto_ids_size 為0 這個值亦為0. |
field_ids_size | uint | Field IDs List的檔案偏移。如果field_ids_size 為0 這個值亦為0。 |
field_ids_off | uint | 字元串位址清單的偏移。 |
method_ids_size | uint | 用來表示MethodIDs List 中的元素個數。 |
method_ids_off | uint | 用來表示Method IDs List 距離Dex 頭的Offset. 如果method_ids_size 為0 這個值亦為0. 資料格式可以參考struct DexMethodId. |
class_defs_size | uint | 用來表示ClassDefinitions List 的總數. |
class_defs_off | uint | 用來表示ClassDefinitionList 距離Dex 頭的Offset. 如果class_defs_size 為0 這個值亦為0. 資料格式可以參考struct DexClassDef. |
data_size | uint | 用來表示DataSection 的大小. 并需為sizeof(uint) 的偶數倍.( 是以就是0,8,16…etc) |
data_off | uint | 用來表示DataSection 距離Dex 頭的Offset. |
在header中有一個map_off,這是一個偏移位址,指向dex data區的maplist。
/*
* Direct-mapped "map_item".
*/
typedef struct DexMapItem {
u2 type; /* type code (see kDexType* above) */
u2 unused;
u4 size; /* count of items of the indicated type */
u4 offset; /* file offset to the start of data */
} DexMapItem;
/*
* Direct-mapped "map_list".
*/
typedef struct DexMapList {
u4 size; /* #of entries in list */
DexMapItem list[1]; /* entries */
} DexMapList;
map_list 裡先用一個 uint 描述後面有 size 個 map_item , 後續就是對應的 size 個 map_item 描述 。map_item 結構有 4 個元素 : type 表示該 map_item 的類型 ,本節能用到的描述如下 ,詳細Dalvik
Executable Format 裡 Type Code 的定義 ;size 表示再細分此 item , 該類型的個數 ;offset 是第一個元素的針對檔案初始位置的偏移量 ; unuse 是用對齊位元組的 ,無實際用處 。在DexMapItem結構中,type是一個枚舉常量。
/* map item type codes */
enum {
kDexTypeHeaderItem = 0x0000,
kDexTypeStringIdItem = 0x0001,
kDexTypeTypeIdItem = 0x0002,
kDexTypeProtoIdItem = 0x0003,
kDexTypeFieldIdItem = 0x0004,
kDexTypeMethodIdItem = 0x0005,
kDexTypeClassDefItem = 0x0006,
kDexTypeMapList = 0x1000,
kDexTypeTypeList = 0x1001,
kDexTypeAnnotationSetRefList = 0x1002,
kDexTypeAnnotationSetItem = 0x1003,
kDexTypeClassDataItem = 0x2000,
kDexTypeCodeItem = 0x2001,
kDexTypeStringDataItem = 0x2002,
kDexTypeDebugInfoItem = 0x2003,
kDexTypeAnnotationItem = 0x2004,
kDexTypeEncodedArrayItem = 0x2005,
kDexTypeAnnotationsDirectoryItem = 0x2006,
};
string_ids
這一塊區域中存儲的是dex檔案中字元串的資源的索引資訊。即目标字元串在dex檔案資料區的實體偏移位址。在DexFile.h中可以找到它的定義:
struct DexStringId {
u4 stringDataOff; /* 在dex檔案中實際偏移量 */
};
該資料結構隻有一個stringDataOff成員,當虛拟機需要讀取該字元串是,隻需要将dex檔案在記憶體中的起始位址加上stringDataOff所表示的偏移值,即可得到該字元串在記憶體中的實際實體位址。
在Dex檔案中,每個字元串都對應了一個DexStringId資料結構,該資料結構的大小為4B,是一個确定的量。并且虛拟機可以通過頭檔案中的String_ids_size知道目前Dex檔案的字元串總數,也就是string_ids區域中DexStringId的總數,是以虛拟機通過簡單的乘法即可實作對改索引資源進行正确的通路。
在data區存放的string并不是ascii編碼,而是MUTF-8編碼。這是一種修改過的UTF-8編碼,與傳統的UTF-8編碼有以下幾點差別:
- MUTF-8使用1-3位元組編碼
- 大于16位的Unicode編碼U+10000~U+10ffff使用3位元組來編碼
- U+0000采用2位元組來編碼
采用空字元作為字元串的結尾
MUTF-8字元串的頭部存放的是由uleb128編碼的字元串個數(字元串個數不包括結尾的空字元)。
type_ids
這一塊區域中存儲的是類型資源的索引資訊。定義如下:
struct DexTypeId {
u4 descriptorIdx; /* 指向字元串索引表 */
};
在dex中,類型是是以字元串的形式儲存在資料區的,是以DexTypeId資料結構中的descriptorIdx儲存的是目标類型在字元串索引表裡的序列号。虛拟機通過這個序列号從字元串索引表中找出對應類型的字元串。
proto_ids
這一區域中存儲的内容是方法原型資源的索引資訊,資料結構DexProtoId負責規格化這些資訊。
struct DexProtoId {
u4 shortyIdx; /* 方法聲明字元串,指向字元串索引表 */
u4 returnTypeIdx; /* 方法傳回類型,指向字元串索引表 */
u4 parametersOff; /* 指向一個DexTypeList的資料結構,DexTypeList表示參數清單*/
};
這個資料結構的前兩個成員同樣指向字元串索引表,而parametersOff指向的是一個DexTypeList的資料結構,這個結構體的定義如下:
/*
* Direct-mapped "type_list".
*/
typedef struct DexTypeList {
u4 size; /* 表示DexTypeItem資料結構的個數 */
DexTypeItem list[1]; /* DexTypeItem清單*/
} DexTypeList;
DexTypeList表示的是一個方法的參數清單,size是參數個數,list這個數組是一個DexTypeItem的數組,這個DexTypeItem就是表示參數類型的資料結構。
/*
* Direct-mapped "type_item".
*/
typedef struct DexTypeItem {
u2 typeIdx; /* 表示參數類型,指向type_ids索引表 */
} DexTypeItem;
typeIdx指向類型資源索引表,虛拟機通過該變亮就能獲得相應的參數類型。
field_ids
這一塊區域儲存的是代碼中的字段的索引資訊。用資料結構DexFieldId對索引資訊規格化。
struct DexFieldId {
u2 classIdx; /* 表示所屬類的類型 */
u2 typeIdx; /* 表示字段的類型 */
u4 nameIdx; /* 字段的名稱 */
};
在DexFieldId中,classIdx和typeIdx都是指向類型索引表type_ids中的一個DexTypeId表項。而nameIdx是指向字元串索引表string_ids中的一個表項。
method_ids
method_ids資源區儲存的是Dex檔案中類方法資料的索引資訊。采用的資料結構為
struct DexMethodId {
u2 classIdx; /* 表示所屬類的類型 */
u2 protoIdx; /* 表示方法原型的類型 */
u4 nameIdx; /* 方法名稱 */
};
classIdx指向類型索引表type_ids中的一個DexTypeId,變量protoIdx記錄的該方法的原型,指向proto_ids表。nameIdx指向字元串表string_ids,表示方法名稱。
class_def
在class_def資源區中,使用資料結構DexClassDef來對資源資訊規格化。
struct DexClassDef {
u4 classIdx; /* 表示類的類型,指向類型索引表 */
u4 accessFlags; /* 通路辨別符 */
u4 superclassIdx; /* 表示超類的類型,指向類型索引表 */
u4 interfacesOff; /* 接口資訊,指向一個DexTypeList資料結構*/
u4 sourceFileIdx; /* 表示源檔案名,指向字元串索引表 */
u4 annotationsOff; /* 注解目錄結構 */
u4 classDataOff; /* 類資料,指向一個DexClassData資料結構*/
u4 staticValuesOff; /* 靜态值得偏移量 */
};
通路辨別符,表示了該類的屬性,同樣也适用于字段和方法,具體如下表:
AccessFlag比特位 | 類(Class) | 方法(Method) | 域(Field) |
---|---|---|---|
0x00001 | Public | Public | Public |
0x00002 | Private | Private | Private |
0x00004 | Protected | Protected | Protected |
0x00008 | Static | Static | Static |
0x00010 | Final | Final | Final |
0x00020 | N/A | Synchronized | N/A |
0x00040 | N/A | Bridge | Volatile |
0x00080 | N/A | VarArgs | Transient |
0x00100 | N/A | Native | N/A |
0x00200 | Interface | N/A | N/A |
0x00400 | Abstract | Abstract | N/A |
0x00800 | N/A | Strict | N/A |
0x01000 | Synthetic | Synthetic | Synthetic |
0x02000 | Annotation | N/A | N/A |
0x04000 | Enum | N/A | Public |
0x08000 | N/A | Miranda | N/A |
0x10000 | Verified | Constructor | N/A |
0x20000 | Optimized | Declared_Synchronized | N/A |
DexClassDef的成員都和前面的分析類似,不同的是classDataOff是一個dex檔案内部的偏移值,指向的是一個DexClassData資料結構執行個體,DexClassData的定義從/dalvik/libdex/DexClass.h中可以找到。
在DexClass.h中,所有的u4類型,都被轉為成uleb128類型。如下所示:
/* Read the header of a class_data_item without verification. This
* updates the given data pointer to point past the end of the read
* data. */
DEX_INLINE void dexReadClassDataHeader(const u1** pData,
DexClassDataHeader *pHeader) {
pHeader->staticFieldsSize = readUnsignedLeb128(pData);
pHeader->instanceFieldsSize = readUnsignedLeb128(pData);
pHeader->directMethodsSize = readUnsignedLeb128(pData);
pHeader->virtualMethodsSize = readUnsignedLeb128(pData);
}
/* Read an encoded_field without verification. This updates the
* given data pointer to point past the end of the read data.
*
* The lastIndex value should be set to 0 before the first field in
* a list is read. It is updated as fields are read and used in the
* decode process.
*/
DEX_INLINE void dexReadClassDataField(const u1** pData, DexField* pField,
u4* lastIndex) {
u4 index = *lastIndex + readUnsignedLeb128(pData);
pField->accessFlags = readUnsignedLeb128(pData);
pField->fieldIdx = index;
*lastIndex = index;
}
/* Read an encoded_method without verification. This updates the
* given data pointer to point past the end of the read data.
*
* The lastIndex value should be set to 0 before the first method in
* a list is read. It is updated as fields are read and used in the
* decode process.
*/
DEX_INLINE void dexReadClassDataMethod(const u1** pData, DexMethod* pMethod,
u4* lastIndex) {
u4 index = *lastIndex + readUnsignedLeb128(pData);
pMethod->accessFlags = readUnsignedLeb128(pData);
pMethod->codeOff = readUnsignedLeb128(pData);
pMethod->methodIdx = index;
*lastIndex = index;
}
每個uleb128類型是leb128的無符号類型,每個leb128類型的資料包含1-5個位元組,表示一個32bit的數值。每個位元組隻有7位有效,最高一位用來表示是否需要使用到下一個位元組,比如如果第一個位元組最高位為1,表示還需要使用到第2個位元組,如果第二個位元組的最高位為1,表示會使用到第3個位元組,以此類推,最多5個位元組。對于一個2個位元組的leb128類型資料,其結構如圖所示。
/* expanded form of class_data_item. Note: If a particular item is
* absent (e.g., no static fields), then the corresponding pointer
* is set to NULL. */
typedef struct DexClassData {
DexClassDataHeader header; /*類資料頭*/
DexField* staticFields; /*指向目标的靜态字段*/
DexField* instanceFields; /*指向目标類的執行個體字段*/
DexMethod* directMethods; /*指向目标類的直接方法*/
DexMethod* virtualMethods; /*指向目标類的虛方法*/
} DexClassData;
首先看這個DexClassData的header成員,這個成員是一個DexClassDataHeader資料結構,記錄了目标類中各個部分資料的個數,主要包括靜态字段,執行個體字段,直接方法和虛方法。
/* expanded form of a class_data_item header */
typedef struct DexClassDataHeader {
u4 staticFieldsSize;
u4 instanceFieldsSize;
u4 directMethodsSize;
u4 virtualMethodsSize;
} DexClassDataHeader;
再看DexField,它的定義如下。
struct DexField {
u4 fieldIdx; /* 指向字段索引表中的一個表項,即一個DexFieldId的資料結構 */
u4 accessFlags; /*通路表示符*/
};
DexMethod描述了類的方法。先看定義。
struct DexMethod {
u4 methodIdx; /* 指向方法索引表中的一個表項,即一個DexMethodId資料結構 */
u4 accessFlags; /*通路辨別符*/
u4 codeOff; /* 指向一個DexCode的資料結構 */
};
注意codeOff這個成員,它指向的是一個DexCode的資料結構,而DexCode中記錄的正是Dex檔案中目标類方法的位元組碼。
struct DexCode {
u2 registersSize; /*寄存器個數*/
u2 insSize; /*輸入參數個數*/
u2 outsSize; /*外部方法使用寄存器數*/
u2 triesSize; /*tries個數*/
u4 debugInfoOff; /* 調試資訊位址 */
u4 insnsSize; /*方法指令個數 */
u2 insns[1]; /*真實指令數組*/
/* followed by optional u2 padding */
/* followed by try_item[triesSize] */
/* followed by uleb128 handlersSize */
/* followed by catch_handler_item[handlersSize] */
};
Dex檔案通過DexCode來管理類方法,成員insns數組儲存的就是方法的真實指令的二進制資料,關于對這些資料的解析,可以參考 https://source.android.com/devices/tech/dalvik/dalvik-bytecode.html。
寫Android dex檔案解析的部落格的比較多,感覺這篇的作者比較用心,寫的很好,轉載學習了:http://zke1ev3n.me/2015/12/02/Android-Dex%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F%E8%A7%A3%E6%9E%90/。