文件頭(File Header)
Dex文件頭主要包括校驗和以及其他結(jié)構(gòu)的偏移地址和長度信息。
字段名稱偏移值長度描述
magic0x08'Magic'值,即魔數(shù)字段,格式如”dex/n035/0”,其中的035表示結(jié)構(gòu)的版本。
checksum0x84校驗碼。
signature0xC20SHA-1簽名。
file_size0x204Dex文件的總長度。
header_size0x244文件頭長度,009版本=0x5C,035版本=0x70。
endian_tag0x284標識字節(jié)順序的常量,根據(jù)這個常量可以判斷文件是否交換了字節(jié)順序,缺省情況下=0x78563412。
link_size0x2C4連接段的大小,如果為0就表示是靜態(tài)連接。
link_off0x304連接段的開始位置,從本文件頭開始算起。如果連接段的大小為0,這里也是0。
map_off0x344map數(shù)據(jù)基地址。
string_ids_size0x384字符串列表的字符串個數(shù)。
string_ids_off0x3C4字符串列表表基地址。
type_ids_size0x404類型列表里類型個數(shù)。
type_ids_off0x444類型列表基地址。
proto_ids_size0x484原型列表里原型個數(shù)。
proto_ids_off0x4C4原型列表基地址。
field_ids_size0x504字段列表里字段個數(shù)。
field_ids_off0x544字段列表基地址。
method_ids_size0x584方法列表里方法個數(shù)。
method_ids_off0x5C4方法列表基地址。
class_defs_size0x604類定義類表中類的個數(shù)。
class_defs_off0x644類定義列表基地址。
data_size0x684數(shù)據(jù)段的大小,必須以4字節(jié)對齊。
data_off0x6C4數(shù)據(jù)段基地址
魔數(shù)字段,主要就是Dex文件的標識符,它占用4個字節(jié),在目前的源碼里是 “dex\n”,它的作用主要是用來標識dex文件的,比如有一個文件也以dex為后綴名,僅此并不會被認為是Davlik虛擬機運行的文件,還要判斷這 四個字節(jié)。另外Davlik虛擬機也有優(yōu)化的Dex,也是通過個字段來區(qū)分的,當(dāng)它是優(yōu)化的Dex文件時,它的值就變成”dey\n”了。根據(jù)這四個字 節(jié),就可以識別不同類型的Dex文件了。
跟在“dex\n”后面的是版本字段,主要用來標識Dex文件的版本。目前支持的版本號為“035\0”,不管是否優(yōu)化的版本,都是使用這個版本號。
檢驗碼字段
主要用來檢查從這個字段開始到文件結(jié)尾,這段數(shù)據(jù)是否完整,有沒有人修改過,或者傳送過程中是否有出錯等等。通常用來檢查數(shù)據(jù)是否完整的算法,有 CRC32、有SHA128等,但這里采用并不是這兩類,而采用一個比較特別的算法,叫做adler32,這是在開源zlib里常用的算法,用來檢查文件 是否完整性。該算法由MarkAdler發(fā)明,其可靠程度跟CRC32差不多,不過還是弱一點點,但它有一個很好的優(yōu)點,就是使用軟件來計算檢驗碼時比較 CRC32要快很多??梢?a target="_blank" rel="nofollow">Android系統(tǒng),就算法上就已經(jīng)為移動設(shè)備進行優(yōu)化了。
Java中可使用java.util.zip.Adler32類做校驗操作
dex文件頭里,前面已經(jīng)有了面有一個4字節(jié)的檢驗字段碼了,為什么還會有SHA-1簽名字段呢?不是重復(fù)了嗎?可是仔細考慮一下,這樣設(shè)計自有道理。因 為dex文件一般都不是很小,簡單的應(yīng)用程序都有幾十K,這么多數(shù)據(jù)使用一個4字節(jié)的檢驗碼,重復(fù)的機率還是有的,也就是說當(dāng)文件里的數(shù)據(jù)修改了,還是很 有可能檢驗不出來的。這時檢驗碼就失去了作用,需要使用更加強大的檢驗碼,這就是SHA-1。SHA-1校驗碼有20個字節(jié),比前面的檢驗碼多了16個字 節(jié),幾乎不會不同的文件計算出來的檢驗是一樣的。設(shè)計兩個檢驗碼的目的,就是先使用第一個檢驗碼進行快速檢查,這樣可以先把簡單出錯的dex文件丟掉了, 接著再使用第二個復(fù)雜的檢驗碼進行復(fù)雜計算,驗證文件是否完整,這樣確保執(zhí)行的文件完整和安全。
SHA(Secure Hash Algorithm, 安全散列算法)是美國國家安全局設(shè)計,美國國家標準與技術(shù)研究院發(fā)布的一系列密碼散列函數(shù)。SHA-1看起來和MD5算法很像,也許是Ron Rivest在SHA-1的設(shè)計中起了一定的作用。SHA-1的內(nèi)部比MD5更強,其摘要比MD5的16字節(jié)長4個字節(jié),這個算法成功經(jīng)受了密碼分析專家 的攻擊,也因而受到密碼學(xué)界的廣泛推崇。這個算法在目前網(wǎng)絡(luò)上的簽名,BT軟件里就有大量使用,比如在BT里要計算是否同一個種子時,就是利用文件的簽名 來判斷的。同一份8G的電影從幾千BT用戶那里下載,也不會出現(xiàn)錯誤的數(shù)據(jù),導(dǎo)致電影不播放。
這個字段主要保存map開始位置,就是從文件頭開始到map數(shù)據(jù)的長度,通過這個索引就可以找到map數(shù)據(jù)。map的數(shù)據(jù)結(jié)構(gòu)如下:
名稱大小說明
size4字節(jié)map里項的個數(shù)
list變長每一項定義為12字節(jié),項的個數(shù)由上面項大小決定。
map數(shù)據(jù)排列結(jié)構(gòu)定義如下:
/*
*Direct-mapped "map_list".
*/typedefstructDexMapList{u4 size;/* #of entries inlist */DexMapItem list[1];/* entries */}DexMapList;
每一個map項的結(jié)構(gòu)定義如下:
/*
*Direct-mapped "map_item".
*/typedefstructDexMapItem{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結(jié)構(gòu)定義每一項的數(shù)據(jù)意義:類型、類型個數(shù)、類型開始位置。
其中的類型定義如下:
/*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文件里可能出現(xiàn)的所有類型??梢钥闯鲞@里的類型與文件頭里定義的類型有很多是一樣的,這里的類型其實就是文件頭里定義 的類型。其實這個map的數(shù)據(jù),就是頭里類型的重復(fù),完全是為了檢驗作用而存在的。當(dāng)Android系統(tǒng)加載dex文件時,如果比較文件頭類型個數(shù)與 map里類型不一致時,就會停止使用這個dex文件
這兩個字段主要用來標識字符串資源。源程序編譯后,程序里用到的字符串都保存在這個數(shù)據(jù)段里,以便解釋執(zhí)行這個dex文件使用。其中包括調(diào)用庫函數(shù)里的類名稱描述,用于輸出顯示的字符串等。
string_ids_size標識了有多少個字符串,string_ids_off標識字符串?dāng)?shù)據(jù)區(qū)的開始位置。字符串的存儲結(jié)構(gòu)如下:
/*
* Direct-mapped "string_id_item".
*/typedefstructDexStringId{u4? stringDataOff;/* file offset to string_data_item */}DexStringId;
可以看出這個數(shù)據(jù)區(qū)保存的只是字符串表的地址索引。如果要找到字符串的實際數(shù)據(jù),還需要通過個地址索引找到文件的相應(yīng)開始位置,然后才能得到字符串?dāng)?shù)據(jù)。 每一個字符串項的索引占用4個字節(jié),因此這個數(shù)據(jù)區(qū)的大小就為4*string_ids_size。實際數(shù)據(jù)區(qū)中的字符串采用UTF8格式保存。
例如,如果dex文件使用16進制顯示出來內(nèi)容如下:
063c 696e 6974 3e00
其實際數(shù)據(jù)則是”\0”
另外這段數(shù)據(jù)中不僅包括字符串的字符串的內(nèi)容和結(jié)束標志,在最開頭的位置還標明了字符串的長度。上例中第一個字節(jié)06就是表示這個字符串有6個字符。
關(guān)于字符串的長度有兩點需要注意的地方:
1、關(guān)于長度的編碼格式
dex文件里采用了變長方式表示字符串長度。一個字符串的長度可能是一個字節(jié)(小于256)或者4個字節(jié)(1G大小以上)。字符串的長度大多數(shù)都是小于 256個字節(jié),因此需要使用一種編碼,既可以表示一個字節(jié)的長度,也可以表示4個字節(jié)的長度,并且1個字節(jié)的長度占絕大多數(shù)。能滿足這種表示的編碼方式有 很多,但dex文件里采用的是uleb128方式。leb128編碼是一種變長編碼,每個字節(jié)采用7位來表達原來的數(shù)據(jù),最高位用來表示是否有后繼字節(jié)。
它的編碼算法如下:
/*
* 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;}}returnptr;}
它的解碼算法如下:
/*
* 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_INLINEintreadUnsignedLeb128(constu1**pStream){constu1*ptr=*pStream;intresult=*(ptr++);if(result>0x7f){intcur=*(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;returnresult;}
根據(jù)上面的算法分析上面例子字符串,取得第一個字節(jié)是06,最高位為0,因此沒有后繼字節(jié),那么取出這個字節(jié)里7位有效數(shù)據(jù),就是6,也就是說這個字符串是6個字節(jié),但不包括結(jié)束字符“\0”。
2、關(guān)于長度的意義
由于字符串內(nèi)容采用的是UTF-8格式編碼,表示一個字符的字節(jié)數(shù)是不定的。即有時是一個字節(jié)表示一個字符,有時是兩個、三個甚至四個字節(jié)表示一個字符。 而這里的長度代表的并不是整個字符串所占用的字節(jié)數(shù),表示這個字符串包含的字符個數(shù)。所以在讀取時需要注意,尤其是在包含中文字符時,往往會因為讀取的長 度不正確導(dǎo)致字符串被截斷。