dex文件結構(轉)

Dex文件和Dalvik虛擬機

在Android系統中,dex文件是可以直接在Dalvik虛擬機中加載運行的文件。通過ADT,經過複雜的編譯,可以把java源代碼轉換爲dex文 件。 那麼這個文件的格式是什麼樣的呢?爲什麼Android不直接使用class文件,而採用這個不一樣文件呢?其實它是針對嵌入式系統優化的結 果,Dalvik虛擬機的指令碼並不是標準的Java虛擬機指令碼,而是使用了自己獨有的一套指令集。如果有自己的編譯系統,可以不生成class文件, 直接生成dex文件。dex文件中共用了很多類名稱、常量字符串,使它的體積比較小,運行效率也比較高。但歸根到底,Dalvik還是基於寄存器的虛擬機 的一個實現。

文件頭(File Header)

Dex文件頭主要包括校驗和以及其他結構的偏移地址和長度信息。

字段名稱 偏移值 長度 描述
magic 0x0 8 'Magic'值,即魔數字段,格式如”dex/n035/0”,其中的035表示結構的版本。
checksum 0x8 4 校驗碼。
signature 0xC 20 SHA-1簽名。
file_size 0x20 4 Dex文件的總長度。
header_size 0x24 4 文件頭長度,009版本=0x5C,035版本=0x70。
endian_tag 0x28 4 標識字節順序的常量,根據這個常量可以判斷文件是否交換了字節順序,缺省情況下=0x78563412。
link_size 0x2C 4 連接段的大小,如果爲0就表示是靜態連接。
link_off 0x30 4 連接段的開始位置,從本文件頭開始算起。如果連接段的大小爲0,這裏也是0。
map_off 0x34 4 map數據基地址。
string_ids_size 0x38 4 字符串列表的字符串個數。
string_ids_off 0x3C 4 字符串列表表基地址。
type_ids_size 0x40 4 類型列表裏類型個數。
type_ids_off 0x44 4 類型列表基地址。
proto_ids_size 0x48 4 原型列表裏原型個數。
proto_ids_off 0x4C 4 原型列表基地址。
field_ids_size 0x50 4 字段列表裏字段個數。
field_ids_off 0x54 4 字段列表基地址。
method_ids_size 0x58 4 方法列表裏方法個數。
method_ids_off 0x5C 4 方法列表基地址。
class_defs_size 0x60 4 類定義類表中類的個數。
class_defs_off 0x64 4 類定義列表基地址。
data_size 0x68 4 數據段的大小,必須以4字節對齊。
data_off 0x6C 4 數據段基地址

魔數字段

魔數字段,主要就是Dex文件的標識符,它佔用4個字節,在目前的源碼裏是 “dex\n”,它的作用主要是用來標識dex文件的,比如有一個文件也以dex爲後綴名,僅此並不會被認爲是Davlik虛擬機運行的文件,還要判斷這 四個字節。另外Davlik虛擬機也有優化的Dex,也是通過個字段來區分的,當它是優化的Dex文件時,它的值就變成”dey\n”了。根據這四個字 節,就可以識別不同類型的Dex文件了。

跟在“dex\n”後面的是版本字段,主要用來標識Dex文件的版本。目前支持的版本號爲“035\0”,不管是否優化的版本,都是使用這個版本號。

檢驗碼字段

主要用來檢查從這個字段開始到文件結尾,這段數據是否完整,有沒有人修改過,或者傳送過程中是否有出錯等等。通常用來檢查數據是否完整的算法,有 CRC32、有SHA128等,但這裏採用並不是這兩類,而採用一個比較特別的算法,叫做adler32,這是在開源zlib裏常用的算法,用來檢查文件 是否完整性。該算法由MarkAdler發明,其可靠程度跟CRC32差不多,不過還是弱一點點,但它有一個很好的優點,就是使用軟件來計算檢驗碼時比較 CRC32要快很多。可見Android系統,就算法上就已經爲移動設備進行優化了。

Adler32算法的C源碼如下(Java中可使用java.util.zip.Adler32類做校驗操作):

#define ZLIB_INTERNAL
#include "zlib.h"
#define BASE 65521UL /* largest prime smaller than 65536 */
#define NMAX 5552 
/*NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <=2^32-1 */
 
#define DO1(buf,i){adler += (buf)[i]; sum2 += adler;}
#define DO2(buf,i) DO1(buf,i); DO1(buf,i+1);
#define DO4(buf,i) DO2(buf,i); DO2(buf,i+2);
#define DO8(buf,i) DO4(buf,i); DO4(buf,i+4);
#define DO16(buf) DO8(buf,0); DO8(buf,8);
 
