Dalvik 調試分析

來源:互聯網
上載者:User

前言:dalvik 是 Android 的重要組成部分 ,掌握其運行機制對理解整個Android系統有著相當之大的協助。本文將介紹GDB單步調試和Dexdump工具的使用,期望為探索dalvik打下一定的基礎。

 

1. Dalvik 之編譯

為了能夠更方便的調試dalvik,我們需要編譯一個在X86上啟動並執行dalvik和相關工具。編譯步驟如下:

  1. 首先進入到Android 源碼根目錄
  2. source build/envsetup.sh (不是網上有些文章寫的只輸入 build/envsetup.sh)
  3. lunch 2  在此之後可以看到TARGET_PRODUCT 為sim。TARGET_ARCH為x86
  4. make  或者 make dalvikvm 和 make dexdump   (make 為編譯所有程式,比較耗時,有時甚至某些模組編譯不過,如為節省時間,可使用make dalvikvm直接編譯dalvik, make dexdump直接編譯dexdump)

 

2. Gdb調試dalvik之準備工作

在用gdb啟動dalvik時,需要設定一些環境,比較繁瑣,這裡建立一個指令碼來簡化這些過程,指令碼名為grund.sh,放於Android源碼根目錄。下面為指令碼內容。

 

#!/bin/sh

base=`pwd`

root=$base/out/debug/host/linux-x86/pr/sim/system

export ANDROID_ROOT=$root

bootpath=$root/framework

export BOOTCLASSPATH=$bootpath/core.jar:$bootpath/ext.jar:$bootpath/framework.jar:$bootpath/android.police.jar

export ANDROID_DATA=/tmp/dalvik_test

mkdir -p $ANDROID_DATA/dalvik-cache

exec gdb $root/bin/dalvikvm 

 

3. Gdb 調試 dalvik
  1. 準備一個簡單的java 程式,如hello.java,編譯後將hello.jar拷貝至Android 源碼根目錄。 (hello.java與makefile見附錄)
  2. 進入到Android 源碼根目錄
  3. ./grund.sh  (執行上述指令碼,之後會看到gdb提示符)
  4. 在gdb提示符後輸入 “set args –cp hello.jar hello”
  5. 這個時候就可以設定斷點,單步跟蹤了!如有對gdb不熟的同學,請google之。 main()函數為入口函數,先在main.c 212行設定斷點(在gdb提示符後輸入 “b 212”)
  6. 在gdb提示符後輸入”r”, OK,我們會看到dalvik被gdb啟動執行,然後停於212行,執行JNI_CreateJavaVM函數前先看看gDvm的內容(輸入p gDvm)。 然後執行JNI_CreateJavaVM函數(輸入“n”),再看看gDvm的內容。對比執行前後的變化,可大概知道JNI_CreateJavaVM函數所做的事情。
  7. main.c 249 行代碼用於載入hello.class,在249行設定斷點。在此中斷後,看下slashClass的內容(輸入“p slashClass“),slashClass正是”hello”字串。接下來單步進入執行之(輸入”s”),然後查看下函數調用棧(輸入”bt”)。可知現在正在執行的是jni.c 中的FindClass函數。通過此方法,可知函數指標指向的是何函數。
  8. main.c 255行代碼用於取得hello.java 中main函數編譯後的位元組碼。類似於g步驟,可知此時執行的函數為jni.c中的GetStaticMethodID函數
  9. main.c 273行代碼執行main函數編譯後的位元組碼,類似於g步驟,可知此時執行的函數為jni.c 中的2681行。此處為宏定義,不容易找到。但通過gdb調試,可以準確的定位。如此時繼續運行程式,”hello world” 就會出現在我們的眼前!

 

此處只做簡單分析,為拋磚引玉用,各位同學可以藉此探究自己感興趣的內容。在接下來的文章中會詳細分析class載入與位元組碼的執行。

 

4.  dexdump查看jar檔案

 

Dexdump 可執行檔放於out目錄下, 可使用”find out/ -name dexdump”命令來找到dexdump。

“dexdump –f  hello.jar” 命令可列印jar檔案的頭部資訊。

“dexdump –d hello.jar” 可列印所編譯的位元組碼。

 

頭部資訊如下:

Opened 'hello.jar', DEX version '035'

DEX file header:

magic               : 'dex

035'

checksum            : f2f85a9c

signature           : 0404...7831

file_size           : 740

header_size         : 112

link_size           : 0

link_off            : 0 (0x000000)

string_ids_size     : 14

string_ids_off      : 112 (0x000070)

type_ids_size       : 7

type_ids_off        : 168 (0x0000a8)

field_ids_size      : 1

field_ids_off       : 232 (0x0000e8)

method_ids_size     : 4

method_ids_off      : 240 (0x0000f0)

class_defs_size     : 1

class_defs_off      : 272 (0x000110)

data_size           : 436

data_off            : 304 (0x000130)

 

 

其中string_ids,type_ids,field_ids,method_ids, class_defs皆可理解為索引。通過這些索引,可以查到真正的資料存放位置。Data_off為真正的資料存放位置。

 

 

位元組碼如下:

    #1              : (in Lhello;)

      name          : 'main'

      type          : '([Ljava/lang/String;)V'

      access        : 0x0009 (PUBLIC STATIC)

      code          -

      registers     : 3

      ins           : 1

      outs          : 2

      insns size    : 10 16-bit code units

000148:                                        |[000148] hello.main:([Ljava/lang/String;)V

000158: 6200 0000                              |0000: sget-object v0, Ljava/lang/System;.out:Ljava/io/PrintStream; // field@0000

00015c: 1a01 0900                              |0002: const-string v1, "hello world" // string@0009

000160: 6e20 0200 1000                         |0004: invoke-virtual {v0, v1}, Ljava/io/PrintStream;.println:(Ljava/lang/String;)V // method@0002

000166: 2a00 0000 0000                         |0007: goto/32 #00000000

      catches       : (none)

      positions     :

        0x0000 line=4

        0x0007 line=5

      locals        :

 

  Virtual methods   -

  source_file_idx   : 10 (hello.java)

 

 

附錄:

Hello.java :

public class hello{

      public static void main(String args[]) {

                  System.out.println("hello world");

                  while(true) {}

      }

}

 

Makefile:

ANDROID_SRC_DIR := /android/platform_sim

 

android_dir_dx = $(ANDROID_SRC_DIR)/out/host/linux-x86/bin/dx

 

all:

      javac hello.java

      $(android_dir_dx) --dex --output=hello.jar hello.class

clean:

      @rm *.jar *.class

 

Java 原始碼經過編譯後會產生尾碼為class的檔案,也即位元組碼檔案。然後在Android中使用dx工具將其轉換為尾碼為jar 的dex類型檔案。Dalvik 虛擬機器負責解釋並執行編譯後的位元組碼。在解釋執行位元組碼之前,當然要讀取檔案,分析檔案的內容,得到位元組碼,然後才能解釋執行之。在整個的載入過程中,最為重要的就是對Class的載入 – Class包含Method,Method 又包含code。通過對Class的載入,我們即可獲得所需執行的位元組碼。

 

本文從dexfile 檔案分析及Class載入中的資料結構入手,結合主要流程,對整個載入過程進行分析,期望對大家有所協助。

 

 

1. DexFile 在記憶體中的映射

 

在Android 系統中,java源檔案會被編譯為尾碼為jar的dex類型檔案,在代碼中稱為dexfile。在載入Class之前,必先讀取相應的jar檔案。通常我們使用read函數來讀取檔案中的內容。但在Dalvik中使用mmap函數,和read不同,mmap函數會將dex檔案對應到記憶體中,這樣通過普通的記憶體讀取操作即可訪問dex file 中的內容。

 

Dexfile 的檔案格式如所示,主要有三部分組成:頭部,索引,資料。通過頭部可知索引的位置和數目,可知資料區的起始位置。其中classDefsOff指定了ClassDef在檔案的起始位置,dataOff指定了資料在檔案的起始位置,ClassDef即可理解為Class的索引。通過讀取ClassDef可獲知Class的基本資料,其中classDataOff指定了Class資料在資料區的位置。

 

在將dexfile檔案對應到記憶體後,即會調用dexFileParse函數對其分析,分析的結果存放於名為DexFile 的資料結構中。DexFile中的baseAddr指向映射區的起始位置,pClassDefs指向ClassDefs(即class索引)的起始位置。由於在尋找class時,都是使用class的名字進行尋找,為了加快尋找速度,建立了一個hash表。在hash表中對class名字進行hash,並產生index。這些操作都是在對檔案解析時所完成的,這樣雖然在載入過程中比較耗時,但是在運行過程中確可節省大量尋找時間。

 

 

 

 

 

 

2. ClassObject -  Class在載入後的表現形式

 

在對檔案解析完成後就要載入Class的具體內容了!在Dalvik中,由ClassObject 這個資料結構負責存放載入的資訊。如所示,載入過程會在記憶體中alloc幾個地區,分別存放directMethods, virtualMethods, sfields, ifields。這些資訊正是從dex 檔案的資料區中讀取。首先會讀取Class的詳細資料,從中獲知directMethod, virtualMethod, sfield, ifield等的資訊,然後再讀取。為載入完成後的示意。 這裡並未介紹載入的每個細節,感興趣的同學可通過此二圖自行分析。

還請大家注意的是在ClassObject結構中有個名為super的成員。通過super成員來指向它的超類。

 

 

 

3. findClassNoInit – 負責載入Class並產生相應ClassObject的函數。

 

第二節介紹了載入後的資料結構,本節會分析負責載入工作的函數- findClassNoInit。 請注意在擷取Class索引時,會分為基本類庫檔案和使用者類檔案兩種情況。在Dalvik分析之準備篇中,grund.sh中有一語句   “export BOOTCLASSPATH=$bootpath/core.jar:$bootpath/ext.jar:$bootpath/framework.jar:$bootpath/android.police.jar”
。 這條語句指定了Dalvik所需的基本庫檔案,如果沒有此語句,Dalvik在啟動過程中就會報錯退出。

 

LoadClassFromDex 函數先會讀取Class的具體資料(從ClassDataoff處),然後按圖索驥,分別載入 directMethod, virtualMethod,ifield,sfield。

 

作為業界TOP公司出品的東東,當然要關注執行效率了^_^。首先,在載入後我們要將其緩衝起來,以便以後使用方便。其次,在尋找過程中,如果我們順序尋找的話,當然是很慢的拉。這當然是俺們senior engineer所不能容忍的,所以gDvm.loadedClasses這個Hash表就隆重出場了。什麼,這位同學說沒有幾個Class,至於這麼大動幹戈麼。讓我們看看,通過在準備篇中介紹的方法,我們在main.c 249行設定斷點,這個時候基本庫已載入完畢。當程式停下時,我們看看gDvm的值,可以看到numLoadedClasses這個成員的值為212
!也即意味著這個時候我們什麼也沒做,使用者類也未載入,Dalvik 已載入的Class數目已達到212。

 

dvmLinkClass, 好長,但是其最終好像會再次調用findClassNoInit。嗯,也是可以理解的嘛。如果一個子類需要調用超類的函數,那它當然要先載入超類了,可能的話,甚至會載入超類的超類^_^。  

 

 

 

 

眼見為虛,實踐出真知,拿gdb調試下。

在findClassNoInit函數處設定斷點(在gdb提示符後輸入”b findClassNoInit”),在gdb提示符後連續幾次執行”c”和”bt”。可出現如下資訊,可以看到在函數調用棧上可以多次看到findClassNoInit的身影。

 

(gdb) bt

#0  findClassNoInit (descriptor=0xfef4c7f4 "??????%", loader=0x0, pDvmDex=0x0)

    at dalvik/vm/oo/Class.c:1373

#1  0xf6fc4d53 in dvmFindClassNoInit (descriptor=0xf5046a63 "Ljava/lang/Object;", loader=0x0)

    at dalvik/vm/oo/Class.c:1194

#2  0xf6fc6c0a in dvmResolveClass (referrer=0xf5837400, classIdx=290,

    fromUnverifiedConstant=false) at dalvik/vm/oo/Resolve.c:94

