Dalvik學(xué)習(xí)之class dex odex文件結(jié)構(gòu)

文中所有內(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查看

class文件結(jié)構(gòu).jpg

常量池

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文件結(jié)構(gò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方法
  1. class轉(zhuǎn)為dex文件,工具是sdk build_tools下的dx命令。dx --dex --debug --verbose-dump--output=test.dex com/test/TestMain.class
  2. 查看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文件

dex文件

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

DexMapList

紅色框畫出來的就是每個(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è)

0x0070

看一看DexStringId的結(jié)構(gòu)體:

struct DexStringId{
    u4 stringDataOff;       //  字符串?dāng)?shù)據(jù)偏移   
}

偏移量是0x0012c120,找到它

0x0012c120

已經(jīng)找到字符串了

再找一個(gè)復(fù)雜些的,找到DexMapList中的MethodIdItem,個(gè)數(shù)0x000038DD,偏移0x00026E98,找到它

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文件有兩種存在方式:

  1. 從Apk文件中提取出來,與Apk文件存放在同一目錄下且文件后綴為odex的文件,這種多是Android ROM的系統(tǒng)程序;
  2. 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)和
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容