綁定帳號登入

Android 台灣中文網

打印 上一主題 下一主題

[教程] Dex檔案結構

[複製連結] 查看: 1682|回覆: 1|好評: 0
跳轉到指定樓層
樓主
fam1001 | 收聽TA | 只看該作者 回帖獎勵 |倒序瀏覽 |閱讀模式
發表於 2016-3-24 10:09

馬上加入Android 台灣中文網,立即免費下載應用遊戲。

您需要 登錄 才可以下載或查看,沒有帳號?註冊

x
檔案頭(File Header)
Dex檔案頭主要包括校驗和以及其他結構的偏移地址和長度訊息。
字段名稱偏移值長度描述
magic0x08"Magic"值,即魔數字段,格式如」dex/n035/0」,其中的035表示結構的版本。
checksum0X84校驗碼。
signature0xC20SHA-1簽名。
file_size0x204Dex檔案的總長度。
header_size0x244檔案頭長度,009版本=0x5C,035版本=0x70。
endian_tag0x284標識位元組順序的常量,根據這個常量可以判斷檔案是否交換了位元組順序,缺省情況下=0x78563412。
link_size0x2C4連接段的大小,如果為0就表示是靜態連接。
link_off0x304連接段的開始位置,從本檔案頭開始算起。如果連接段的大小為0,這裡也是0。
map_off0x344map資料基地址。
string_ids_size0x384字符串列表的字符串個數。
string_ids_off0x3C4字符串列表表基地址。
type_ids_size0x404類型列表裡類型個數。
type_ids_off0x444類型列表基地址。
proto_ids_size0x484原型列表裡原型個數。
proto_ids_off0x4C4原型列表基地址。
field_ids_size0x504字段列表裡字段個數。
field_ids_off0x544字段列表基地址。
method_ids_size0x584方法列表裡方法個數。
method_ids_off0x5C4方法列表基地址。
class_defs_size0x604類定義類表中類的個數。
class_defs_off0x644類定義列表基地址。
data_size0x684資料段的大小,必須以4位元組對齊。
data_off0x6C4資料段基地址


魔數字段
     魔數字段,主要就是Dex檔案的標識符,它佔用4個位元組,在目前的源碼裡是 「dex
」,它的作用主要是用來標識dex檔案的,比如有一個檔案也以dex為後綴名,僅此並不會被認為是Davlik虛擬機執行的檔案,還要判斷這 四個位元組。另外Davlik虛擬機也有優化的Dex,也是通過個字段來區分的,當它是優化的Dex檔案時,它的值就變成」dey
」了。根據這四個字 節,就可以識別不同類型的Dex檔案了。
      跟在「dex
」後面的是版本字段,主要用來標識Dex檔案的版本。目前支援的版本號為「035」,不管是否優化的版本,都是使用這個版本號。

檢驗碼字段
     主要用來檢查從這個字段開始到檔案結尾,這段資料是否完整,有沒有人修改過,或者傳送過程中是否有出錯等等。通常用來檢查資料是否完整的算法,有 CRC32、有SHA128等,但這裡採用並不是這兩類,而採用一個比較特別的算法,叫做adler32,這是在開源zlib裡常用的算法,用來檢查檔案 是否完整性。該算法由MarkAdler發明,其可靠程度跟CRC32差不多,不過還是弱一點點,但它有一個很好的優點,就是使用軟體來計算檢驗碼時比較 CRC32要快很多。可見Android系統,就算法上就已經為移動設備進行優化了。
     Java中可使用java.util.zip.Adler32類做校驗操作