#3  0xf6fc3476 in dvmLinkClass (clazz=0xf5837400, classesResolved=false)

    at dalvik/vm/oo/Class.c:2537

#4  0xf6fc1b67 in findClassNoInit (descriptor=0xf6ff0df6 "Ljava/lang/Class;", loader=0x0,

    pDvmDex=0xa04c720) at dalvik/vm/oo/Class.c:1489

 

 

現在從另一個角度去觀察。在class.c 2575行設定斷點,然後等待程式停下。看下clazz的內容。

(gdb) p clazz->super->descriptor

$6 = 0xf5046a63 "Ljava/lang/Object;"

(gdb) p clazz->descriptor

$7 = 0xf5046121 "Ljava/lang/Class;"

 

 

4. 基本類庫檔案的載入

 

先在findClassNoInit函數處設定斷點,然後運行程式,等待程式的停下。

 

(gdb) b findClassNoInit

Breakpoint 2 at 0xf6fc13e0: file dalvik/vm/oo/Class.c, line 1373.

(gdb) c

Continuing.

 

看看誰是第一個載入的Class,及其調用關係。

 

(gdb) bt

#0  findClassNoInit (descriptor=0x0, loader=0x0, pDvmDex=0x0) at dalvik/vm/oo/Class.c:1373

#1  0xf6fc32a1 in dvmLinkClass (clazz=0xf5837350, classesResolved=false)

    at dalvik/vm/oo/Class.c:2491

#2  0xf6fc1b67 in findClassNoInit (descriptor=0xf6ff1ded "Ljava/lang/Thread;", loader=0x0,

    pDvmDex=0xa04c720) at dalvik/vm/oo/Class.c:1489

#3  0xf6f92692 in dvmThreadObjStartup () at dalvik/vm/Thread.c:328

#4  0xf6f800e6 in dvmStartup (argc=2, argv=0xa041190, ignoreUnrecognized=false, pEnv=0xa0411a0)

    at dalvik/vm/Init.c:1155

#5  0xf6f8b8e3 in JNI_CreateJavaVM (p_vm=0xf6ff0df6, p_env=0xf6ff0df6, vm_args=0xfef4d0b0)

    at dalvik/vm/Jni.c:4198

#6  0x08048893 in main (argc=3, argv=0xfef4d168) at dalvik/dalvikvm/Main.c:212

 

函數調用順序清晰可見:main -> JNI_CreateJavaVM-> dvmStartup-> dvmThreadObjStartup-> dvmFindSystemClassNoInit-> findClassNoInit 觀察仔細的同學可能會問在調用棧中沒有看到dvmFindSystemClassNoInit啊,為何你這樣寫啊?我估計編譯器將其作為inline最佳化了,導致gdb看不到有dvmFindSystemClassNoInit的棧。從回溯棧中也可清晰的看到

 

 

5. 使用者類檔案的載入

使用者類檔案的載入頗為曲折,它會先載入一個Class。然後這個Class去負責使用者類檔案的載入,當然了,這個Class又會通過JNI的方式去曲折調用到findClassNoInit。這個就留給大家自己分析了, 其實樓主自己也不是很明白為什麼要這麼大費周折^_^。

 

 

 

 

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.