文中所有內(nèi)容均是鄧凡平老師的 深入理解Android之Dalvik 和豐生強(qiáng)老師的 Android軟件安全與逆向分析 閱讀中的筆記
class文件結(jié)構(gòu)
ClassFile{
//唯一取值:0xCAFEBABE
u4 magic;
//class文件的版本號(hào),和Java編譯器有關(guān)
u2 minor_version;
u2 major_version;
//常量池,長(zhǎng)度為字符串?dāng)?shù)量加1,constant_pool[0]留給JVM用
u2 constant_pool_count;
cp_info constant_pool[constant_pool_count - 1];
//class的類型和名字,類型有三種 0x0001:ACC_PUBLIC
//0x0010:ACC_FINAL 0x0200:ACC_INTERFACE
u2 access_flag;
u2 this_class;
u2 super_class;
//類interface數(shù)量,變量數(shù)量,方法數(shù)量(無論static還是非static),
//屬性數(shù)量,名字都作為字符串存在常良池中
u2 interfaces_count;
u2 interfaces[interfaces_count - 1];
u2 fields_count;
field_info fields[fields_count - 1];
u2 methods_count;
method_info methods[methods_count - 1];
u2 attributes_count;
attribute_info attributes[attributes_count - 1];
}
class文件實(shí)操,先寫一個(gè)簡(jiǎn)單的Java文件:
package com.example;
public class MyClass {
public static void main(String[] args){
String s = "hello world";
System.out.println(s);
}
}
然后調(diào)用javac MyClass.java生成MyClass.class
調(diào)用javap -verbose MyClass.class查看

常量池
tips:constant_pool_count值為常量池?cái)?shù)組長(zhǎng)度+1,就像上圖中常量第一個(gè)元素
以#1開頭,0默認(rèn)是給VM用的
常量池的元素類型這樣表示:
cp_info {
//特別注意,這是介紹的cp_info 是相關(guān)元素類型的通用表達(dá)。
u1 tag; //tag 為1 個(gè)字節(jié)長(zhǎng)。不論cp_info 具體是哪種,第一個(gè)字節(jié)一定代表tag
u1 info[]; //其他信息,長(zhǎng)度隨tag 不同而不同
}
tag的取值:
- tag=7 <==info 代表這個(gè)cp_info 是CONSTANT_Class_info 結(jié)構(gòu)體
- tag=9 <==info 代表CONSTANT_Fieldrefs_info 結(jié)構(gòu)體
- tag=10 <==info 代表CONSTANT_Methodrefs_info 結(jié)構(gòu)體
- tag=8 <==info 代表CONSTANT_String_info 結(jié)構(gòu)體
- tag=1 <==info 代表CONSTANT_Utf8_info 結(jié)構(gòu)體
看幾個(gè)例子,字符串結(jié)構(gòu)體:
CONSTANT_Utf8_info {
u1 tag; //取值為1
u2 length; //下面就是存儲(chǔ)UTF8 字符串的地方了
u1 bytes[length];
}
類信息結(jié)構(gòu)體
CONSTANT_Class_info {
u1 tag; //tag 取值為7,代表CONSTANT_Class_info
u2 name_index; //name_index 表示代表自己類名的字符串信息位于于常量池?cái)?shù)組中哪一個(gè),也就是索引
}
dex文件
class文件顯然有很多可以優(yōu)化的地方,比如每一個(gè)class文件都有一個(gè)常量池,如果有重復(fù)字符串就造成了資源浪費(fèi),所以Dalvik的dex文件對(duì)其進(jìn)行了優(yōu)化

