Android筆記 - 從DEX檔案格式看Dalvik的運作

本文详细介绍了Android的DEX文件格式,探讨了Dalvik虚拟机的工作原理。通过DEX文件的结构,包括DEX和ODEX头文件,以及如何验证和计算文件校验和。内容涵盖字符串、类型、方法和字段的查找,以及RegisterMap在优化Dalvik执行效率中的作用。文章还讨论了LEB128编码、MUTF-8字符编码等关键概念,旨在帮助开发者深入理解Android系统的底层运作。
摘要由CSDN通过智能技术生成

 

Android筆記-DEX檔案格式看Dalvik的運作

 

 

hlchou@mail2000.com.tw
byloda

 

 Loda's Blog

App BizOrz

Android/Linux Source Code Tags
App BizOrz 
BizOrz.COM 
BizOrz Blog



記得剛投入系統軟體領域時,透過一些文章(當年MicrosoftSystems Journal算是很補的技術雜誌.),知道DOSMZ(DOS開發者MarkZbikowski的縮寫命名)檔案格式,也藉此了解在DOS時代使用MemorySegment.com.exe檔案的差異,隨後,Windows3.1推出,NE(NewExecutable)執行檔,VxDLE( Linear Executable)驅動程式與之後的Windows9x/ME/NT/2000/XPPE(PortableExecutable)檔案格式,是有志於了解動態函式庫DLL,Windows應用與核心跟反組譯Hacking,所必備的基礎知識.到了Linux/Unix的環境時,ELF(Executableand Linkable Format)也是從事開發前,從反組譯工具,到了解系統運作時,可以深化系統觀點的知識背景.

 

也因此,在開發Android相關產品時,了解DalvikDEX檔案格式,也應是個值得深入探究的資訊,並藉此了解在Dalvik系統的底層與檔案格式中,所提供的資訊,在評估系統改善項目時,這些資訊就能幫助我們有足夠的背景做出適當的判斷與設計建議.

 

同樣的,筆者會盡力確保所提供的訊息正確性,然可能因為Android本身的改版,與筆者有限的知識,若有不盡完美之處,也歡迎給予指教.

 

關心Android發展的人,也可以透過這個連結http://code.google.com/p/android/issues/list#了解目前相關Issue的狀況,同時,也推薦一個很不錯的Dalvik Talk "Android:Dalvik VM Internals -- Google 2008 台北程式開發日",講者為程本中,目前是Android團隊的一員,Youtube網址是在http://www.youtube.com/watch?v=FsNKIo4bIro&feature=player_embedded#at=26.

 

透過DEX檔案格式的支援,相比過去的作法,筆者認為可以有如下的好處

1,同類型的String(例如:lang/.../string)可以只存在一份,然後透過Index對應,節省為數可觀的儲存空間

2,可以把屬於同一個應用的Class檔案,整合在同一個DEX檔案中,有利於儲存空間與個別應用的管理

3,支援OptimizedDEX格式,只要曾在載入或是安裝時執行過最佳化流程,就可以保留該次的結果,加速下一次的執行.(只限於驗證與最佳化,JIT每次只要重新啟動就會重新執行)

 

一個經過優化過的DEX檔案,前面會加上40bytesODEX檔頭,格式如下所示

typedefstructDexOptHeader {

u1 magic[8];/*includes version number */

 

u4 dexOffset;/*file offset of DEX header */

u4 dexLength;

u4 depsOffset;/*offset of optimized DEX dependency table */

u4 depsLength;

u4 optOffset;/*file offset of optimized data tables */

u4 optLength;

 

u4 flags;/*some info flags */

u4 checksum;/*adler32 checksum covering deps/opt */

 

/*pad for 64-bit alignment if necessary */

}DexOptHeader;

 

 

我們可以透過辨認前面的MagicCode(例如用UltraEdit打開DEX檔案查看前8bytes)是否為"dey/n036/0",確認DEX檔案是否已經被最佳化過.而沒有被優化過的DEX檔案檔頭MagicCode會直接就是"dex/n035/0",可供相關的DEX檔案處理機制判斷.