SHA-1簽名字段
     dex檔案頭裡,前面已經有了面有一個4位元組的檢驗字段碼了,為什麼還會有SHA-1簽名字段呢?不是重複了嗎?可是仔細考慮一下,這樣設計自有道理。因 為dex檔案一般都不是很小,簡單的應用程式都有幾十K,這麼多資料使用一個4位元組的檢驗碼,重複的機率還是有的,也就是說當檔案裡的資料修改了,還是很 有可能檢驗不出來的。這時檢驗碼就失去了作用,需要使用更加強大的檢驗碼,這就是SHA-1。SHA-1校驗碼有20個位元組,比前面的檢驗碼多了16個字 節,幾乎不會不同的檔案計算出來的檢驗是一樣的。設計兩個檢驗碼的目的,就是先使用第一個檢驗碼進行快速檢查,這樣可以先把簡單出錯的dex檔案丟掉了, 接著再使用第二個複雜的檢驗碼進行複雜計算,驗證檔案是否完整,這樣確保執行的檔案完整和安全。
      SHA(Secure Hash Algorithm, 安全散列算法)是美國國家安全局設計,美國國家標準與技術研究院發佈的一系列密碼散列函數。SHA-1看起來和MD5算法很像,也許是Ron Rivest在SHA-1的設計中起了一定的作用。SHA-1的內部比MD5更強,其摘要比MD5的16位元組長4個位元組,這個算法成功經受了密碼分析專家 的攻擊,也因而受到密碼學界的廣泛推崇。這個算法在目前網路上的簽名,BT軟體裡就有大量使用,比如在BT裡要計算是否同一個種子時,就是利用檔案的簽名 來判斷的。同一份8G的電影從幾千BT用戶那裡下載,也不會出現錯誤的資料,導致電影不播放。

map_off字段
這個字段主要保存map開始位置,就是從檔案頭開始到map資料的長度,通過這個索引就可以找到map資料。map的資料結構如下:
名稱大小說明
size4位元組map裡項的個數
list變長每一項定義為12位元組,項的個數由上面項大小決定。


map資料排列結構定義如下:
/*
*Direct-mapped "map_list".
*/

typedef struct DexMapList {
    u4 size; /* #of entries inlist */
    DexMapItem list[1]; /* entries */
}DexMapList;
每一個map項的結構定義如下:
/*
*Direct-mapped "map_item".
*/

typedef struct DexMapItem {
    u2 type; /* type code (seekDexType* above) */
    u2 unused;
    u4 size; /* count of items ofthe indicated type */
    u4 offset; /* file offset tothe start of data */
}DexMapItem;
DexMapItem結構定義每一項的資料意義:類型、類型個數、類型開始位置。
其中的類型定義如下:

/*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,
};
從上面的類型可知,它包括了在dex檔案裡可能出現的所有類型。可以看出這裡的類型與檔案頭裡定義的類型有很多是一樣的,這裡的類型其實就是檔案頭裡定義 的類型。其實這個map的資料,就是頭裡類型的重複,完全是為了檢驗作用而存在的。當Android系統載入dex檔案時,如果比較檔案頭類型個數與 map裡類型不一致時,就會停止使用這個dex檔案

string_ids_size/off字段

這兩個字段主要用來標識字符串資源。源程式編譯後,程式裡用到的字符串都保存在這個資料段裡,以便解釋執行這個dex檔案使用。其中包括調用庫函數里的類名稱描述,用於輸出顯示的字符串等。
string_ids_size標識了有多少個字符串,string_ids_off標識字符串資料區的開始位置。字符串的存儲結構如下:
/*
* Direct-mapped "string_id_item".
*/
typedef struct DexStringId {
    u4  stringDataOff;      /* file offset to string_data_item */
} DexStringId;
可以看出這個資料區保存的只是字符串表的地址索引。如果要找到字符串的實際資料,還需要通過個地址索引找到檔案的相應開始位置,然後才能得到字符串資料。 每一個字符串項的索引佔用4個位元組,因此這個資料區的大小就為4*string_ids_size。實際資料區中的字符串採用UTF8格式保存。
例如,如果dex檔案使用16進制顯示出來內容如下:
063c 696e 6974 3e00
其實際資料則是」<init>」
另外這段資料中不僅包括字符串的字符串的內容和結束標誌,在最開頭的位置還標明了字符串的長度。上例中第一個位元組06就是表示這個字符串有6個字符。
關於字符串的長度有兩點需要注意的地方:
1、關於長度的編碼格式
dex檔案裡採用了變長方式表示字符串長度。一個字符串的長度可能是一個位元組(小於256)或者4個位元組(1G大小以上)。字符串的長度大多數都是小於 256個位元組,因此需要使用一種編碼,既可以表示一個位元組的長度,也可以表示4個位元組的長度,並且1個位元組的長度佔絕大多數。能滿足這種表示的編碼方式有 很多,但dex檔案裡採用的是uleb128方式。leb128編碼是一種變長編碼,每個位元組採用7位來表達原來的資料,最高位用來表示是否有後繼位元組。
它的編碼算法如下:

