Thursday, October 29, 2009

Dalvik 程式碼分析與示範(一)

Dalvik 程式碼分析與示範(一)
by thinker
2 Columns
關鍵字:
Android
近來 Android 十分熱門, Google 的大動作,撼動整個業界。雖已震天撼地,和過去 MS 或 Apple 所興之波瀾相較,還是有些差距。身為一個技術研究者,新聞性似乎不是這麼重要,倒底葫蘆裡賣的是什麼藥,才是吾輩所想知道。小弟最近獲邀加入某團體,而擇主題研究,企圖改善國內 Open Source 的風氣和態度。於是著手分析 Dalvik 程式碼。

Dalvik 的成分

Dalvik 是一個 VM (Virtual Machine) ,相當於 Java 的 JVM 、 .Net 的 CLI 和 Python 、 Perl 、 Ruby 的 Interpreter 。Dalvik 定義自己的 bytecode ,為 VM 的指令,相當於 CPU 的機械碼。這些 bytecode 指令由 VM 進行解釋、執行。 Android 利用 Dalvik VM ,達到跨平台的目的。Dalvik 的程式碼目錄裡,又豈只 VM 而已,尚有一核心 library ,包括 Java Source Code 和 implement JNI 的 native method code。然,前述皆是 library ,非 VM 本體,不在本文討論之列。本文就 VM 本體進行分析和討論,並示範 trace code 的方法,希望有志學習者能因而獲益一、二。

Dalvik 目錄

Dalvik 目錄下,會看到二十來個檔案和目錄,有文件、Makefile 、library 、工具等。和 VM 有關者,唯

* dalvikvm/
* vm/

二者。dalvikvm/ 目錄實為 VM 的 main function ,透過呼叫 vm/ 目錄下所定義的 function ,初始並啟動 VM ,以執行指定程式。而 VM 的真正本體則在 vm/ 目錄之下。概欲研究 Dalvik 程式碼者,因集中於此二目錄,勿被其它目錄內的程式碼所迷惑。然而, docs/ 目錄下的文件,有許多有參考價值。有志者,或可一讀,為預習功課。研究過程中,遇不解名詞時,請隨時透過 search engine 進行查詢。

開始分析

分析任何系統之初,首要為找到程式的進入點。以 C 的 application 而言,就是 main() function ,在 dalvikvm/Main.c 裡。接著著手進行分析。然而,main() function 一開始就有滾滾江水、源源不絕之勢,十之八九,可能從第一行開始看,順著每一個呼叫追蹤而入,覓技微末節而不棄,直至迷途於茫茫程式碼之間,終至意志損耗殆盡而投降。正是見樹而不見林之害,分析之初唯恐避之不急。因此,謹記研究初衷為 VM 本身,而非相關初始設定。需先觀測 main() function ,再鎖定可疑的主體,再進行分析,以了解大架構為先,以達提綱挈領之效,不至於迷罔。

從 main() function 裡,我們大概能本能性的猜測到

* JNI_CreateJavaVM()
* FindClass()
* GetStaticMethodID()
* CallStaticVoidMethod()

這幾行,應該和 VM 本體關係較大。透過分析這幾個 function ,應該能了解程式碼的架構

* 主要 function
* function 和 .c file 間的關係
* 每個 .c file 主要 implement 哪一部分的功能
* 每個子目錄又個別負責什麼功能。

起初應以瞭解上面所提之事,熟悉程式碼的檔案和目錄結構而主,不可一頭栽入程式細節之中。

從上面幾行,幾可猜出, Dalvik 的執行,必需先建立、初始化一個 VM,然後透過 FindClass() 載入、並取得命令列指定的 class ,並透過 GetStaticMethodID() 取得 class 裡的 static method,最後執行該 method 。我們知道, Java 程式的執行,是透過指定一個 class , VM 假設該 class 有一 static method 名為 main ,以執行之。很快的,我們了解到, Dalvik 主要流程大概是上述的過程。

JNI_CreateJavaVM()
JNI_CreateJavaVM() 故名思義,應該就是建立、初始化一個 Dalvik VM ,用以執行指定的程式碼。我們必需先知道, Dalvik 幾乎是一個 Java VM ,因此有許多名詞是直接延用 Java 的用法,如 JNI 即是 Java Native Interface。作為 Java 和 native code 的介面。因此,此處 JNI 料想和 JNI 有關。有可能是提供給 Native code 呼叫的 function ,或用以呼叫 native code 。此處是 create VM ,因此可假設為提供 native code 呼叫。