先看看dex文件中的數(shù)據(jù)結(jié)構(gòu)
下面很多代碼定義在Android源碼的 DexFile.h 中
| 類型 | 含義 |
|---|---|
| u1 | 等同于uint8_t,一個(gè)字節(jié)的無符號(hào)數(shù) |
| u2 | 等同于uint16_t,兩個(gè)字節(jié)的無符號(hào)數(shù) |
| u4 | 等同于uint32_t,四個(gè)字節(jié)的無符號(hào)數(shù) |
| u8 | 等同于uint64_t,八字節(jié)的無符號(hào)數(shù) |
| sleb128 | 有符號(hào)LEB128,可變長(zhǎng)度1~5字節(jié) |
| uleb128 | 無符號(hào)LEB128,可變長(zhǎng)度1~5字節(jié) |
| uleb128p1 | 無符號(hào)LEB128值加1,可變長(zhǎng)度1~5字節(jié) |
- sleb128是dex文件中特有的數(shù)據(jù)類型,每個(gè)字節(jié)7個(gè)有效位,最高位取值1表示要用到第二個(gè)字節(jié),以此類推但最長(zhǎng)五個(gè)字節(jié),如果讀取到
第五個(gè)字節(jié)最高位仍為1,表示該dex文件無效,Dalvik虛擬機(jī)在驗(yàn)證dex時(shí)會(huì)失敗返回 - dex文件里采用了變長(zhǎng)方式表示字符串長(zhǎng)度。一個(gè)字符串的長(zhǎng)度可能是一個(gè)字節(jié)(小于256)或者4個(gè)字節(jié)(1G大小以上)。字符串的長(zhǎng)度大多數(shù)都是小于 256個(gè)字節(jié),因此需要使用一種編碼,既可以表示一個(gè)字節(jié)的長(zhǎng)度,也可以表示4個(gè)字節(jié)的長(zhǎng)度,并且1個(gè)字節(jié)的長(zhǎng)度占絕大多數(shù)。能滿足這種表示的編碼方式有 很多,但dex文件里采用的是uleb128方式。leb128編碼是一種變長(zhǎng)編碼,每個(gè)字節(jié)采用7位來表達(dá)原來的數(shù)據(jù),最高位用來表示是否有后繼字節(jié)。
查看dex方法
- class轉(zhuǎn)為dex文件,工具是sdk build_tools下的dx命令。dx --dex --debug --verbose-dump--output=test.dex com/test/TestMain.class
- 查看dex文件,利用build-tools 下的dexdump 命令查看,dexdump -d -l plain test.dex
dex文件整體結(jié)構(gòu)
整體結(jié)構(gòu)比較簡(jiǎn)單,由七個(gè)結(jié)構(gòu)體組成:
- dex header 指定了dex文件的一些屬性,并記錄其他六個(gè)部分在dex文件中的物理偏移
- string_ids
- type_ids
- proto_ids
- field_ids
- method_ids
- class_def
- data
- link_data
dexHeader結(jié)構(gòu)體的組成
struct DexHeader {
000 u1 magic[8]; //dex版本標(biāo)識(shí)
u4 checksum; //adler32檢驗(yàn)
u1 signature[KSHA1DIGESTLEN]; //SHA-1哈希值 長(zhǎng)度為20,定義在DexFile.h中
020 u4 fileSize; //整個(gè)文件大小
u4 headerSize; //DexHeader結(jié)構(gòu)大小 70 00 00 00
u4 endianTag; //字節(jié)序標(biāo)記 預(yù)設(shè)78 56 34 12 即0x12345678,表示小端little-Endian字節(jié)序
u4 linkSize; //鏈接段大小
030 u4 linkoff; //連接段偏移
u4 mapoff; //DexMapList的文件偏移,這里mapoff等于dataOff
u4 stringIdsSize; //DexStringId的個(gè)數(shù)
u4 stringIDsOff; //DexStringId的文件偏移
040 u4 typeIdsSize; //DexTypeID的個(gè)數(shù)
u4 typeIdsOff; //DexTypeId的文件偏移
u4 protoIdsSize; //DexProtoId的個(gè)數(shù)
u4 protoIdsOff; //DexProtoId的文件偏移
050 u4 fieldIdsSize; //DexFieldId的個(gè)數(shù)
u4 fieldIdsOff; //DexFieldId的文件偏移
u4 methodIdsSize; //DexMethodId的個(gè)數(shù)
u4 methodIdsOff; //DexMethonId的文件偏移
060 u4 classDefsSize; //DexClassDef的個(gè)數(shù)
u4 classDefsOff; //DexClassDef的文件偏移
u4 dataSize; //數(shù)據(jù)段的大小
u4 dataOff; //數(shù)據(jù)段的文件偏移
}
tips:
由上面結(jié)構(gòu)體也可以看出來,Android 65K方法數(shù)問題的根本原因并不在于Dex文件方法索引長(zhǎng)度限制
dex文件結(jié)構(gòu)分析
tips:
這里 書中(Android軟件安全與逆向分析)有一點(diǎn)不明白,說Dalvik虛擬機(jī)解析dex文件的內(nèi)容,最終將其映射成DexMapList數(shù)據(jù)結(jié)構(gòu)
,是說Dex文件生成過程中有Dalvik虛擬機(jī)的參與嗎。
我分析了一個(gè)簡(jiǎn)單的Android程序,使用十六進(jìn)制編輯器C32Asm,打開apk解壓出的dex文件

上圖就是完整DexHeader的數(shù)據(jù),在注釋里寫得很清楚了,觀察發(fā)現(xiàn),mapOff值為0x00059178,這里要注意小端字節(jié)序,找到