基本的DEX檔頭如下所示,大小為112bytes.

 

typedefstructDexHeader {

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;

 

接下來就讓我們基於這些檔案欄位,進一步的加以說明.

 

計算DexChecksum

 

Dex驗證Checksum,如果該檔案已經被最佳化處理過,會先跳過前面40bytesDexOptHeader,只取DexHeader之後的部份計算Checksum,首先,會把Dex檔案長度先減去sizeof(pHeader->magic)+ sizeof(pHeader->checksum)(12bytes),從這開始往後用alder32計算原本Dex檔案大小的值,alder32的值與pHeader->checksum值比對,確認該Dex檔案是否有被修改過.

 

而經過最佳化過的檔案,會加上DexOptHeader檔頭,與在檔案尾部加上最佳化時所相依的Classes資訊,因此,ODEXChecksum主要是針對Dex以外的部份,也就是最後面所針對最佳化而加上的訊息進行Checksum計算與比對,運作的機制為從ODEX檔頭往後加上pOptHeader->depsOffset的位置,與取得檔尾的位置,目前的實作為end=pOptHeader+ pOptHeader->optOffset +pOptHeader->optLength,再透過alder32計算從depsOffsetODEX檔案結束點的值與pOptHeader->checksum值比對,確認目前ODEX檔案區間是否有被修改過.

 

驗證目前的ODEX檔案,基本上,ODEX所加入的節區,是在原本的DEX檔案前加上40bytes的檔頭,與在DEX檔案最後加上最佳化相關資訊,而成為一個ODEX檔案.概念如下所示,未來是否會有變動,還請以Android最新的SourceCode為依據.

 

 

ODEX

 

ODEX Header
(DexOptHeader)

DEX

DEX Header
(DexHeader)

DEX Body

pStringIds

pTypeIds

pProtoIds

pFieldIds

pMethodIds

pClassDefs

pLinkData

 

ODEXBody
(DexOptHeader.depsOffset)

 

pClassLookup

pRegisterMapPool

 

 

 

如下所示為Dex檔案對應欄位的說明,

 

名稱

資料格式

說明

ODexHeader

 

OptimizedDex檔頭

DexHeader

header_item

Dex檔頭(請看下一個表格有更清楚的檔頭欄位說明)

string_ids

string_id_item[]

dex檔案中所用到的UTF-16LE字串識別IDStringIds)列表,包括內部的命名(e.g.,type descriptors) 或由程式碼所參考的字串常數物件.

type_ids

type_id_item[]

dex檔案中所用的形態名稱識別ID(TypeIds)列表,包括檔案中所有參考到classes,arrays, or primitive types,不管是否有在這檔案中定義,只要有參考到的就會納入到此.

型態名稱也會對應到StringID Index.

proto_ids

proto_id_item[]

ClassMethod對應的PrototypeIDs,在這會包括所有這個Dex檔案中參考的PrototypeIDs.

field_ids

field_id_item[]

ClassField (變數Type)所對應的IDs,在這會包括所有這個Dex檔案中參考的FieldIDs.

method_ids

method_id_item[]

ClassMethod所對應的IDs,在這會包括所有這個Dex檔案中參考的MethodIDs.在檔案中的排序方式會以TypeId為主,之後參考StringID,之後參考ProtoID.

class_defs

class_def_item[]

ClassDefinitions List可用來查詢每個ClassField/Method,Class Index,Access Flag相關訊息.如果今天要去Dump所有Class/Method/Field的訊息時,ClassDefinitions List會是基本必要元素之一.

link_data

ubyte[]

用來支援StaticllyLinked的資料.(mmm,不過筆者手中沒有看到有支援這Section的檔案.)

 

如下所示為DEX檔頭對應欄位的說明,

 

Name

Format

Description

magic

ubyte[8]= DEX_FILE_MAGIC

屬於DEX檔頭的8bytesMagic Code "dex/n035/0"