/*use NO_DIVIDE if your processor does not do division in hardware */
#ifdef NO_DIVIDE
#define MOD(a) \ 
do{ \ 
if(a >= (BASE << 16)) a -= (BASE << 16); \ 
if(a >= (BASE << 15)) a -= (BASE << 15); \ 
if(a >= (BASE << 14)) a -= (BASE << 14); \ 
if(a >= (BASE << 13)) a -= (BASE << 13); \ 
if(a >= (BASE << 12)) a -= (BASE << 12); \ 
if(a >= (BASE << 11)) a -= (BASE << 11); \ 
if(a >= (BASE << 10)) a -= (BASE << 10); \ 
if(a >= (BASE << 9)) a -= (BASE << 9); \ 
if(a >= (BASE << 8)) a -= (BASE << 8); \ 
if(a >= (BASE << 7)) a -= (BASE << 7); \ 
if(a >= (BASE << 6)) a -= (BASE << 6); \ 
if(a >= (BASE << 5)) a -= (BASE << 5); \ 
if(a >= (BASE << 4)) a -= (BASE << 4); \ 
if(a >= (BASE << 3)) a -= (BASE << 3); \ 
if(a >= (BASE << 2)) a -= (BASE << 2); \ 
if(a >= (BASE << 1)) a -= (BASE << 1); \ 
if(a >= BASE) a -= BASE; \ 
}while (0)
# define MOD4(a) \ 
do{ \ 
if(a >= (BASE << 4)) a -= (BASE << 4); \ 
if(a >= (BASE << 3)) a -= (BASE << 3); \ 
if(a >= (BASE << 2)) a -= (BASE << 2); \ 
if(a >= (BASE << 1)) a -= (BASE << 1); \ 
if(a >= BASE) a -= BASE; \ 
}while (0)
#else
#define MOD(a) a %= BASE
#define MOD4(a) a %= BASE
#endif
 
/*=========================================================================*/
uLong ZEXPORT adler32(adler, buf, len)
    uLong adler;
    const Bytef *buf;
    uInt len;
{
    unsigned long sum2;
    unsigned n;
 
    /*split Adler-32 into component sums */
    sum2= (adler >> 16) & 0xffff;
    adler&= 0xffff;
 
    /*in case user likes doing a byte at a time, keep it fast */
    if(len == 1) {
        adler+= buf[0];
        if(adler >= BASE)adler-= BASE;
        sum2+= adler;
        if(sum2 >= BASE)sum2-= BASE;
        return adler|(sum2 << 16);
    }
 
    /*initial Adler-32 value (deferred check for len == 1 speed) */
    if(buf == Z_NULL)return 1L;
 
    /*in case short lengths are provided, keep it somewhat fast */
    if(len < 16) {
        while(len--) {
            adler+= *buf++;
            sum2+= adler;
        }
        if(adler >= BASE)
            adler-= BASE;
        MOD4(sum2); /* only added so many BASE's */
        return adler|(sum2 << 16);
    }
 
    /*do length NMAX blocks -- requires just one modulo operation */
    while(len >= NMAX) {
        len-= NMAX;
        n= NMAX/16; /* NMAX is divisible by 16 */
        do{
            DO16(buf); /* 16 sums unrolled */
            buf+= 16;
        }while (--n);
        MOD(adler);
        MOD(sum2);
    }
 
    /*do remaining bytes (less than NMAX, still just one modulo) */
    if(len) { 
        /* avoid modulos if none remaining */
        while(len >= 16) {
            len-= 16;
            DO16(buf);
            buf+= 16;
        }
        while(len--) {
            adler+= *buf++;
            sum2+= adler;
        }
        MOD(adler);
        MOD(sum2);
    }
 
    /*return recombined sums */
    return adler|(sum2 << 16);
}

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的數據結構如下:

名稱 大小 說明
size 4字節 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>\0”

另外這段數據中不僅包括字符串的字符串的內容和結束標誌,在最開頭的位置還標明瞭字符串的長度。上例中第一個字節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個字節,但不包括結束字符“\0”。

2、關於長度的意義

由於字符串內容採用的是UTF-8格式編碼,表示一個字符的字節數是不定的。即有時是一個字節表示一個字符,有時是兩個、三個甚至四個字節表示一個字符。 而這裏的長度代表的並不是整個字符串所佔用的字節數,表示這個字符串包含的字符個數。所以在讀取時需要注意,尤其是在包含中文字符時,往往會因爲讀取的長 度不正確導致字符串被截斷。

關於計算UTF-8字符串的長度,可參見字符編碼知識-UTF8編碼規則

from:www.cnblogs.com/santry/archive/2011/10/24/2222900.html

發佈了106 篇原創文章 · 獲贊 2 · 訪問量 3669
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章