紅色框畫出來的就是每個(gè)元素頭部,其中第一個(gè)0x12,代表有16個(gè)DexMapItem結(jié)構(gòu)
DexMapItem結(jié)構(gòu):
struct DexMapItem{
u2 type; //類型,枚舉常量
u2 unused; //未使用,用于字節(jié)對(duì)其
u4 size; //指定類型的個(gè)數(shù)
u4 offset; //指定類型數(shù)據(jù)的文件偏移
}
//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,
};
舉個(gè)香甜的栗子,找到DexMapList中的StringIdItem,個(gè)數(shù):0x39EE,偏移:0x0070,去找0x0070中的第一個(gè)

看一看DexStringId的結(jié)構(gòu)體:
struct DexStringId{
u4 stringDataOff; // 字符串?dāng)?shù)據(jù)偏移
}
偏移量是0x0012c120,找到它

已經(jīng)找到字符串了
再找一個(gè)復(fù)雜些的,找到DexMapList中的MethodIdItem,個(gè)數(shù)0x000038DD,偏移0x00026E98,找到它

看一看DexMethodId的結(jié)構(gòu)體:
/*
* Direct-mapped "method_id_item".
*/
struct DexMethodId {
u2 classIdx; /* index into typeIds list for defining class */
u2 protoIdx; /* index into protoIds for method prototype */
u4 nameIdx; /* index into stringIds for method name */
};
odex文件
odex文件有兩種存在方式:
- 從Apk文件中提取出來,與Apk文件存放在同一目錄下且文件后綴為odex的文件,這種多是Android ROM的系統(tǒng)程序;
- dalvik-cache緩存文件,這類odex文件仍然以dex作為后綴,存放在cache/dalvik-cache目錄下,保存形式為"apk路徑@apk名@classes.dex";
由于Android程序的Apk文件為Zip壓縮包格式,Dalvik虛擬機(jī)每次加載他們時(shí)需要從Apk中讀取classes.dex文件,這樣會(huì)耗費(fèi)很多CPU時(shí)間,而采用odex
方式優(yōu)化的dex文件已經(jīng)包含了加載dex必須的依賴庫文件列表,Dalvik虛擬機(jī)只需檢測(cè)并加載所需的依賴庫即可執(zhí)行相應(yīng)的dex文件,這大大縮短了讀取dex文件
所需的時(shí)間。
odex文件整體結(jié)構(gòu)
- odex文件頭
- dex文件
- 依賴庫
- 輔助數(shù)據(jù)
odex文件的寫入和讀取并沒有像dex文件那樣定義了全系列的數(shù)據(jù)結(jié)構(gòu),Dalvik虛擬機(jī)將dex文件映射到內(nèi)存中后是DexFile格式,結(jié)構(gòu)如下:
/*
* Structure representing a DEX file.
*
* Code should regard DexFile as opaque, using the API calls provided here
* to access specific structures.
*/
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;
/*
* These are mapped out of the "auxillary" section, and may not be
* included in the file.
*/
const DexClassLookup* pClassLookup;
const void* pRegisterMapPool; // RegisterMapClassPool
/* 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;
};
最前面的DexOptHeader就是odex的頭,DexLink一下的部分是"auxillary section",即輔助數(shù)據(jù)段,記錄了文件被優(yōu)化后添加的一些信息。不過DexFile
機(jī)構(gòu)描述的是加載金內(nèi)存的數(shù)據(jù)結(jié)構(gòu),還有一些數(shù)據(jù)是不會(huì)加載進(jìn)內(nèi)存的。豐生強(qiáng)老師將odex文件結(jié)構(gòu)定義整理如下:
struct ODEXFile {
DexOptHeader header; //odex文件頭
DexFile DexFile; //dex文件
Dependences deps; //依賴庫列表
ChunkDexClassLookup lookup; //類查詢結(jié)構(gòu)
ChunkRegisterMapPool mapPool; //映射池
ChunkEnd end; //結(jié)束標(biāo)識(shí)
}
odex文件結(jié)構(gòu)分析
ODEXFile的文件頭DexOptHeader在DexFile.h文件中定義如下:
struct DexOptHeader{
u1 magic[8]; //odex版本標(biāo)識(shí) ,目前固定值 64 65 79 0A 30 33 36 00
u4 dexOffset; //dex文件頭偏移 ,目前0x28 = 40,等于odex文件頭大小
u4 dexLength; //dex文件總長(zhǎng)度
u4 depsOffset; //odex依賴庫列表偏移
u4 depsLength; //依賴庫列表總長(zhǎng)度
u4 optOffset; //輔助數(shù)據(jù)偏移
u4 optLength; //輔助數(shù)據(jù)總長(zhǎng)度
u4 flags; //標(biāo)志,Dalvik虛擬機(jī)加載odex時(shí)的優(yōu)化與驗(yàn)證選項(xiàng)
u4 checksum; //依賴庫與輔助數(shù)據(jù)的校驗(yàn)和
}