checksum

uint

使用zlibadler32所計算的32-bitsCheckSum.計算的範圍為DEX檔案的長度(Header->fileSize)減去8bytes Magic Code4bytes CheckSum的範圍.用來確保DEX檔案內容沒有損毀.

signature

ubyte[20]

SHA-1signature (hash)用來識別原本的DEX檔案(被最佳化以前的DEX).SHA-1計算的範圍為DEX檔案的長度(Header->fileSize)減去8bytes Magic Code,4 bytes CheckSum20bytesSHA-1的範圍.用來識別最原本的DEX檔案的唯一性.(所以被最佳化過後,這個SHA-1儘能用來識別原本的DEX檔案,而無法透過ODEX檔案計算回最原本的DEX檔案SHA-1值了).

file_size

uint

包含DEXHeader與到DEX檔案的檔案長度(inbytes).

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.如果LinkSize0,此值也會為0.資料格式可以參考structDexLink.

map_off

uint

用來表示MapItem距離Dex檔頭的Offset.如果為0,表示這DEX檔案沒有MapItem.資料格式可以參考structmap_list.

string_ids_size

uint

用來表示StringIDs List的總數.

string_ids_off

uint

用來表示StringIds List距離Dex檔頭的Offset.如果StringIDs Size0,此值也會為0.資料格式可以參考structDexStringId.

type_ids_size

uint

用來表示TypeIDs List的總數.

type_ids_off

uint

用來表示TypeIDsList距離Dex檔頭的Offset.如果type_ids_size0這個值亦為0.資料格式可以參考structDexTypeId.

proto_ids_size

uint

用來表示PrototypeIDs List的總數.

proto_ids_off

uint

用來表示PrototypeIDsList距離Dex檔頭的Offset.如果proto_ids_size0這個值亦為0.資料格式可以參考structDexProtoId.

field_ids_size

uint

用來表示FieldIDs List的總數.

field_ids_off

uint

用來表示FieldIDsList距離Dex檔頭的Offset.如果field_ids_size0這個值亦為0.資料格式可以參考structDexFieldId.

method_ids_size

uint

用來表示MethodIDs List的總數.

method_ids_off

uint

用來表示MethodIDsList距離Dex檔頭的Offset.如果method_ids_size0這個值亦為0.資料格式可以參考structDexMethodId.

class_defs_size

uint

用來表示ClassDefinitions List的總數.

class_defs_off

uint

用來表示ClassDefinitionList距離Dex檔頭的Offset.如果class_defs_size0這個值亦為0.資料格式可以參考structDexClassDef.

data_size

uint

用來表示DataSection的大小.並需為sizeof(uint)的偶數倍.(所以就是0,8,16...etc)

data_off

uint

用來表示DataSection距離Dex檔頭的Offset.

 

 

 

Class查找Method,Field與相關的Types,PrototypeString.

 

接下來,我們以實際的例子,Class來查找相關的資訊,讓各位可以對DEX中所包含的資料與結構更有感覺.

 

首先,我們知道DEX檔案可以是一個包含多個Classes檔案的集合,也因此在進行Class分析前,要先從DEX檔頭classDefsSize欄位取得目前DEX檔案所包含的Classes總數,在知道總數後,我們便可以選擇所要解析的是第幾個Class,接下來筆者假設要Dump出第五個Class的資料,就以如下的struct並以距離Dex檔頭pHeader->classDefsOff的距離,取出第五個DexClassDef的資料.

 

typedefstruct DexClassDef {
u4 classIdx; /* index intotypeIds for this class */
u4 accessFlags;
u4 superclassIdx; /* index into typeIds for superclass */
u4 interfacesOff; /* file offset to DexTypeList */
u4 sourceFileIdx; /* index into stringIds for source file name */
u4 annotationsOff; /* file offset toannotations_directory_item */
u4 classDataOff; /* fileoffset to class_data_item */
u4 staticValuesOff; /* fileoffset to DexEncodedArray */
} DexClassDef;