/*
* Writes a 32-bit value in unsigned ULEB128 format.
* Returns the updated pointer.
*/
DEX_INLINE u1* writeUnsignedLeb128(u1* ptr, u4 data)
{
    while (true) {
        u1 out = data & 0x7f;
        if (out != data) {
            *ptr++ = out | 0x80;
            data >>= 7;
        } else {
            *ptr++ = out;
            break;
        }
    }
    return ptr;
}
它的解碼算法如下:

/*
* Reads an unsigned LEB128 value, updating the given pointer to point
* just past the end of the read value. This function tolerates
* non-zero high-order bits in the fifth encoded byte.
*/
DEX_INLINE int readUnsignedLeb128(const u1** pStream) {
    const u1* ptr = *pStream;
    int result = *(ptr++);
   if (result > 0x7f) {
        int cur = *(ptr++);
        result = (result & 0x7f) | ((cur & 0x7f) << 7);
        if (cur > 0x7f) {
            cur = *(ptr++);
            result |= (cur & 0x7f) << 14;
            if (cur > 0x7f) {
                cur = *(ptr++);
                result |= (cur & 0x7f) << 21;
                if (cur > 0x7f) {
                    /*
                     * Note: We don"t check to see if cur is out of
                     * range here, meaning we tolerate garbage in the
                     * high four-order bits.
                     */
                    cur = *(ptr++);
                    result |= cur << 28;
                }
            }
        }
    }
    *pStream = ptr;
    return result;
}
根據上面的算法分析上面例子字符串,取得第一個位元組是06,最高位為0,因此沒有後繼位元組,那麼取出這個位元組裡7位有效資料,就是6,也就是說這個字符串是6個位元組,但不包括結束字符「」。
2、關於長度的意義
由於字符串內容採用的是UTF-8格式編碼,表示一個字符的位元組數是不定的。即有時是一個位元組表示一個字符,有時是兩個、三個甚至四個位元組表示一個字符。 而這裡的長度代表的並不是整個字符串所佔用的位元組數,表示這個字符串包含的字符個數。所以在讀取時需要注意,尤其是在包含中文字符時,往往會因為讀取的長 度不正確導致字符串被截斷。
「用Android 就來APK.TW」,快來加入粉絲吧!
Android 台灣中文網(APK.TW)

評分

參與人數 2碎鑽 +1 幫助 +2 收起 理由
ranth + 1 + 1 非常讃
球-球 + 1 好內容,老衲來為這篇文章開開光.

查看全部評分

收藏收藏2 分享分享 分享專題
用Android 就來Android 台灣中文網(https://apk.tw)
回覆

使用道具 舉報

沙發
monitorstudio | 收聽TA | 只看該作者
發表於 2016-4-13 05:00

評分

參與人數 1幫助 +1 收起 理由
ranth + 1 姐很想給你一個吻,但不現實,還是給分吧.

查看全部評分

用Android 就來Android 台灣中文網(https://apk.tw)
回覆 支持 反對

使用道具 舉報

您需要登錄後才可以回帖 登錄 | 註冊

本版積分規則