天天看點

Android Dex檔案格式解析

Dex檔案是Android虛拟機下的可執行檔案,包含了應用程式所用到所有操作指令和運作時資料。在程式編譯過程中,java源檔案先被編譯成class檔案,然後通過dx工具将多個class檔案整合為一個dex檔案。這樣的檔案結構使得各個類能夠共享資料,充分減少了存儲空間,提升了運作效率。

Java源檔案生成Dex檔案的映射關系。

Android Dex檔案格式解析

dex檔案的結構如下圖:

Android 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.
Android Dex檔案格式解析

在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類型資料,其結構如圖所示。
Android Dex檔案格式解析
/* 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/。

繼續閱讀