接著,我們追入 JNI_CreateJavaVM() 看其葫蘆裡賣的是黑藥、白藥。這裡,請善用 ctags 搭配 editor,否則就用 grep 整個目錄才能找到 JNI_CreateJavaVM() 的位置。以 emacs 而言,將游標移至該 function 的位置,按 meta+. 或 alt+. 即觸發 emacs 查詢 ctags db 裡的相關資料,開啟定義該 function 的檔案,並移至該 function 的開頭。如: 在 JNI_CreateJavaVM() 上,按 alt+. ,emacs 即跳至 JNI_CreateJavaVM() function 首行。 JNI_CreateJavaVM() 是在 vm/Jni.c 檔案裡。因此, Jni.c 應該是 VM 提供的主要介面。

要確定 vm/Jni.c 的主要功能,最好是檢示一次裡面的 function 。若觀察 vm/ 目錄,或許發現 Jni.h 。大部分 programmer 在 coding 時,都有類似的習慣。例如:將 Jni.c 所定義的 function ,透過 Jni.h export 給其它 module 使用。這是一種常見的習慣,若一個 programmer 的 coding 習慣離一般習慣太遠,或變化甚大,甚至於毫無章法,該程式品質必然低落,也無研究價值。在研究它人程式碼時,需利用常用習慣。用之則有如神助,棄之則如牛犛田。一般 module ,會將其主要功能,以一組 function 和 data structure 加以定義,並 export 。然程式碼則充滿各式的實作細節,一一檢視何者為 export 的介面,何者為非,得花上大半時間。最好的方式是直接看 header file, .h file。header file 一般只有 export 的部分,因此沒有太多雜訊。檢視 module 的 header file 內容,大抵能了解一個 module 的功用。

然而,我們並沒有發現 Jni.h ,這時只有直接檢視 Jni.c 的內容。很幸運的, Dalvik 的 developer 們有良好習慣,將 local 使用的 function 都宣告為 static。這是一個常見的良好的習慣,因些我們可以很快略過 static function ,得知有哪些重要的 function 。為了更加快速,可利用 editor 的 folding 功能,讓 editor 收疊程式的內容,只 show 出最外層的 function 定義。以 emacs 為例,按 ctrl+c @ ctrl+alt+h 能達到前述功能。按 ctrl+c @ ctrl+alt+s 則顯示所有細節。

在 Jni.c 我們看到

* dvmJniStartup()
* dvmJniShutdown()
* dvmGetJNIEnvForThread()
* dvmCreateJNIEnv()
* dvmDestroyJNIEnv()
* dvmGetJNIRefType()
* dvmReleaseJniMonitors()
* dvmLateEnableCheckedJni()
* JNI_GetDefaultJavaVMInitArgs()
* JNI_GetCreatedJavaVMs()
* JNI_CreateJavaVM()

裡有數十個 static function 被我們略過,只需注意其中這幾個 global function 。一個 module 在介面定義了許多功能,其中只有一部分是重要的,其它則是補助性質。其中

* dvmJniStartup()
* dvmJniShutdown()

看起來是使用這個 module 必需進行初始化的 function 。於是我們 grep 一下目錄,發現在 dvmStartup() (in vm/Init.c) 裡呼叫了這個程式。dvmStartup() 也呼叫了許多其它 *Startup() function ,看來所有子系統的 initialization function 都在這裡呼叫。而 dvmStartup() 則被 JNI_CreateJavaVM() 所呼叫。因而我們可以猜測,所有的 sub-module 的 initialize 都透過 JNI_CreateJavaVM() 呼叫 dvmStartup() 而進行。包括 Jni.c 本身。

gDvm

在 main() function 裡,我們知道 vm 、 env 兩個未 local variable 是 pointer,並且未初始化 。 main() function 傳兩者的reference (address) 於 JNI_CreateJavaVM() ,此意乃 JNI_CreateJavaVm() 需初始化兩個 pointer 的內容。大抵上,developer coding 之時,不會將未初始化的 variable 的 reference 傳入其它 function ,除非該 function 需負責設定 variable 的內容。從變數名,吾者已能臆測 vm 應該是保留 JNI_CreateJavaVM() 所建立的 VM object 的位址。從 main() function 中,透過 env 呼叫 CallStaticVoidMethod() 、FindClass() 等等 function ,想必是 VM 提供之控制介面。

