前言:dalvik 是 Android 的重要組成部分 ,掌握其運行機制對理解整個Android系統有著相當之大的協助。本文將介紹GDB單步調試和Dexdump工具的使用,期望為探索dalvik打下一定的基礎。
1. Dalvik 之編譯
為了能夠更方便的調試dalvik,我們需要編譯一個在X86上啟動並執行dalvik和相關工具。編譯步驟如下:
- 首先進入到Android 源碼根目錄
- source build/envsetup.sh (不是網上有些文章寫的只輸入 build/envsetup.sh)
- lunch 2 在此之後可以看到TARGET_PRODUCT 為sim。TARGET_ARCH為x86
- 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
- 準備一個簡單的java 程式,如hello.java,編譯後將hello.jar拷貝至Android 源碼根目錄。 (hello.java與makefile見附錄)
- 進入到Android 源碼根目錄
- ./grund.sh (執行上述指令碼,之後會看到gdb提示符)
- 在gdb提示符後輸入 “set args –cp hello.jar hello”
- 這個時候就可以設定斷點,單步跟蹤了!如有對gdb不熟的同學,請google之。 main()函數為入口函數,先在main.c 212行設定斷點(在gdb提示符後輸入 “b 212”)
- 在gdb提示符後輸入”r”, OK,我們會看到dalvik被gdb啟動執行,然後停於212行,執行JNI_CreateJavaVM函數前先看看gDvm的內容(輸入p gDvm)。 然後執行JNI_CreateJavaVM函數(輸入“n”),再看看gDvm的內容。對比執行前後的變化,可大概知道JNI_CreateJavaVM函數所做的事情。
- main.c 249 行代碼用於載入hello.class,在249行設定斷點。在此中斷後,看下slashClass的內容(輸入“p slashClass“),slashClass正是”hello”字串。接下來單步進入執行之(輸入”s”),然後查看下函數調用棧(輸入”bt”)。可知現在正在執行的是jni.c 中的FindClass函數。通過此方法,可知函數指標指向的是何函數。
- main.c 255行代碼用於取得hello.java 中main函數編譯後的位元組碼。類似於g步驟,可知此時執行的函數為jni.c中的GetStaticMethodID函數
- 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。這個就留給大家自己分析了, 其實樓主自己也不是很明白為什麼要這麼大費周折^_^。