Dalvik分析(一)
近來 Android 十分熱門, Google 的大動(dòng)作,撼動(dòng)整個(gè)業(yè)界。雖已震天撼地,和過去 MS 或 Apple 所興之波瀾相較,還是有些差距。身為一個(gè)技術(shù)研究者,新聞性似乎不是這麼重要,倒底葫蘆裡賣的是什麼藥,才是吾輩所想知道。小弟最近獲邀加入某團(tuán)體,而擇 主題研究,企圖改善國內(nèi) Open Source 的風(fēng)氣和態(tài)度。於是著手分析 Dalvik 程式碼。
Dalvik 的成分
Dalvik 是一個(gè) VM (Virtual Machine) ,相當(dāng)於 Java 的 JVM 、 .Net 的 CLI 和 Python 、 Perl 、 Ruby 的 Interpreter 。Dalvik 定義自己的 bytecode ,為 VM 的指令,相當(dāng)於 CPU 的機(jī)械碼。這些 bytecode 指令由 VM 進(jìn)行解釋、執(zhí)行。 Android 利用 Dalvik VM ,達(dá)到跨平臺(tái)的目的。Dalvik 的程式碼目錄裡,又豈只 VM 而已,尚有一核心 library ,包括 Java Source Code 和 implement JNI 的 native method code。然,前述皆是 library ,非 VM 本體,不在本文討論之列。本文就 VM 本體進(jìn)行分析和討論,並示範(fàn) trace code 的方法,希望有志學(xué)習(xí)者能因而獲益一、二。
Dalvik 目錄
Dalvik 目錄下,會(huì)看到二十來個(gè)檔案和目錄,有文件、Makefile 、library 、工具等。和 VM 有關(guān)者,唯
- dalvikvm/
- vm/
二者。dalvikvm/ 目錄實(shí)為 VM 的 main function ,透過呼叫 vm/ 目錄下所定義的 function ,初始並啟動(dòng) VM ,以執(zhí)行指定程式。而 VM 的真正本體則在 vm/ 目錄之下。概欲研究 Dalvik 程式碼者,因集中於此二目錄,勿被其它目錄內(nèi)的程式碼所迷惑。然而, docs/ 目錄下的文件,有許多有參考價(jià)值。有志者,或可一讀,為預(yù)習(xí)功課。研究過程中,遇不解名詞時(shí),請(qǐng)隨時(shí)透過 search engine 進(jìn)行查詢。
開始分析
分析任何系統(tǒng)之初,首要為找到程式的進(jìn)入點(diǎn)。以 C 的 application 而言,就是 main() function ,在 dalvikvm/Main.c 裡。接著著手進(jìn)行分析。然而,main() function 一開始就有滾滾江水、源源不絕之勢(shì),十之八九,可能從第一行開始看,順著每一個(gè)呼叫追蹤而入,覓技微末節(jié)而不棄,直至迷途於茫茫程式碼之間,終至意志損耗 殆盡而投降。正是見樹而不見林之害,分析之初唯恐避之不急。因此,謹(jǐn)記研究初衷為 VM 本身,而非相關(guān)初始設(shè)定。需先觀測(cè) main() function ,再鎖定可疑的主體,再進(jìn)行分析,以了解大架構(gòu)為先,以達(dá)提綱挈領(lǐng)之效,不至於迷罔。
從 main() function 裡,我們大概能本能性的猜測(cè)到
- JNI_CreateJavaVM()
- FindClass()
- GetStaticMethodID()
- CallStaticVoidMethod()
這幾行,應(yīng)該和 VM 本體關(guān)係較大。透過分析這幾個(gè) function ,應(yīng)該能了解程式碼的架構(gòu)
- 主要 function
- function 和 .c file 間的關(guān)係
- 每個(gè) .c file 主要 implement 哪一部分的功能
- 每個(gè)子目錄又個(gè)別負(fù)責(zé)什麼功能。
起初應(yīng)以瞭解上面所提之事,熟悉程式碼的檔案和目錄結(jié)構(gòu)而主,不可一頭栽入程式細(xì)節(jié)之中。
從上面幾行,幾可猜出, Dalvik 的執(zhí)行,必需先建立、初始化一個(gè) VM,然後透過 FindClass() 載入、並取得命令列指定的 class ,並透過 GetStaticMethodID() 取得 class 裡的 static method,最後執(zhí)行該 method 。我們知道, Java 程式的執(zhí)行,是透過指定一個(gè) class , VM 假設(shè)該 class 有一 static method 名為 main ,以執(zhí)行之。很快的,我們了解到, Dalvik 主要流程大概是上述的過程。
JNI_CreateJavaVM()
JNI_CreateJavaVM() 故名思義,應(yīng)該就是建立、初始化一個(gè) Dalvik VM ,用以執(zhí)行指定的程式碼。我們必需先知道, Dalvik 幾乎是一個(gè) Java VM ,因此有許多名詞是 直接延用 Java 的用法,如 JNI 即是 Java Native Interface。作為 Java 和 native code 的介面。因此,此處 JNI 料想和 JNI 有關(guān)。有可能是提供給 Native code 呼叫的 function ,或用以呼叫 native code 。此處是 create VM ,因此可假設(shè)為提供 native code 呼叫。
接著,我們追入 JNI_CreateJavaVM() 看其葫蘆裡賣的是黑藥、白藥。這裡,請(qǐng)善用 ctags 搭配 editor,否則就用 grep 整個(gè)目錄才能找到 JNI_CreateJavaVM() 的位置。以 emacs 而言,將游標(biāo)移至該 function 的位置,按 meta+. 或 alt+. 即觸發(fā) emacs 查詢 ctags db 裡的相關(guān)資料,開啟定義該 function 的檔案,並移至該 function 的開頭。如: 在 JNI_CreateJavaVM() 上,按 alt+. ,emacs 即跳至 JNI_CreateJavaVM() function 首行。 JNI_CreateJavaVM() 是在 vm/Jni.c 檔案裡。因此, Jni.c 應(yīng)該是 VM 提供的主要介面。
要確定 vm/Jni.c 的主要功能,最好是檢示一次裡面的 function 。若觀察 vm/ 目錄,或許發(fā)現(xiàn) Jni.h 。大部分 programmer 在 coding 時(shí),都有類似的習(xí)慣。例如:將 Jni.c 所定義的 function ,透過 Jni.h export 給其它 module 使用。這是一種常見的習(xí)慣,若一個(gè) programmer 的 coding 習(xí)慣離一般習(xí)慣太遠(yuǎn),或變化甚大,甚至於毫無章法,該程式品質(zhì)必然低落,也無研究?jī)r(jià)值。在研究它 人程式碼時(shí),需利用常用習(xí)慣。用之則有如神助,棄之則如牛犛田。一般 module ,會(huì)將其主要功能,以一組 function 和 data structure 加以定義,並 export 。然程式碼則充滿各式的實(shí)作細(xì)節(jié),一一檢視何者為 export 的介面,何者為非,得花上大半時(shí)間。最好的方式是直接看 header file, .h file。header file 一般只有 export 的部分,因此沒有太多雜訊。檢視 module 的 header file 內(nèi)容,大抵能了解一個(gè) module 的功用。
然而,我們並沒有發(fā)現(xiàn) Jni.h ,這時(shí)只有直接檢視 Jni.c 的內(nèi)容。很幸運(yùn)的, Dalvik 的 developer 們有良好習(xí)慣,將 local 使用的 function 都宣告為 static。這是一個(gè)常見的良好的習(xí)慣,因些我們可以很快略過 static function ,得知有哪些重要的 function 。為了更加快速,可利用 editor 的 folding 功能,讓 editor 收疊程式的內(nèi)容,只 show 出最外層的 function 定義。以 emacs 為例,按 ctrl+c @ ctrl+alt+h 能達(dá)到前述功能。按 ctrl+c @ ctrl+alt+s 則顯示所有細(xì)節(jié)。
在 Jni.c 我們看到
- dvmJniStartup()
- dvmJniShutdown()
- dvmGetJNIEnvForThread()
- dvmCreateJNIEnv()
- dvmDestroyJNIEnv()
- dvmGetJNIRefType()
- dvmReleaseJniMonitors()
- dvmLateEnableCheckedJni()
- JNI_GetDefaultJavaVMInitArgs()
- JNI_GetCreatedJavaVMs()
- JNI_CreateJavaVM()
裡有數(shù)十個(gè) static function 被我們略過,只需注意其中這幾個(gè) global function 。一個(gè) module 在介面定義了許多功能,其中只有一部分是重要的,其它則是補(bǔ)助性質(zhì)。其中
- dvmJniStartup()
- dvmJniShutdown()
看起來是使用這個(gè) module 必需進(jìn)行初始化的 function 。於是我們 grep 一下目錄,發(fā)現(xiàn)在 dvmStartup() (in vm/Init.c) 裡呼叫了這個(gè)程式。dvmStartup() 也呼叫了許多其它 *Startup() function ,看來所有子系統(tǒng)的 initialization function 都在這裡呼叫。而 dvmStartup() 則被 JNI_CreateJavaVM() 所呼叫。因而我們可以猜測(cè),所有的 sub-module 的 initialize 都透過 JNI_CreateJavaVM() 呼叫 dvmStartup() 而進(jìn)行。包括 Jni.c 本身。
gDvm
在 main() function 裡,我們知道 vm 、 env 兩個(gè)未 local variable 是 pointer,並且未初始化 。 main() function 傳兩者的reference (address) 於 JNI_CreateJavaVM() ,此意乃 JNI_CreateJavaVm() 需初始化兩個(gè) pointer 的內(nèi)容。大抵上,developer coding 之時(shí),不會(huì)將未初始化的 variable 的 reference 傳入其它 function ,除非該 function 需負(fù)責(zé)設(shè)定 variable 的內(nèi)容。從變數(shù)名,吾者已能臆測(cè) vm 應(yīng)該是保留 JNI_CreateJavaVM() 所建立的 VM object 的位址。從 main() function 中,透過 env 呼叫 CallStaticVoidMethod() 、FindClass() 等等 function ,想必是 VM 提供之控制介面。
為確定猜想,必然要檢示 JNI_CreateJavaVM() 內(nèi)容。pVM,即 main() function 的 vm ,是在此 function 裡 allocate memory ,並設(shè)定其內(nèi)容。而 pEnv,即 main() function 的 env,則呼叫 dvmCreateJNIEnv() 而得。檢視 dvmCreateJNIEnv() 和 JNI_CreateJavaVM(),發(fā)現(xiàn) pVM 和 pEnv 之間並沒有資料的關(guān)聯(lián),也就是 pVM 和 pEnv 並沒有相互指向?qū)Ψ?#xff0c;應(yīng)該兩者無關(guān)。但在 main() function 又有暗示兩者之間的關(guān)聯(lián)。於是在 JNI_CreateJavaVM() 裡,可以發(fā)現(xiàn) gDvm 這個(gè) global variable 。這個(gè)變數(shù)似乎是由 JNI_CreateJavaVM() 所初始化。 Global variable 通常意指系統(tǒng)有些 function 是透過這些 variable ,相互分享資料。難道 pVM 和 pEnv 的關(guān)聯(lián),即透過 gDvm 建立?
pVM 和 pEnv 是透過 casting 才 assign 給 main() function 的 vm 和 env
001 *p_env = (JNIEnv*) pEnv;
002 *p_vm = (JavaVM*) pVM;
這通常意謂著 pEnv 開頭第一個(gè)欄位是 JNIEnv 型別,而 pVM 的頭一個(gè)欄位是 JavaVM 型別。這是 C programmer 常用的技巧,隱藏實(shí)作所需的資料,將必需 export 的資料,放在第一個(gè)欄位裡透過 casting export 出去,並避免 user 看到其它資料。至此,讀者或會(huì)發(fā)現(xiàn),trace 別人的 code 往往必需了解作者的習(xí)慣和技巧。所幸好用的習(xí)慣的技術(shù),會(huì)被大多數(shù)人接受、採用。也因此,要想 trace 別人的 code ,自己本身也需要有一定的 coding 的技術(shù),並透過閱讀別人的 code ,累積一些大多數(shù)常用的技巧和習(xí)慣。幾年前有一本書,名為 Code Reading (by by Diomidis Spinellis)。這本書應(yīng)該加入這些材料才對(duì)。
為確定 pVM 和 pEnv 的頭一個(gè)欄位,我們檢識(shí) JniInternal.h 裡的 JavaVMExt 和 JNIEnvExt,發(fā)覺分別是 JNIInvokeInterface* 和 JNINativeInterface* 型別。這是怎麼回事? 於是回頭檢查 JavaVM 和 JNIEnv ,發(fā)覺
001 #if defined(__cplusplus)
002 typedef _JNIEnv JNIEnv;
003 typedef _JavaVM JavaVM;
004 #else
005 typedef const struct JNINativeInterface* JNIEnv;
006 typedef const struct JNIInvokeInterface* JavaVM;
007 #endif
很明顯,我們是使用 C ,非 C++ ,所以是下半部。沒錯(cuò),正和前面所發(fā)現(xiàn)的相同。於是確定 pVM 和 pEnv 的第一個(gè)欄位 export 了 main() function 裡的 vm 和 env 所提供的內(nèi)容。
再仔細(xì)檢視 JNI_CreateJavaVM() 和 dvmCreateJNIEnv() ,會(huì)發(fā)覺頭個(gè)欄位的內(nèi)容分別定義在 gInvokeInterface 和 gNativeInterface 。而兩者的內(nèi)容則是列了一堆 function pointer ,指向許多 function 。完全證實(shí)我們之前所猜測(cè)的。
至此,讀者應(yīng)已發(fā)現(xiàn)。我在 trace code 時(shí),並不是一行一行的看。而是大膽假設(shè)、然後小心求證、再假設(shè)、再求證,跳躍式 trace source code 。試想,programmer 在寫程式時(shí)也不是線性的,一行一行,一口氣從第一行寫到最後一行。大抵上也是一次 implement 一個(gè)概念,然後一個(gè)概念接著一個(gè)概念,慢慢將 code 填上去。一個(gè) function 或許來來回回修了很多次。
gInvokeInterface 和 gInvokeInterface 所列之 function ,似乎全為 Jni.c 裡面的 static function 。我們?cè)?Jni.c 裡 search gDvm ,看看這些 function 是否有 access gDvm 及其用法,發(fā)覺使用的不多。再仔細(xì)看看 JavaVMExt 和 JNIEnvExt 的定義,發(fā)覺沒有存放什麼狀況。反倒是 gDvm 的型別 DvmGlobals 存放了許多資料,似乎 VM 的狀態(tài)大多存放在此。如果一個(gè)系統(tǒng)將狀態(tài)存放在 global variables,那意謂著該系統(tǒng)在一個(gè) process 只能執(zhí)行一份??磥?Dalvik 目前在一個(gè) process 裡,只允許一個(gè) VM 。我在寫程式極力避免這種情形,以保有單一 process 執(zhí)行多份相同的 instance 的可能性。然而, global variable 倒是偷懶的好方法,如果你很確定不想保有此彈性。
http://heaven.branda.to/~thinker/GinGin_CGI.py/show_id_doc/382
總結(jié)
以上是生活随笔為你收集整理的Dalvik分析(一)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Dalvik class加载的处理
- 下一篇: Dalvik分析(二)