為確定猜想,必然要檢示 JNI_CreateJavaVM() 內容。pVM,即 main() function 的 vm ,是在此 function 裡 allocate memory ,並設定其內容。而 pEnv,即 main() function 的 env,則呼叫 dvmCreateJNIEnv() 而得。檢視 dvmCreateJNIEnv() 和 JNI_CreateJavaVM(),發現 pVM 和 pEnv 之間並沒有資料的關聯,也就是 pVM 和 pEnv 並沒有相互指向對方,應該兩者無關。但在 main() function 又有暗示兩者之間的關聯。於是在 JNI_CreateJavaVM() 裡,可以發現 gDvm 這個 global variable 。這個變數似乎是由 JNI_CreateJavaVM() 所初始化。 Global variable 通常意指系統有些 function 是透過這些 variable ,相互分享資料。難道 pVM 和 pEnv 的關聯,即透過 gDvm 建立?

pVM 和 pEnv 是透過 casting 才 assign 給 main() function 的 vm 和 env

001 *p_env = (JNIEnv*) pEnv;
002 *p_vm = (JavaVM*) pVM;

這通常意謂著 pEnv 開頭第一個欄位是 JNIEnv 型別,而 pVM 的頭一個欄位是 JavaVM 型別。這是 C programmer 常用的技巧,隱藏實作所需的資料,將必需 export 的資料,放在第一個欄位裡透過 casting export 出去,並避免 user 看到其它資料。至此,讀者或會發現,trace 別人的 code 往往必需了解作者的習慣和技巧。所幸好用的習慣的技術,會被大多數人接受、採用。也因此,要想 trace 別人的 code ,自己本身也需要有一定的 coding 的技術,並透過閱讀別人的 code ,累積一些大多數常用的技巧和習慣。幾年前有一本書,名為 Code Reading (by by Diomidis Spinellis)。這本書應該加入這些材料才對。

為確定 pVM 和 pEnv 的頭一個欄位,我們檢識 JniInternal.h 裡的 JavaVMExt 和 JNIEnvExt,發覺分別是 JNIInvokeInterface* 和 JNINativeInterface* 型別。這是怎麼回事? 於是回頭檢查 JavaVM 和 JNIEnv ,發覺

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++ ,所以是下半部。沒錯,正和前面所發現的相同。於是確定 pVM 和 pEnv 的第一個欄位 export 了 main() function 裡的 vm 和 env 所提供的內容。

再仔細檢視 JNI_CreateJavaVM() 和 dvmCreateJNIEnv() ,會發覺頭個欄位的內容分別定義在 gInvokeInterface 和 gNativeInterface 。而兩者的內容則是列了一堆 function pointer ,指向許多 function 。完全證實我們之前所猜測的。

至此,讀者應已發現。我在 trace code 時,並不是一行一行的看。而是大膽假設、然後小心求證、再假設、再求證,跳躍式 trace source code 。試想,programmer 在寫程式時也不是線性的,一行一行,一口氣從第一行寫到最後一行。大抵上也是一次 implement 一個概念,然後一個概念接著一個概念,慢慢將 code 填上去。一個 function 或許來來回回修了很多次。

gInvokeInterface 和 gInvokeInterface 所列之 function ,似乎全為 Jni.c 裡面的 static function 。我們在 Jni.c 裡 search gDvm ,看看這些 function 是否有 access gDvm 及其用法,發覺使用的不多。再仔細看看 JavaVMExt 和 JNIEnvExt 的定義,發覺沒有存放什麼狀況。反倒是 gDvm 的型別 DvmGlobals 存放了許多資料,似乎 VM 的狀態大多存放在此。如果一個系統將狀態存放在 global variables,那意謂著該系統在一個 process 只能執行一份。看來 Dalvik 目前在一個 process 裡,只允許一個 VM 。我在寫程式極力避免這種情形,以保有單一 process 執行多份相同的 instance 的可能性。然而, global variable 倒是偷懶的好方法,如果你很確定不想保有此彈性。

寫到這了,發覺這一篇文章似乎不是一篇,而是好多篇。所以。待續.....

No comments: