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

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

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

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

0x000002

PRIVATE

PRIVATE

PRIVATE

0x000004

PROTECTED

PROTECTED

PROTECTED

0x000008

STATIC

STATIC

STATIC

0x000010

FINAL

FINAL

FINAL

0x000020

?

SYNCHRONIZED

?

0x000040

?

BRIDGE

VOLATILE

0x000080

?

VARARGS

TRANSIENT

0x000100

?

NATIVE

?

0x000200

INTERFACE

?

?

0x000400

ABSTRACT

ABSTRACT

?

0x000800

?

STRICT

?

0x001000

SYNTHETIC

SYNTHETIC

SYNTHETIC

0x002000

ANNOTATION

?

?

0x004000

ENUM

?

ENUM

0x008000

?

MIRANDA

?

0x010000

VERIFIED

CONSTRUCTOR

?

0x020000

OPTIMIZED

DECLARED_SYNCHRONIZED

?

 

如果ClassDefinition中的interfacesOff不為0,則以structDexTypeList在距離Dex檔頭interfacesOff的位置讀取出Interface的資訊.

 

typedefstruct DexTypeList {
u4 size; /* #of entries inlist */
DexTypeItem list[1]; /* entries */
}DexTypeList;

 

Class對應的Interfaces總數為size,在之後依據size個數,銜接對應的structDexTypeItem內容,

 

typedefstruct DexTypeItem {
u2 typeIdx; /* index intotypeIds */
} DexTypeItem;

 

如同取得ClassDescription字串一樣,透過DexTypeItem會取得TypeIDs Index,之後依循如下的流程,取得InterfaceName (framework.jar為例,Class"android/os/Binder"有如下Interface"android/os/IBinder",Class"android/accessibilityservice/IEventListener"有如下Interface"android/os/IInterface")

 

InterfaceIndex(DexTypeItem)->Type Id Index (DexTypeId)->Desceiptor IdIndex(DexStringId)->StringDataOffset

 

 

Method為例,會以每個Methodstruct DexMethod 中的methodIdx,透過Dex檔頭的methodIdsOff,structDexMethodId,取出該Method對應的ClassIndex/Prototype Index/Name Index(可以看到...都是透過Index,只要其中有越多同名的字串,就可以節省越多的儲存空間成本).

 

typedefstruct DexMethodId {
u2 classIdx; /* index intotypeIds list for defining class */
u2 protoIdx; /*index into protoIds for method prototype */
u4 nameIdx; /* index into stringIds for method name */
} DexMethodId;

透過NameIndex (nameIdx)取得字串示意如下

MethodIndex(DexMethod)->Method Ids Offset (DexMethodId)->NameIndex->Desceiptor Id Index(DexStringId)->StringDataOffset

 

PrototypeIndex (protoIdx)會透過Dex檔頭的protoIdsOff,structDexProtoId取出有關Prototype的相關資訊,

 

typedefstruct DexProtoId {
u4 shortyIdx; /* index intostringIds for shorty descriptor */
u4 returnTypeIdx; /*index into typeIds list for return type */
u4 parametersOff; /* file offset to type_list for parameter types */
}DexProtoId;

 

其中如果parametersOff不為0,則會以距離Dex檔頭parametersOff距離,struct DexTypeList並根據size取得TypeList總量,DexTypeItem中的typeIdx就會在對應到每個函式Method參數所對應的TypeIndex,並可由此取得最後的參數名稱.

 

typedefstruct DexTypeList {
u4 size; /* #of entries inlist */
DexTypeItem list[1]; /* entries */
}DexTypeList;

 

同時,函式Method的返回值型態會透過returnTypeIdx來反查.framework.jarClassandroid/os/IBinder一個有4個參數的Method"transact"例子為例,Method參數(ILandroid/os/Parcel;Landroid/os/Parcel;I)會由左而右,透過DexTypeList中的list依序往後擺放.

 

 

Method參數左
I

DexTypeItemlist[0]

Landroid/os/Parcel;

DexTypeItemlist[1]

Landroid/os/Parcel;

DexTypeItemlist[2]

Method參數右
I

DexTypeItemlist[3]

 

並透過returnTypeIdx取得該函式Method返回值的形態字串Z.我們可以看到,這些型態的表述方式,跟透過JNI介面,要由C程式調用Java函式時,所描述的Java函式的形態是一致的(請自行參考JNI中對於TypeSignature的說明,例如:http://journals.ecs.soton.ac.uk/java/tutorial/native1.1/implementing/method.html).shortyIdx則直接對應到StringIndex.

 

有關Method所對應的ByteCode位置,則可透過structDexMethod中的codeOff取得structDexCode,struct中會包含Method函式中所用到的Registers總數(registersSize),Method所包含的參數個數(insSize),Method實作中用來做為呼叫其他Method帶入參數的Registers個數(outsSize),Method實作中Try/Catch的個數(triesSize),16bits為單位的Method實作指令集個數(insnsSize)與最後真正Method的實作內容(insns).

 

 

typedefstruct DexCode {
u2 registersSize;
u2 insSize;
u2 outsSize;
u2 triesSize;
u4 debugInfoOff; /*file offset to debug info stream */
u4 insnsSize; /*size of the insns array, in u2 units */
u2 insns[1];
/* followed by optional u2 padding */
/* followed bytry_item[triesSize] */
/* followed by uleb128 handlersSize */
/* followed by catch_handler_item[handlersSize] */
} DexCode;

 

MethodTry/Catch的資訊會透過struct DexTry取得,所在位置為DexCode->insns ByteCode實作的結尾(結尾位置為4bytes的倍數).

 

typedefstruct DexTry {
u4 startAddr; /* start address, in16-bit code units */
u2 insnCount; /* instructioncount, in 16-bit code units */
u2 handlerOff; /*offset in encoded handler data to handlers */
} DexTry;

 

 

 

而有關Class變數Field的部份,會以每個Fieldstruct DexField 中的fieldIdx,透過Dex檔頭的fieldIdsOff,structDexFieldId,取出該Field對應的ClassIndex/Prototype Index/Name Index

 

typedefstruct DexFieldId {
u2 classIdx; /* index intotypeIds list for defining class */
u2 typeIdx; /*index into typeIds for field type */
u4 nameIdx; /* index into stringIds for field name */
} DexFieldId;

 

如同前述對Method資訊的解析,classIdx可以作為所屬ClassTypeIndex,typeIdx可以作為Field本身型態的TypeIndex,最後nameIdx則做為Field名稱的SreingID Index.

 

 

DEXSHA-1 Signature

 

DEX檔頭會帶一個160-bitsSHA-1簽名,主要功能是用來識別最佳化前的DEX檔案,作為最佳化前DEX檔案唯一的識別碼,一旦DEX檔案有經過最佳化或Byte-Swapped(如果把Little-EndianDex放到Big-Endian環境執行),SHA-1值就會無法被計算回來,該值的意義主要只用於識別而非驗證檔案是否有被修改.

 

計算SHA-1簽名的方式為,Dex檔案長度先減去sizeof(pHeader->magic)+ sizeof(pHeader->checksum) + kSHA1DigestLen (8 bytes MagicCode,4bytes Checksum20bytes SHA-1 Digest),然後再透過SHA-1計算Dex檔案數值.只要該檔案有經過最佳化,計算的結果就會與原本的簽名不同.

 

Dalvik虛擬器的ByteCode指令格式

 

 

參考Android文件,筆者在這表述方式為,每個[]都代表一個16bits,由低位元到高位元,op等於一個8bits的指令碼,A/B/C/D/E/F/G/H代表一個4bits的數值,可用來代表暫存器0-15或是數值.或是兩個AA/BB/CC/DD/EE/FF/GG/HH代表一個8bits的數值,表彰暫存器0-255或是數值..或是4AAAA/BBBB/CCCC/DDDD/EEEE/FFFF/GGGG/HHHH代表一個16bits的數值.

 

指令集格式

語法

範例

[op|ØØ]

Op

[0000]

nop

[op|B|A]

OpvA, vB

[0110]
movev0, v1

[0760]

move-objectv0, v6

OpvA, #+B

[1214]

const/4 v4,#int 1 // #1

[op|AA]

OpvAA

[0a02]

move-result v2

Op+AA

[28ec]

goto 0030 //-0014

[op|ØØ][AAAA]

Op+AAAA

[2900 56ff]

goto/16 0005// -00aa

[op|AA][BBBB]

opvAA, vBBBB

[0801 1a00]

move-object/from16v1, v26

opvAA, +BBBB

[3805 f5ff]

if-eqz v5,002e // -000b

opvAA, #+BBBB

[1306 0a00]

const/16 v6,#int 10 // #a

opvAA, #+BBBB0000

opvAA, #+BBBB000000000000

[1504 0100](const/high16vAA, #+BBBB0000)

const/high16v4, #int 65536 // #1

[1900 f07f](const-wide/high16vAA, #+BBBB000000000000)

const-wide/high16v0, #long 9218868437227405312 // #7ff0

opvAA, type@BBBB

opvAA, field@BBBB

opvAA, string@BBBB

[1c00090e](const-classvAA, type@BBBB )

const-classv0, [F // class@0e09

(sstaticopvAA,field@BBBB )

[1a02dd0d](const-stringvAA, string@BBBB )

const-stringv2, "Class doesn't implement Cloneable" // string@0ddd

[op|AA][BB|CC]

opvAA,vBB, vCC

[2d000405](cmpkindvAA,vBB, vCC)

cmpl-float v0,v4, v5

opvAA,vBB, #+CC

[d8040401](binop/lit8vAA, vBB, #+CC )

add-int/lit8v4, v4, #int 1 // #01

[op|B|A][CCCC]

opvA,vB, +CCCC

[3376 1400](if-testvA,vB, +CCCC )

if-ne v6, v7,001a // +0014

opvA,vB, #+CCCC

[d209 e803]7(binop/lit16vA, vB, #+CCCC )

mul-int/lit16v9, v0, #int 1000 // #03e8

opvA,vB, type@CCCC
opvA,vB, field@CCCC

[2394 0a0e](new-arrayvA, vB, type@CCCC)

new-array v4,v9, [I // class@0e0a

[2049 c600](instance-ofvA, vB, type@CCCC)

instance-ofv9, v4, Ljava/io/Serializable; // class@00c6

opvA,vB, fieldoff@CCCC

 

[op|ØØ][AAAAlo] [AAAAhi]

op+AAAAAAAA

(goto/32+AAAAAAAA)

[op|ØØ][AAAA] [BBBB]

opvAAAA,vBBBB

(move-object/16vAAAA, vBBBB )

[op|AA][BBBBlo][BBBBhi]

opvAA,#+BBBBBBBB

(constvAA, #+BBBBBBBB)

opvAA,+BBBBBBBB

(packed-switchvAA, +BBBBBBBB )

opvAA,string@BBBBBBBB

(const-string/jumbovAA, string@BBBBBBBB)

[op|B|A][CCCC] [E|D|G|F]

[B=5op{vD,vE, vF, vG, vA}, meth@CCCC
[B=5op{vD,vE, vF, vG, vA}, type@CCCC
[B=4op{vD,vE, vF, vG}, kind@CCCC
[B=3op{vD,vE, vF}, kind@CCCC
[B=2op{vD,vE}, kind@CCCC
[B=1op{vD}, kind@CCCC
[B=0op{}, kind@CCCC

 

and

 

[B=5]op{vD,vE, vF, vG, vA}, vtaboff@CCCC
[B=4]op{vD,vE, vF, vG}, vtaboff@CCCC
[B=3]op{vD,vE, vF}, vtaboff@CCCC
[B=2]op{vD,vE}, vtaboff@CCCC
[B=1]op{vD},vtaboff@CCCC

 

[7020 090a2100] (invoke-kind{vD,vE, vF, vG, vA}, meth@CCCC)

invoke-direct{v1, v2}, Ljava/lang/AssertionError;.<init>:(Ljava/lang/Object;)V// method@0a09

[2420 0a0e6500] (filled-new-array {vD, vE, vF, vG, vA}, type@CCCC)

filled-new-array{v5, v6}, [I // class@0e0a

[f840 fb001032]

+invoke-virtual-quick{v0, v1, v2, v3}, [00fb] // vtable #00fb

[f830 fa001002]

+invoke-virtual-quick{v0, v1, v2}, [00fa] // vtable #00fa

[f820 d3001000]

+invoke-virtual-quick{v0, v1}, [00d3] // vtable #00d3

[op|B|A][CCDD][F|E|H|G]

[B=5]op{vE,vF, vG, vH, vA}, vtaboff@CC, iface@DD
[B=4]op{vE,vF, vG, vH}, vtaboff@CC, iface@DD
[B=3]op{vE,vF, vG}, vtaboff@CC, iface@DD
[B=2]op{vE,vF}, vtaboff@CC, iface@DD
[B=1]op{vE},vtaboff@CC, iface@DD

 

[op|AA][BBBB][CCCC]

op{vCCCC.. vNNNN}, meth@BBBB
op{vCCCC.. vNNNN}, type@BBBB

(where NNNN= CCCC+AA-1,that is A determinesthe count 0..255,and C determinesthe first register)

 

and

 

op{vCCCC.. vNNNN}, vtaboff@BBBB

(where NNNN= CCCC+AA-1,that is A determinesthe count 0..255,and C determinesthe first register)

 

 

[7609 65020000]

invoke-direct/range{v0, v1, v2, v3, v4, v5, v6, v7, v8},Landroid/view/animation/TranslateAnimation;.<init>:(IFIFIFIF)V// method@0265

[7701 77031100]

invoke-static/range{v17},Lcom/android/launcher2/AllApps3D$RolloRS;.access$400:(Lcom/android/launcher2/AllApps3D$RolloRS;)F// method@0377method@2b49

[7803 8a001500]

invoke-interface/range{v21, v22, v23},Landroid/content/SharedPreferences;.getBoolean:(Ljava/lang/String;Z)Z// method@008a

[f91b fc000400]

+invoke-virtual-quick/range{v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17,v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30},[00fc] // vtable #00fc

[op|AA][BBCC] [DDDD]

op{vDDDD.. vNNNN}, vtaboff@BB, iface@CC

(where NNNN= DDDD+AA-1,that is A determinesthe count 0..255,and D determinesthe first register)

 

 

[op|AA][BBBBlo] [BBBB][BBBB] [BBBBhi]

opvAA,#+BBBBBBBBBBBBBBBB

[1807 ffff0000 ffff 0000]

const-wide v7,#double 0.000000 // #0000ffff0000ffff

[1803 6d9c2e3a 42ce 478e]

const-wide v3,#double -0.000000 // #8e47ce423a2e9c6d

 

dalvik指令集來看,比較像是CISC架構的指令集思維,每個指令基本單位為16bits,最長的指令需要80bits(10bytes)的長度,根據不同指令的需求,可以有16bits倍數的延伸,這跟ARMRISC架構下,會以固定32bits16bits完成一個指令的設計取向有所不同.反而比較接近x86CISC架構下,指令可以根據設計有不同長度的延伸,例如下列x86指令1,2,711bytes的例子

 

(1bytes)0×48= dec eax

 

(2bytes)0×89F9= mov ecx,edi

 

(7bytes)0x8BBC 24 A4 01 00 00 = mov edi,dword ptr [esp+000001A4h]

 

(11bytes)0×81BC 24 14 01 00 00 FF 00 00 00 = cmp dword ptr [esp+00000114h],0FFh

 

DalvikByteCodeSunByteCode的進一步說明

 

Dalvik虛擬器指令集最多可以支援256個暫存器,每個暫存器都會透過FP對應到一個32bits的記憶體位置,除了在指令執行階段會透過ARM暫存器(以目前Dalvik的實作,會用到R0,R1,R2,R3,R9R106個暫存器)對應到Dalvik指令實作進行加速外,主要的Dalvik暫存器都是透過外部的記憶體來暫存,模擬為Dalvik虛擬器中的暫存器.

 

接下來,我們透過相同的Java函式呼叫程式碼,分別編譯為DalvikByteCodeSunJava ByteCode進行比較,了解兩者在實現上的差異.

 

首先,以如下的函式呼叫為例

 

Main1(111);(函式原型voidMain1(intA))

 

dalvik,ByteCode的實現為

 

[13046f00] const/16 v4, #int 111 // #6f

[08002400] move-object/from16 v0, v36 //暫存器36指到TestAAClass Object本身

[0141] move v1, v4

[f820f900 1000] +invoke-virtual-quick {v0, v1}, [00f9] // vtable #00f9

 

sunJava,ByteCode實現為(會把函式參數由左往右推入Stack)

11: aload_0 //把區域變數0(指到TestAAClass Object本身)推到Stack

12: bipush 111 //Push 1 byte (111)Stack

14: invokevirtual #58; //Method Main1:(I)V

 

不過以指令集個數而言,DalvikDx顯然做了沒有效率的轉譯,先把值放到v4暫存器,之後再轉給v1,其實可以直接轉譯為

 

const/16v1,#int111 // #6f

 

就可以省去1move指令(2bytes)

 

再以如下的函式呼叫為例

intv2=Main2(111,222); (函式原型為intMain2(intA,intB))

 

dalvik,ByteCode的實現為

[13046f00] const/16 v4, #int 111 // #6f

[1305de00] const/16 v5, #int 222 // #de

[08002400] move-object/from16 v0, v36//暫存器36指到TestAAClass Object本身

[0141]move v1, v4

[0152]move v2, v5

[f830fa00 1002] +invoke-virtual-quick {v0, v1, v2}, [00fa] // vtable #00fa

[0a1f] move-result v31 //Return值存到暫存器31

 

sunJava,ByteCode實現為(會把函式參數由左往右推入Stack)

17: aload_0 //把區域變數0(指到TestAAClass Object本身)推到Stack

18: bipush 111 //Push 1 byte (111)Stack

20: sipush 222 //Push 2 byte (Short) (222)Stack

23: invokevirtual #60; //Method Main2:(II)I

26: istore_2 //return值存到區域變數2

 

相比SunJavaCode而言,還是多了兩次沒有必要的Move動作,浪費了2個指令共4bytes的空間.可能是筆者對Google大師期待過高.....@_@,在比較DexByteCode的一些邏輯後,其實還是有待改善的部份,但不可否認的Dalvik整個實作,是一個很不錯的作品,值得有志於了解模擬器實作的人,花時間深入理解.

 

我們可以看到,在函式參數傳遞時,Dalvik會選擇以暫存器的方式傳遞,以便對應到底層實作時,可以透過處理器本身的暫存器加速指令集的效率,SunJava Vm的函數參數的傳遞,則是透過Stack,由左往右把函式參數推到Stack(C的由右往左推,是相反的).

 

在函式參數的返回值部分,Dalvik會透過暫存器返回,再由呼叫端儲存到自己的暫存器中(這點跟C語言也是比較類似的),反之,SunJava虛擬器的函式返回值會放在Stack,再由呼叫端從Stack取出放到本地的區域變數中.

 

ByteCode顧名思義指令集會以一個Byte的長度來定義,但是在Dalvik,ByteCode的單位是以16bits為單位(Dalvik目前共有約230個指令集),通常第一個Byte為指令集,第二個Byte為使用的暫存器,一個基本指令集的描述,會需要16-bits(1byte指令 +1bytes暫存器),一個最簡單的例子就是

return-void= 0e00

returnv1 = 0f01

returnv2 = 0f02

return-objectv1 = 1101

 

另一種組合就是32-bits的指令集組合,如下所示,可以用各4bits代表目標與來源暫存器,16bitsOffset.

 

+iput-quickv2, v3, [obj+0120] = f532 2001

+iput-quickv1, v3, [obj+0118] = f531 1801

+iput-quickv0, v1, [obj+0110] = f510 1001

+iput-object-quickv1, v0, [obj+00d8] = f701 d800

 

其次,還有48-bits的組合

+invoke-virtual-quick{v6, v5}, [00d1] = f820 d100 5600

+invoke-virtual-quick{v6, v2}, [00d3] = f820 d300 2600

+invoke-virtual-quick{v6, v3}, [0034] = f820 3400 3600

+invoke-virtual-quick{v2}, [01b7] = f810 b701 0200

 

 

參考Android文件,DalvikByteCode在設計初期,希望MachineModelCallingConventions能儘量接近於真實系統與C風格的CallingConventions.Dalvik虛擬器為Register-Based,並會根據每個Method所需要的函式參數或是區域變數,在該Method被執行時,產生對應固定大小的Frame(這會供DalvikRegister使用).每個DalvikRegister固定長度為32bits,相鄰的兩個DalvikRegister可以用來表示64bits的值.

 

函式如果有N個參數,在呼叫該函式時,就會用到NRegister,來作為該函式的呼叫之用,而在函式內部的實作中,也會用到NRegister來操作所傳入的N個參數,只是上述的N個暫存器的對應,在呼叫端與函式內部的實作,不一定能直接對應.如果參數為64bits(例如double),就會一次用到兩個Register代表該64bits的函式參數.目前Method所在的Class(也就是this),會是每個Method函式呼叫時的第一個參數.

 

Dalvik指令集中所帶的數值,都固定為16bitsunsigned型態,根據指令的不同,該指令16bits的描述中有的Bits可以被忽略,或是必須為0.對於透過32bits暫存器進行Mov的資料,並不會去確認資料是整數(int)或是浮點數(float).站在指令集的角度,所對應的暫存器範圍不大於256(也就是說最多會用1bytes=8bits去描述一個暫存器,最大定義到0xff),而有些16-bits指令描述暫存器只會用到4bits(也就是說對這類指令暫存器最大只定義到0x0f).

 

有些DalvikByteCode指令所參考的資料為不定長度的內容,這些資料所在記憶體位置必須是4bytesMemory Alignment,例如:fill-array-data,packed-switch,在不足4bytesAlignment的部份就會插入16bits Nop 指令作為Spacer,如下例子

 

0005: packed-switch v0, 00000012 // +0000000d

=>參考位於12的資料,由於要為4bytesAlignment,所以在11的位址插入2bytesSpacer

......................

.......................

0011: nop // spacer

0012:0001 0200 1300 0000 0800 0000 0a00 ... : packed-switch-data (8 units)

 

為解決原本JavaByteCode檔案格式中,重複字串所導致的儲存空間浪費,DalvikDex檔案格式,會建置StringID,Type ID,Prototype ID,Field ID,Method IDClassIDTables,只要有重複的字串,就會透過StringID指到同一個Data區塊中,節省儲存空間

 

接下來讓我們以實際的Java程式碼,對應到DalvikSun JavaByteCode,比較其中實作的差異化,來了解兩者技術的不同,以下列程式碼為例

 

publicvoidMain1(intA)

{

intvA;

intvB;

vA=A;

vB=A*2;

}

 

反組譯的DalvikByteCode

 

000430:com.TestAA.TestAA.Main1:(I)V

0000:[0130] move v0, v3

0001:[da01 0302] mul-int/lit8 v1, v3, #int 2 // #02

0003:[0e00] return-void

 

其中,暫存器對應如下所示

0x0001- 0x0004 reg=0 vA I

0x0003- 0x0004 reg=1 vB I

0x0000- 0x0004 reg=2 this Lcom/TestAA/TestAA;

0x0000- 0x0004 reg=3 A I

 

而同樣的程式碼,如果用JavaByteCode呈現如下

0: iload_1 //load函式參數A

1: istore_2 //store到區域變數vA

2: iload_1 //load函式參數A

3: iconst_2 //設定常數2

4: imul //A*2

5: istore_3 //store到區域變數vB

6: return //return void.

LocalVariableTable:

Start Length Slot Name Signature

0 7 0 this Lcom/TestAA/TestAA;

0 7 1 A I

2 5 2 vA I

6 1 3 vB I

 

 

我們可以看到DalvikByteCode的實作比較簡潔,CodeSize8bytes,總共用了三個指令,而原本Stack-BasedJavaByteCode所需CodeSize7bytes,總共用了7個指令,需要比較多的load/store動作,無法像是DalvikRegister-Based的實作,以仿似一般處理器使用暫存器的方式,用三個指令就把原本Java代碼中的邏輯完成.

 

 

接下來,我們增加函式的參數,並修改return值為int,如下程式碼

publicintMain2(intA,intB)

{

intvA;

intvB;

vA=A;

vB=A*2+B;

returnvA+vB;

}

 

反組譯的DalvikByteCode

 

000448:com.TestAA.TestAA.Main2:(II)I

0000:[0140] move v0, v4

0001:[da02 0402] mul-int/lit8 v2, v4, #int 2// #02

0003:[9001 0205] add-int v1, v2, v5

0005:[9002 0001] add-int v2, v0, v1

0007:[0f02] return v2

其中,暫存器對應如下所示

0x0001- 0x0008 reg=0 vA I

0x0005- 0x0008 reg=1 vB I

0x0000- 0x0008 reg=3 this Lcom/TestAA/TestAA;

0x0000- 0x0008 reg=4 A I

0x0000- 0x0008 reg=5 B I

 

而同樣的程式碼,如果用JavaByteCode呈現如下

0: iload_1

1: istore_3

2: iload_1

3: iconst_2

4: imul

5: iload_2

6: iadd

7: istore 4

9: iload_3

10: iload 4

12: iadd

13: ireturn

 

LocalVariableTable:

Start Length Slot Name Signature

0 14 0 this Lcom/TestAA/TestAA;

0 14 1 A I

0 14 2 B I

2 12 3 vA I

9 5 4 vB I

 

 

接下來,我們把函式參數增為三個,並修改return值為double,如下程式碼

publicdoubleMain3(intA,intB,intC)

{

intvA;

intvB;

vA=A+3;

vB=A*2+B*3+C*4;

return(double)vA+vB;

}

 

反組譯的DalvikByteCode

000468:com.TestAA.TestAA.Main3:(III)D

0000:[d800 0703] add-int/lit8 v0, v7, #int 3 // #03

0002:[da02 0702] mul-int/lit8 v2, v7, #int 2 // #02

0004:[da03 0803] mul-int/lit8 v3, v8, #int 3 // #03

0006:[b032] add-int/2addr v2, v3

0007:[da03 0904] mul-int/lit8 v3, v9, #int 4 // #04

0009:[9001 0203] add-int v1, v2, v3

000b:[8302] int-to-double v2, v0

000c:[8314] int-to-double v4, v1

000d:[cb42] add-double/2addr v2, v4

000e:[1002] return-wide v2

 

而同樣的程式碼,如果用JavaByteCode呈現如下

0: iload_1

1: iconst_3

2: iadd

3: istore 4

5: iload_1

6: iconst_2

7: imul

8: iload_2

9: iconst_3

10: imul

11: iadd

12: iload_3

13: iconst_4

14: imul

15: iadd

16: istore 5

18: iload 4

20: i2d

21: iload 5

23: i2d

24: dadd

25: dreturn

 

接下來,我們把函式參數增為26,並修改return值為int,如下程式碼

publicintMain4(intA,intB,intC,intD,intE,intF,intG,intH,intI,intJ,intK,intL,intM,intN,intO,intP,intQ,intR,intS,intT,intU,intV,intW,intX,intY,intZ)

{

intvA;

intvB;

vA=A+B+C+D+E+F+G+H+I+J+K+L+M+N+O+P+Q+R+S+T+U+V+W+X+Y+Z;

vB=A*1+B*2+C*3+D*4+E*5+F*6+G*7+H*8+I*9+J*10+K*11+L*12+M*13+N*14+O*15+P*16+Q*17+R*18+S*19+T*20+U*21+V*22+W*23+X*24+Y*25+Z*26;

returnvA+vB;

}

 

反組譯的DalvikByteCode

000498:com.TestAA.TestAA.Main4:(IIIIIIIIIIIIIIIIIIIIIIIIII)I

0000:[9002 0506] add-int v2, v5, v6

0002:[b072] add-int/2addr v2, v7

0003:[b082] add-int/2addr v2, v8

0004:[b092] add-int/2addr v2, v9

0005:[b0a2] add-int/2addr v2, v10

0006:[b0b2] add-int/2addr v2, v11

0007:[b0c2] add-int/2addr v2, v12

0008:[b0d2] add-int/2addr v2, v13

0009:[b0e2] add-int/2addr v2, v14

000a:[b0f2] add-int/2addr v2, v15

000b:[9002 0210] add-int v2, v2, v16

000d:[9002 0211] add-int v2, v2, v17

000f:[9002 0212] add-int v2, v2, v18

0011:[9002 0213] add-int v2, v2, v19

0013:[9002 0214] add-int v2, v2, v20

0015:[9002 0215] add-int v2, v2, v21

0017:[9002 0216] add-int v2, v2, v22

0019:[9002 0217] add-int v2, v2, v23

001b:[9002 0218] add-int v2, v2, v24

001d:[9002 0219] add-int v2, v2, v25

001f:[9002 021a] add-int v2, v2, v26

0021:[9002 021b] add-int v2, v2, v27

0023:[9002 021c] add-int v2, v2, v28

0025:[9002 021d] add-int v2, v2, v29

0027:[9000 021e] add-int v0, v2, v30

0029:[da02 0501] mul-int/lit8 v2, v5, #int 1 // #01

002b:[da03 0602] mul-int/lit8 v3, v6, #int 2 // #02

002d:[b032] add-int/2addr v2, v3

002e:[da03 0703] mul-int/lit8 v3, v7, #int 3 // #03

0030:[b032] add-int/2addr v2, v3

0031:[da03 0804] mul-int/lit8 v3, v8, #int 4 // #04

0033:[b032] add-int/2addr v2, v3

0034:[da03 0905] mul-int/lit8 v3, v9, #int 5 // #05

0036:[b032] add-int/2addr v2, v3

0037:[da03 0a06] mul-int/lit8 v3, v10, #int 6 // #06

0039:[b032] add-int/2addr v2, v3

003a:[da03 0b07] mul-int/lit8 v3, v11, #int 7 // #07

003c:[b032] add-int/2addr v2, v3

003d:[da03 0c08] mul-int/lit8 v3, v12, #int 8 // #08

003f:[b032] add-int/2addr v2, v3

0040:[da03 0d09] mul-int/lit8 v3, v13, #int 9 // #09

0042:[b032] add-int/2addr v2, v3

0043:[da03 0e0a] mul-int/lit8 v3, v14, #int 10 // #0a

0045:[b032] add-int/2addr v2, v3

0046:[da03 0f0b] mul-int/lit8 v3, v15, #int 11 // #0b

0048:[b032] add-int/2addr v2, v3

0049:[da03 100c] mul-int/lit8 v3, v16, #int 12 // #0c

004b:[b032] add-int/2addr v2, v3

004c:[da03 110d] mul-int/lit8 v3, v17, #int 13 // #0d

004e:[b032] add-int/2addr v2, v3

004f:[da03 120e] mul-int/lit8 v3, v18, #int 14 // #0e

0051:[b032] add-int/2addr v2, v3

0052:[da03 130f] mul-int/lit8 v3, v19, #int 15 // #0f

0054:[b032]: add-int/2addr v2, v3

0055:[da03 1410] mul-int/lit8 v3, v20, #int 16 // #10

0057:[b032] add-int/2addr v2, v3

0058:[da03 1511] mul-int/lit8 v3, v21, #int 17 // #11

005a:[b032] add-int/2addr v2, v3

005b:[da03 1612] mul-int/lit8 v3, v22, #int 18 // #12

005d:[b032] add-int/2addr v2, v3

005e:[da03 1713] mul-int/lit8 v3, v23, #int 19 // #13

0060:[b032] add-int/2addr v2, v3

0061:[da03 1814] mul-int/lit8 v3, v24, #int 20 // #14

0063:[b032] add-int/2addr v2, v3

0064:[da03 1915] mul-int/lit8 v3, v25, #int 21 // #15

0066:[b032] add-int/2addr v2, v3

0067:[da03 1a16] mul-int/lit8 v3, v26, #int 22 // #16

0069:[b032] add-int/2addr v2, v3

006a:[da03 1b17] mul-int/lit8 v3, v27, #int 23 // #17

006c:[b032] |: add-int/2addr v2, v3

006d:[da03 1c18] mul-int/lit8 v3, v28, #int 24 // #18

006f:[b032] add-int/2addr v2, v3

0070:[da03 1d19] mul-int/lit8 v3, v29, #int 25 // #19

0072:[b032] add-int/2addr v2, v3

0073:[da03 1e1a] mul-int/lit8 v3, v30, #int 26 // #1a

0075:[9001] [0203] add-int v1, v2, v3

0077:[9002 0001] add-int v2, v0, v1

0079:[0f02] return v2

 

 

而同樣的程式碼,如果用JavaByteCode呈現如下

0: iload_1

1: iload_2

2: iadd

3: iload_3

4: iadd

5: iload 4

7: iadd

8: iload 5

10: iadd

11: iload 6

13: iadd

14: iload 7

16: iadd

17: iload 8

19: iadd

20: iload 9

22: iadd

23: iload 10

25: iadd

26: iload 11

28: iadd

29: iload 12

31: iadd

32: iload 13

34: iadd

35: iload 14

37: iadd

38: iload 15

40: iadd

41: iload 16

43: iadd

44: iload 17

46: iadd

47: iload 18

49: iadd

50: iload 19

52: iadd

53: iload 20

55: iadd

56: iload 21

58: iadd

59: iload 22

61: iadd

62: iload 23

64: iadd

65: iload 24

67: iadd

68: iload 25

70: iadd

71: iload 26

73: iadd

74: istore 27

76: iload_1

77: iconst_1

78: imul

79: iload_2

80: iconst_2

81: imul

82: iadd

83: iload_3

84: iconst_3

85: imul

86: iadd

87: iload 4

89: iconst_4

90: imul

91: iadd

92: iload 5

94: iconst_5

95: imul

96: iadd

97: iload 6

99: bipush 6

101:imul

102:iadd

103:iload 7

105:bipush 7

107:imul

108:iadd

109:iload 8

111:bipush 8

113:imul

114:iadd

115:iload 9

117:bipush 9

119:imul

120:iadd

121:iload 10

123:bipush 10

125:imul

126:iadd

127:iload 11

129:bipush 11

131:imul

132:iadd

133:iload 12

135:bipush 12

137:imul

138:iadd

139:iload 13

141:bipush 13

143:imul

144:iadd

145:iload 14

147:bipush 14

149:imul

150:iadd

151:iload 15

153:bipush 15

155:imul

156:iadd

157:iload 16

159:bipush 16

161:imul

162:iadd

163:iload 17

165:bipush 17

167:imul

168:iadd

169:iload 18

171:bipush 18

173:imul

174:iadd

175:iload 19

177:bipush 19

179:imul

180:iadd

181:iload 20

183:bipush 20

185:imul

186:iadd

187:iload 21

189:bipush 21

191:imul

192:iadd

193:iload 22

195:bipush 22

197:imul

198:iadd

199:iload 23

201:bipush 23

203:imul

204:iadd

205:iload 24

207:bipush 24

209:imul

210:iadd

211:iload 25

213:bipush 25

215:imul

216:iadd

217:iload 26

219:bipush 26

221:imul

222:iadd

223:istore 28

225:iload 27

227:iload 28

229:iadd

230:ireturn

 

彙整上述四個函式的Java與反組譯結果,我們可以發現

1,Dalvik虛擬器所配置的暫存器個數會以資料總數為依據,每個暫存器寬度為32bits.且除函式參數與區域變數外,暫存器會配合執行需求重複利用.

2,Dalvik虛擬器暫存器並不像是C或是ARM原生程式一樣,有固定的函式參數推入的機制或是會使用的暫存器(C語言在X86Stack由右往左推或是ARMR0-R3暫存器來傳遞函式參數),而是根據資料的長度決定

 

 

stackReq= method->registersSize * 4 // params + locals

+sizeof(StackSaveArea) * 2 // break frame + regular frame

+method->outsSize * 4; // args to other methods

 

比較Stack-BasedRegister-BasedVM記憶體與指令實作的方式

 

透過JDK所編譯出來的JavaClass檔案,會包含如下的資訊

 

LocalVariableTable:

Start Length Slot Name Signature

0 38 0 this Lcom/testAA/testAA;

0 38 1 A I

0 38 2 B I

0 38 3 C I

5 33 4 vA I

18 20 5 vB I

 

用來描述每一個函式參數或區域變數對應到StackFrame中的位置,以如下Main3Method時做為例

 

publicclasstestAAextendsActivity {

intgA;

intgB;

.....

publicdoubleMain3(intA,intB,intC)

{

intvA;

intvB;

vA=A+3;

vB=A*2+B*3+C*4;

gA=vA;

gB=vB;

return(double)vA+vB;

}

....

}

 

對應到Main3的呼叫端實作

doublev3=Main3(111,222,333);

 

在呼叫端的JavaByteCode實現為

27: aload_0

28: bipush 111

30: sipush 222

33: sipush 333

36: invokevirtual #62;//Method Main3:(III)D

39: dstore_3

 

我們可以看到參數由左往右推入堆疊中,參照StackFrame中的位置,可以看到StackFrame Slot 0會擺放函式參數的第一個變數(也就是Class本身this),依此類推把函式參數放置完後,再配置Method中對應的區域變數,同樣由宣告的順序開始擺放到StackFrame Slot.

 

每個Slot中所包含的變數,都會包含該變數對應到ByteCode所需存在的行數起點與終點,參考上述LocalVariableTable的描述,Slot1 (函式參數A)會在ByteCode0行前存在,在第38行結束使用.

0: iload_1

1: iconst_3

.....

36: dadd

37: dreturn

或根據LocalVariableTable描述,Slot4 (區域變數vA)會在ByteCode5行前存在,在第38行結束使用.

.....

3: istore 4

5: iload_1

....

30: iload 4

32: i2d

33: iload 5

35: i2d

36: dadd

37: dreturn

Slot5 (區域變數vB)會在ByteCode18行前存在,在第38行結束使用.

....................

16: istore 5

18: aload_0

19: iload 4

...................

32: i2d

33: iload 5

35: i2d

36: dadd

37: dreturn

 

 

如下所示對Class內部變數的寫入是透過指令putfield,先把Class本身推入Stack,再把vA(Slot4)值推入Stack,之後透過"putfield #27",vA寫入Class中的Constantpool Index=27.

 

18: aload_0

19: iload 4

21: putfield #27;//Field gA:I

 

最後會呼叫dreturn,把返回的double數值,從目前所在的StackFrame中取出,放到呼叫端的StackFrame,回到呼叫端後,再呼叫dstore從呼叫端的StackFrame中放到呼叫端對應變數所在的Slot.

 

藉由上述說明,我們可以了解SunJava對函式參數,區域變數與返回值對應到StackFrame中的操作,接下來讓我們比對Dalvik在這部份的記憶體行為.

首先,我們可以看到Main3會具備如下的LocalsTable

 

locals :

0x0002- 0x0013 reg=0 vA I

0x000b- 0x0013 reg=1 vB I

0x0000- 0x0013 reg=6 this Lcom/testAA/testAA;

0x0000- 0x0013 reg=7 A I

0x0000- 0x0013 reg=8 B I

0x0000- 0x0013 reg=9 C I

 

在對應到呼叫端的實作,

00060a:1304 6f00 |001f: const/16 v4, #int 111// #6f

00060e:1305 de00 |0021: const/16 v5, #int 222// #de

000612:1306 4d01 |0023: const/16 v6, #int 333// #14d

000616:0800 2400 |0025: move-object/from16 v0,v36

00061a:0141 |0027: move v1, v4

00061c:0152 |0028: move v2, v5

00061e:0163 |0029: move v3, v6

000620:f840 fb00 1032 |002a: +invoke-virtual-quick{v0, v1, v2, v3}, [00fb] // vtable #00fb

000626:0b20 |002d: move-result-wide v32

 

同樣的,Dalvik中會去描述每一個暫存器在DalvikByteCode行數所存在的範圍,並不是每個區域變數都會從頭到尾被需要.同時,在寫入Class內部的變數時,則是透過指令iput-quick操作.

[f560b000] +iput-quick v0, v6, [obj+00b0]

[f561b400] +iput-quick v1, v6, [obj+00b4]

 

 

我們可以看到,Dalvik,會從右到左為順序去把參數推到StackFrame(也可以參考指令OP_INVOKE_VIRTUAL_QUICK的實作流程.),也因此,指到Class本身的this,會變成是最靠區域變數的最後一個函式參數,並以此去對應到被呼叫函式中所對應的Register編號.區域變數則由最後宣告的變數,放在Register最大值,最先宣告的區域變數對應到Register最小值.StackFrameDalvik暫存器對應到記憶體的角度來看,其實這跟SunJava把函式參數,區域變數跟返回值對應到StackFrame Slot的意義是一樣的,雖然Dalvik指令最多可以定址到256個暫存器,但這些暫存器在記憶體中儲存的位置還是對應到StackFrame,同樣的,SunJava實作中,如果今天有數量較多的區域變數或是函式參數,一樣都是對應到StackFrame中的Slot,這就如同Dalvik是對應到StackFrame中對應的暫存器位置(每個都是32-bits)在操作與記憶體成本上是可以一致的(不過我沒看過SunJava的實作SourceCode,只是從反組譯與行為來推測).

 

DalvikRegisterBased實作虛擬器的角度來看,最大的效能改善應該不是來自於記憶體或是StakeFrame的改善,而是來自於要達到同樣目的時,RegisterBased指令集可以比StackBased虛擬器指令集更加的精簡(前提是AndroidDX也要能產生出對應有效率的SunJava ByteCode -> Dalvik ByteCode轉譯,真的發揮RegisterBased虛擬器的優勢),舉如下的例子來說

 

如果要把值寫入到Class中的變數gA,SunJava ByteCode實作為

aload_0

iload 4

putfield #27;//Field gA:I

 

共三個指令,而在DalvikByteCode中只要一個指令就好

iput-quickv0, v6, [obj+00b0]

 

同樣的,如果要做區域變數vA等於函式參數A的動作"vA=A",SunJava ByteCode實作為

 

iload_1(Load A to Stack Frame)

istore_3(Store value from Stack Frame to vA)

 

2個指令,而在DalvikByteCode中只要一個指令就好

 

movev0, v4 //v0=vA and v4=A

 

我們可以看到Register-Based在指令集的效率上可以比較高,效能的好壞會在於AndroidJava實作的DX轉譯工具,這工具如何把SunJava ByteCode轉譯為DalvikByteCode,會不會有其他如本文之前所提到的,產生出沒有效率的DalvikByteCode代碼,會是影響Android執行效率的關鍵.

 

雖然DalvikByteCode最小成本為16bits,比起SunJavaByteCode最小可以為8bits,但實際上以目前ARM處理器Bus與對外部記憶體的寬度至少都是32-bits來看,存取一個16bits8bits值對ExternalMemory Bus成本基本上是一樣的.減少指令的用量,應該會比減少指令長度來的對效能有所助益.

 

 

 

 

 

RegisterMap Area

 

DEX檔案中,會有每一個Method實作時,所需用到的暫存器數量,函式參數總數,用來呼叫其它函式所用到的暫存器總數,與指令集總個數(16bits為單位),格式如下所示

 

registers : 10(包含函式參數,區域變數與中間資料搬移時所會用到的暫存器個數加總)

ins : 4(Method函式參數用到的暫存器個數=>都會加上第一個參數指到ClassObject本身)

outs : 3(Method 中呼叫其它函式所用到的暫存器總數)

insnssize : 31 16-bit code units(16bits為單位,統計這個Method所佔用的指令總長度)

 

Dalvik虛擬器一個最大的特徵就是不同於過去其他JavaVMStack-basedVM,它是一個Register-basedVM,在對應到ARM處理器的實作時,可以透過原生處理器的暫存器進行優化.同時,DalvikARM的實作中有些ARM處理器暫存器已經被Dalvik虛擬器本身,gcc編譯器或是ARMCallingConvention所使用,如下就列出有被指定使用的暫存器儘供參考.

 

 

ARM暫存器

識別名稱

說明

R0/R1/R2/R3/R9/R10

 

6ARM暫存器,是在實現Dalvik指令時,會被使用的ARM暫存器.

R4

rPC

interpretedprogram counter, used for fetching instructions(Dalvik預定用途)

R5

rFP

interpretedframe pointer, used for accessing locals and args(Dalvik預定用途)

R6

rGLUE

MterpGluepointer(Dalvik預定用途)

R7

rINST

first16-bit code unit of current instruction(Dalvik預定用途)

R8

rIBASE

interpretedinstruction base pointer, used for computed goto(Dalvik預定用途)

R11

fp

Usedby gcc (unless -fomit-frame-pointer is set)

R12

ip

ARMIntra-Procedure-call scratch register

R13

sp

ARMStack Pointer

R14

lr

ARMLink Register

R15

pc

ARMProgram Counter

 

Dalvik虛擬器,主要會讓虛擬器的暫存器與資料對應到ARMR0,R1,R2,R3,R9R126個暫存器,作為DalvikRegister-Based虛擬器執行與加速之用.可參考如下例子

 

 

.L_OP_CONST_WIDE:/* 0x18 */

/*File: armv5te/OP_CONST_WIDE.S */

/*const-wide vAA, #+HHHHhhhhBBBBbbbb */

FETCH(r0,1) @ r0<- bbbb (low)

FETCH(r1,2) @ r1<- BBBB (low middle)

FETCH(r2,3) @ r2<- hhhh (high middle)

orr r0, r0, r1, lsl #16 @ r0<- BBBBbbbb (low word)

FETCH(r3,4) @ r3<- HHHH (high)

mov r9, rINST, lsr #8 @ r9<- AA

orr r1, r2, r3, lsl #16 @ r1<- HHHHhhhh (high word)

FETCH_ADVANCE_INST(5) @ advance rPC, load rINST

add r9, rFP, r9, lsl #2 @ r9<- &fp[AA]

GET_INST_OPCODE(ip) @ extract opcode from rINST

stmia r9, {r0-r1} @ vAA<- r0/r1

GOTO_OPCODE(ip) @ jump to next instruction

 

基本的DEX檔案中會包括如下的Register對應到行數存在的範圍,

com.testAA.testAA.MainX:(I)V

0000:[0130] move v0, v3

0001:[da01 0302] mul-int/lit8 v1, v3, #int 2 //#02

0003:[f520 b000] +iput-quick v0, v2, [obj+00b0]

0005:[f521 b400] +iput-quick v1, v2, [obj+00b4]

0007:[0e00] return-void

locals :

0x0001- 0x0008 reg=0 vA I

0x0003- 0x0008 reg=1 vB I

0x0000- 0x0008 reg=2 this Lcom/testAA/testAA;

0x0000- 0x0008 reg=3 A I

 

這會表示,暫存器v0對應到區域變數vA,存在範圍從1-8ByteCode指令行數.v3對應到函式參數A,存在範圍從0-8ByteCode指令行數.

 

為了達到更有效率的系統加速,Dalvik提供了針對處理器的RegisterMap機制,主要是在Dex檔案進行Verification,同步的根據每個指令集的行為,對應到目前要執行的處理器的暫存器使用,使得最後在運作時,相關的變數,MethodClass都已經根據暫存器的使用與長度而有對應的排列,因為是在Verification中進行的,也就是說只有ODEX檔案格式中才會包含這個區塊,參考Android的文件,有加入RegisterMap資訊的ODEX檔案,可能會比原本的DEX檔案多出25%的儲存需求,也因此Androiddalvik/vm/analysis/RegisterMap.c中其實也有實作JIT版本的RegisterMap,以便搭配Trace-JIT的機制,既希望可以達到RegisterMap的目標,又希望可以達到Trace-JIT節省記憶體需求的優勢,目前已筆者手中的程式碼來看,這個機制目前尚未啟用,且也有可能Android未來會移除這部份的程式碼實作.

 

如果DalvikVM在啟動時有加上"-Xgenregmap"參數,之後在進行DEX檔案的最佳化時就會呼叫函式dvmGenerateRegisterMaps(實作在dalvik/vm/analysis/RegisterMap.c),ODEX檔案中產生RegisterMap,執行中,會先配置4MB的記憶體區塊,然後呼叫函式writeMapsAllClasses,writeMapsAllMethods,writeMapForMethod,computeRegisterMapSize產生RegisterMap資訊.

參考dalvik/vm/analysis/RegisterMap.c中函式dvmGenerateRegisterMapV的實作,

 

regWidth= (vdata->method->registersSize + 7) / 8;

regWidth長度每一個bit都會對應到每一個Method中所用到的暫存器.如果,一個函式用到四個暫存器(V0,V1,V2V3),其中V1V3屬於RegTypeUninit,在第0行會用到V1,此時的RegisterMap內容值會為b0010=0x02(bit 1會設為1).在第4行會用到V3,RegisterMap會為b1010=0x0a(bit 1bit3會設為1),目前RegisterMap會根據所用到的暫存器順序,對應到regWidth長度(inbytes)中每一個bit.設定為1bit,所對應的暫存器值,會是需要在運作時,進行初始化的暫存器.

 

此外,在設定dalvik.vm.dexopt-flags時加入m=y,也可以開啟RegisterMap最佳化參數

 

setpropdalvik.vm.dexopt-flags v=a,o=v,m=y

(或是把dalvik.vm.dexopt-flags=v=a,o=v,m=y加入到開機Property參數檔中亦可)

 

DalvikRegisterMap主要用來表示哪一個RegisterType屬於kRegTypeUninit或是大於kRegTypeMAX(實作程式碼可以參考dalvik/vm/analysis/RegisterMap.c中的函式outputTypeVector),在實際的執行上,例如以下的暫存器會屬於kRegTypeUninit.(Uninitial--在執行時期才指定對應的值)

 

1,對應到ClassObject所用到的暫存器

2,ReturnClass Object所用到的暫存器

 

舉實際的例子來說,

 

com.htc.android.worldclock.widget.NumberTableView.TableAdapter.getItem:(I)Ljava/lang/Object;

0000:invoke-static{v2}, Ljava/lang/Integer;.valueOf:(I)Ljava/lang/Integer; //method@0a4e

0003:move-result-objectv0

0004:return-objectv0

locals :

0x0000- 0x0005 reg=1 thisLcom/htc/android/worldclock/widget/NumberTableView$TableAdapter;

0x0000- 0x0005 reg=2 position I

 

由於v1暫存器符合條件,所以第0行的RegisterMapb0010=0x02

由於第四行透過v0返回Class物件參考且v1符合條件,所以第四行的RegisterMapb0011=0x03

參考RegisterMap結果如下

#1:0x00061628 getItem

0:02

4:03

 


再舉如下的例子來說明

 

com.htc.android.worldclock.widget.MyScrollView.<init>:(Landroid/content/Context;Landroid/util/AttributeSet;)V

0000:const/4 v0, #int 0 // #0

0001:invoke-direct {v1, v2, v3, v0},Lcom/htc/android/worldclock/widget/MyScrollView;.<init>:(Landroid/content/Context;Landroid/util/AttributeSet;I)V// method@0902

0004:return-void

 

locals :

0x0000- 0x0005 reg=1 this Lcom/htc/android/worldclock/widget/MyScrollView;

0x0000- 0x0005 reg=2 context Landroid/content/Context;

0x0000- 0x0005 reg=3 attrs Landroid/util/AttributeSet;

 

暫存器v1,v2,v3都指向對應的ClassObject而返回值為void,所以行數14對應的RegisterMapb1110=0x0e

參考RegisterMap結果如下

#1:0x00061564 <init>

1:0e

4:0e

 

再舉如下的例子來說明

com.htc.android.worldclock.TimeZonePicker.SimpleSearchModule.access$1300:(Lcom/htc/android/worldclock/TimeZonePicker$SimpleSearchModule;Ljava/lang/String;)Landroid/database/Cursor;

0000:invoke-direct {v1, v2},Lcom/htc/android/worldclock/TimeZonePicker$SimpleSearchModule;.coreSearch:(Ljava/lang/String;)Landroid/database/Cursor;// method@06fc

0003:move-result-object v0

0004:return-object v0

 

locals :

0x0000- 0x0005 reg=1 x0Lcom/htc/android/worldclock/TimeZonePicker$SimpleSearchModule;

0x0000- 0x0005 reg=2 x1 Ljava/lang/String;

 

v1v2暫存器符合條件,都指向對應的ClassObject,所以第0行的RegisterMapb0110=0x06

由於第四行透過v0返回Class物件參考且v1v2符合條件,所以第四行的RegisterMapb0111=0x07

參考RegisterMap結果如下

#1:0x0005fad8 access$1300

0:06

4:07

由上述舉例,我們可以知道RegisterMap可以表示每一個Class中的每一個Method所用到的暫存器有哪一個是尚未被初始化而必須要在執行階段解決ClassObject相對位址的,藉此可以在Dalvik虛擬器載入DEX檔案執行時,透過RegfisterMap知道每一個函式Method中對應的哪一個暫存器是需要在執行階段解決對應位置的,藉此讓虛擬器可以在執行階段優化處理,加速執行的效率.

 

RegisterMapOptimizedDEX檔案中的資料格式為.

(有關VirtualMethodDirectMethod分別的總數,會透過ClassDefinition中的classDataOff,對應到structDexClassDataHeader中的virtualMethodsSizedirectMethodsSize取得)

 

欄位屬性

長度

說明

Class and MethodInformation

4bytes

Number of Classes

4bytes * Number of Classes

Classes Data Offset

..........

.........................(4 bytes Class Offset)......

...........

2 bytes

Method Countfor the Class
包含DirectMethodsVirtualMethods的總數(不超過2^16)

2 bytes

two pad bytesfollow methodCount

Direct Methods

1 byte

Direct MethodsRegMapFormat

kRegMapFormatNone(addrWidth = 0;)

kRegMapFormatCompact8(addrWidth = 1;)

kRegMapFormatCompact16(addrWidth = 2;)

kRegMapFormatDifferential

1 byte

regWidth

2 byte

NumEntries(不超過2^16)

0, 1 or 2 bytes

指令Address (addrWidth為依據)

1 bytes *regWidth

Register長度

 

VirtualMethods

1 byte

VirtualMethods RegMapFormat

kRegMapFormatNone(addrWidth = 0;)

kRegMapFormatCompact8(addrWidth = 1;)

kRegMapFormatCompact16(addrWidth = 2;)

kRegMapFormatDifferential

1 byte

regWidth

2 byte

NumEntries(不超過2^16)

0, 1 or 2 bytes

指令Address (addrWidth為依據)

1 bytes *regWidth

Register長度

 

 

Dex檔案的資料型態

 

LEB128(Little-EndianBase 128)是一個可將signedunsigned32bits整數資料編碼為可變長度資料的格式,主要是參考DWARF3(除錯檔案格式)的標準而來(參考網址http://dwarfstd.org/Dwarf3Std.php).每一個LEB128編碼的資料長度可為15bytes,用以表示一個32-bits的整數值.singedLEB128來說,最後一個bytes,的最高bit會用來表示該32-bits值的正負.

 

以如下2bytesUnsigned LEB128數值為例

0x9020

Bitwisediagram of a two-byte LEB128 value

FirstByte

SecondByte

1

Bit6

Bit5

Bit4

Bit3

Bit2

Bit1

Bit0

0

Bit13

Bit12

Bit11

Bit10

Bit9

Bit8

Bit7

 

1byte 0x90 & 0x80 不為0,表示尚未到結尾

2byte 0x20 & 0x80 0,表示到結尾

所以Unsigned LEB128結果為0x90&0x7f+ (0x20&0x7f) <<7 = 0x1010

以如下3bytes Unsigned LEB128數值為例

0xd4df 04

Bitwisediagram of a three-byte LEB128 value

FirstByte

SecondByte

ThirdByte

1

Bit6

Bit5

Bit4

Bit3

Bit2

Bit1

Bit0

1

Bit13

Bit12

Bit11

Bit10

Bit9

Bit8

Bit7

0

Bit20

Bit19

Bit18

Bit17

Bit16

Bit15

Bit14

 

1byte 0xd4 & 0x80 不為0,表示尚未到結尾

2byte 0xdf & 0x80 不為0,表示尚未到結尾

3byte 0x04 & 0x80 0,表示到結尾

所以Unsigned LEB128結果為0xd4&0x7f+ (0xdf&0x7f) <<7 + (0x04&0x7f) <<14= 0x012fd4

 

以如下4bytes Unsigned LEB128數值為例

0xb884 80 01

Bitwisediagram of a fourth-byte LEB128 value

FirstByte

SecondByte

ThirdByte

FourthByte

1

Bit6

Bit5

Bit4

Bit3

Bit2

Bit1

Bit0

1

Bit13

Bit12

Bit11

Bit10

Bit9

Bit8

Bit7

1

Bit20

Bit19

Bit18

Bit17

Bit16

Bit15

Bit14

0

Bit27

Bit26

Bit25

Bit24

Bit23

Bit22

Bit21

 

1byte 0xb8 & 0x80 不為0,表示尚未到結尾

2byte 0x84& 0x80 不為0,表示尚未到結尾

3byte 0x80 & 0x80 不為0,表示尚未到結尾

4byte 0x01 & 0x80 0,表示到結尾

所以Unsigned LEB128結果為0xb8&0x7f+ (0x84&0x7f) <<7 + (0x80&0x7f) <<14 +(0x01&0x7f) <<21 = 0x200238

 

以如下1bytes Signed LEB128數值為例

0x1c

1byte 0x1c & 0x80 0,表示到結尾

所以Signed LEB128結果為((0x1c&0x7f)<< 25) >> 25 = 0x1c

 

以如下2bytes Signed LEB128數值為例

0x8e04

 

Bitwisediagram of a two-byte LEB128 value

FirstByte

SecondByte

1

Bit6

Bit5

Bit4

Bit3

Bit2

Bit1

Bit0

0

Bit13

Bit12

Bit11

Bit10

Bit9

Bit8

Bit7

1byte 0x8e & 0x80 不為0,表示尚未到結尾

2byte 0x04 & 0x80 0,表示到結尾

所以Signed LEB128結果為((0x8e&0x7f+ (0x04&0x7f) <<7)<< 18) >> 18 = 0x020e

 

以如下3bytes Signed LEB128數值為例

0xd1db 7e

 

Bitwisediagram of a three-byte LEB128 value

FirstByte

SecondByte

ThirdByte

1

Bit6

Bit5

Bit4

Bit3

Bit2

Bit1

Bit0

1

Bit13

Bit12

Bit11

Bit10

Bit9

Bit8

Bit7

0

Bit20

Bit19

Bit18

Bit17

Bit16

Bit15

Bit14

1byte 0xd1 & 0x80 不為0,表示尚未到結尾

2byte 0xdb & 0x80 不為0,表示尚未到結尾

3byte 0x7e & 0x80 0,表示到結尾

所以Signed LEB128結果為((0xd1&0x7f+ (0xdb&0x7f) <<7 + (0x7e&0x7f) <<14) <<11) >> 11= (0x1add1 << 11)>>11 = 0xffffadd1

 

LEB128的形態還包括了UnsignedLEB128p1,編碼的方式為將要編碼的32-bits值加一後,依循UnsignedLED128方式編碼為1-5bytes的結果,解碼時,則將1-5bytesUnsigned LEB128的值,解回32bits值後減一,則可還原原本32-bits值的結果.

 

參考Android文件有如下表格,可以清楚表達SingedLEB128,Unsigned LEB128UnsignedLEB128p1的差別

 

編碼後的結果

Signed LEB128

Unsigned LEB128

Unsigned LEB128p1

0x00

0

0

-1

0x01

1

1

0

0x7f

(0xffffffff) = -1

127

126

0x80 7f

(0xffffff80)= -128

16256

16255

 

其他變數型態列表如下

 

型態名稱

說明

byte

8-bitsigned int

ubyte

8-bitunsigned int

short

16-bitsigned int, little-endian

ushort

16-bitunsigned int, little-endian

int

32-bitsigned int, little-endian

uint

32-bitunsigned int, little-endian

long

64-bitsigned int, little-endian

ulong

64-bitunsigned int, little-endian

sleb128

signedLEB128

uleb128

unsignedLEB128, variable-length (see below)

uleb128p1

unsignedLEB128 plus 1,variable-length (see below)

 

 

 

MUTF-8(Modified UTF-8) Encoding

 

一個標準的UTF-8編碼的格式如下所示(也可以參考網頁)http://en.wikipedia.org/wiki/UTF-8

 

 

Bits

Lastcode point

Byte1

Byte2

Byte3

Byte4

Byte5

Byte6

7

U+007F

0xxxxxxx

 

 

 

 

 

11

U+07FF

110xxxxx

10xxxxxx

 

 

 

 

16

U+FFFF

1110xxxx

10xxxxxx

10xxxxxx

 

 

 

21

U+1FFFFF

11110xxx

10xxxxxx

10xxxxxx

10xxxxxx

 

 

26

U+3FFFFFF

111110xx

10xxxxxx

10xxxxxx

10xxxxxx

10xxxxxx

 

31

U+7FFFFFFF

1111110x

10xxxxxx

10xxxxxx

10xxxxxx

10xxxxxx

10xxxxxx

 

Dex所遵循的語言編碼機制為MUTF-8,UTF-8的差異在於

1,只有1,2,3bytes三種編碼長度,也就是說MUTF-8是專為UTF-16編碼設計的,最長支援16bitsUnicode,也因此,不像UTF-8可支援4,5,6bytes長度編碼.

2,空字元(NULL)不以0x00編碼,而是兩個bytes0xc00x80,可確保在編碼的字串中,不會有0x00出現,也可避免在例如C語言的處理時,造成字串被截斷.(C語言以0x00做為字串的結尾).

3,由於Java中的字串都是以UTF-16為單位(JNIC Code中為UTF-8),也因此在大於16bitsUnicode以上的編碼(例如:U+10000- U+10ffff ),就會需要兩個Java字元(16bits)來表示,反應到MUTF-8,就會變成兩個3bytes的編碼.(對應到UTF-8中為4bytes).

 

Android文件中也提到,MUTF-8類似於CESU-8(都是針對UTF-16設計的編碼),並支援surrogatepairs,相關資訊可以參考CESU-8Wiki (http://en.wikipedia.org/wiki/CESU-8).

 

結語

 

本文主要只涉及筆者認為值得說明的DEX檔案格式內容,Android還在持續的進版中,對應到實作的正確性,還請以各位手中的SourceCode為主.感謝!

 

 

 

 

 

 

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值