再來參考DexClassDefclassDataOff欄位,得到對應ClassData距離DEX檔頭的位置,並以如下struct讀出DexClassDataHeader的資料,

 

typedefstruct DexClassDataHeader {
u4 staticFieldsSize;
u4instanceFieldsSize;
u4 directMethodsSize;
u4virtualMethodsSize;
} DexClassDataHeader;

並以UnsignedLEB128的方式,讀出值,驗證該值格式正確的方式為,如果讀出後的指標跟原本距離5bytes (LEB128可編碼為1-5bytes),就確認該UsignedLEB1285bytes是否有使用超過4bits(ptr[4] > 0x0f,根據UnsignedLEB128編碼,5bytes只會用到前4bits).如果超過,就返回驗證失敗.

 

若驗證無誤,就會以UnsignedLED128編碼方式,把前四個UnsignedLEB128值讀出來,對應到DexClassDataHeader中的staticFieldsSize,instanceFieldsSize, directMethodsSizevirtualMethodsSize,並以上述static/instanceFielddirect/virtualMethod的個數和所對應的structDexFieldDexMethod用下列的DexClassData配置記憶體記錄ClassData檔頭與其對應的Field/Method資料.

 

typedefstruct DexClassData {
DexClassDataHeader header;
DexField* staticFields;
DexField* instanceFields;
DexMethod* directMethods;
DexMethod* virtualMethods;
} DexClassData;

之後,依序以UnsignedLEB128編碼讀出staticFieldsinstanceFieldsfieldIdx/accessFlags, directMethods/ virtualMethodsmethodIdx/accessFlags/codeOff.筆者把ClassData資料排列的方式透過表格對應如下,由於UnsignedLEB128所編碼的32bits整數值長度會介於1-5bytes,實際要以幾個Bytes來表示個別的UnsignedLEB128,需根據Decode的內容為主.

 

 

長度

對應的結構

4UnsignedLEB128

typedefstruct DexClassDataHeader {
u4 staticFieldsSize;
u4instanceFieldsSize;
u4 directMethodsSize;
u4virtualMethodsSize;
} DexClassDataHeader;

2UnsignedLEB128 *staticFieldsSize

typedef struct DexField {
u4 fieldIdx; /* index to a field_id_item */
u4accessFlags;
} DexField;

2UnsignedLEB128 *instanceFieldsSize

typedef struct DexField {
u4 fieldIdx; /* index to a field_id_item */
u4accessFlags;
} DexField;

3UnsignedLEB128 *directMethodsSize

typedef struct DexMethod {
u4 methodIdx; /* index to a method_id_item */
u4accessFlags;
u4 codeOff; /* file offset to a code_item*/
} DexMethod;

3UnsignedLEB128 *virtualMethodsSize

typedef struct DexMethod {
u4 methodIdx; /* index to a method_id_item */
u4accessFlags;
u4 codeOff; /* file offset to a code_item*/
} DexMethod;

 

 

取得上述DexClassDefClassData,我們會得到相關的Class/Field/MethodIndex,接下來就透過這些Index取得對應的字串.

首先把ClassIndex透過DexHeader中的typeIdsOffstruct DexTypeId取得對應IndexDexTypeId的值,

 

typedefstruct DexTypeId {
u4 descriptorIdx; /* index intostringIds list for type descriptor */
} DexTypeId;

再把取得的descriptorIdx透過DexHeader中的stringIdsOffstructDexStringId取得對應descriptorIdxDexStringId的值,

typedefstruct DexStringId {
u4 stringDataOff; /* file offset tostring_data_item */
} DexStringId;

 

距離Dex檔頭stringDataOff就是對應字串所在位置.

 

DexClassDef(classIdx/superclassIdx)->TypeId Index (DexTypeId)->Desceiptor IdIndex(DexStringId)->StringDataOffset

 

有關Class/Method/Field對應的accessFlagsBit意義如下表

AccessFlag Bit

Class

Method

Field

0x000001

PUBLIC

PUBLIC

PUBLIC

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值