2009年9月12日 星期六

好用的 Trace 工具 cflow

Standard
自己寫軟體,自己的邏輯,自己的世界,往往不會有什麼困難,這也是很多人從學生時代一路爬上來的歷程,直到進入到職場後撰寫著商業軟體,更是只有自己的一片天。但寫軟體有如寫作文,言之有物前必先廣閱天下文章,否則若是能獨樹一格是好,不能便陳腔濫調且原地踏步。但是看懂別人的程式實在是很困難的一件事,除了要懂他人寫程式的風格和思維外,還要通盤了解架構,這必須要有見山不是山的能力才能勝任。一般人想單靠著程式碼上彎彎曲曲的豆芽菜,反推回去程式的原貌,真有如瞎子摸象。對於做為一般人的我們,這時便要借助些工具,以幫助我們更省時省力的去 Trace 程式碼。

講到閱讀程式碼,『cflow』就是不得不提到的方便工具之一,它能夠幫助我們確認程式的大架構,以及分析程式碼相互的關聯性。這裡是使用 cflow 去演示分析 Android Dalvik VM,分析的檔案是 dalvik/dalvikvm/Main.c:
$ cflow dalvikvm/Main.c
main() <int main (int argc,char *const argv[]) at dalvikvm/Main.c:141>:
setvbuf()
malloc()
memset()
strdup()
strcmp()
fprintf()
assert()
blockSigpipe() <void blockSigpipe () at dalvikvm/Main.c:31>:
sigemptyset()
sigaddset()
sigprocmask()
fprintf()
JNI_CreateJavaVM()
createStringArray() <jobjectArray createStringArray (JNIEnv *env,char *const argv[],int argc) at dalvikvm/Main.c:44>:
FindClass()
ExceptionCheck()
fprintf()
assert()
NewObjectArray()
NewStringUTF()
SetObjectArrayElement()
DeleteLocalRef()
FindClass()
GetStaticMethodID()
methodIsPublic() <int methodIsPublic (JNIEnv *env,jclass clazz,jmethodID methodId) at dalvikvm/Main.c:92>:
ToReflectedMethod()
fprintf()
FindClass()
GetMethodID()
CallIntMethod()
DeleteLocalRef()
CallStaticVoidMethod()
ExceptionCheck()
DetachCurrentThread()
DestroyJavaVM()
free()
一般來說,cflow 會從進入點 main() 開始做解析,可以看到每一個被呼叫的 function 都被 cflow 抓了出來,甚至是出處和行數都清楚被標示,此外,只要是 function name 取得漂亮,我們已經可以從這樣的樹狀結構大致了解到 Dalvik VM 的初始化或執行流程。整個過程簡單易瞭,就算用猜的也能大致猜出許多部份的設計目的。

但是在前一個範例中,很多的 function 並未標示出處,甚至沒有更進一步的資訊,這是因為目前只有對 dalvik/dalvikvm/Main.c 做解析所致,但是我們還是可以一步步將程式碼引入,慢慢尋出一些脈絡,現在假設為了追 JNI_CreateJavaVM(),我們可以多引入其他程式碼檔案(*.c):
$ cflow dalvikvm/Main.c vm/*.c
main() <int main (int argc,char *const argv[]) at dalvikvm/Main.c:141>:
setvbuf()
malloc()
memset()
strdup()
strcmp()
fprintf()
assert()
blockSigpipe() <void blockSigpipe () at dalvikvm/Main.c:31>:
sigemptyset()
sigaddset()
sigprocmask()
fprintf()
JNI_CreateJavaVM() <jint JNI_CreateJavaVM (JavaVM **p_vm,JNIEnv **p_env,void *vm_args) at vm/Jni.c:3501>:
memset()
malloc()
dvmInitMutex()
fprintf()
strcmp()
strncmp()
LOGW()
strchr()
dvmUseCheckedJniVm() <void dvmUseCheckedJniVm (JavaVMExt *pVm) at vm/CheckJni.c:2422>:
assert()
dvmCreateJNIEnv() <JNIEnv *dvmCreateJNIEnv (Thread *self) at vm/Jni.c:492>:
assert()
calloc()
dvmSetJniEnvThreadId()
dvmUseCheckedJniEnv() <void dvmUseCheckedJniEnv (JNIEnvExt *pEnv) at vm/CheckJni.c:2412>:
assert()
dvmLockMutex()
dvmUnlockMutex()
dvmStartup() <int dvmStartup (int argc,const char *const argv[],bool ignoreUnrecognized,JNIEnv *pEnv) at vm/Init.c:910>:
assert()
LOGV()
setCommandLineDefaults() <void setCommandLineDefaults () at vm/Init.c:814>:
getenv()
strdup()
dvmPropertiesStartup() <bool dvmPropertiesStartup (int maxProps) at vm/Properties.c:29>:
malloc()
dvmProcessOptions() <int dvmProcessOptions (int argc,const char *const argv[],bool ignoreUnrecognized) at vm/Init.c:542>:
LOGV()
...(以下省略)
如此,將所有的程式碼檔案引入,就可以得到所有 function 的資訊,而其中便可以得知 JNI_CreateJavaVM() 被定義在 vm/Jni.c(當然也可以使用 grep 達到目的)。不過你或許發現到,一次引入到這麼多檔案所產生的層層資訊太過龐大,並不利於閱讀和分析,因此這樣的做法其實並不被常使用,而通常是知道 JNI_CreateJavaVM() 所在檔案後,單獨引入做簡單的組合分析:
$ cflow dalvikvm/Main.c vm/Jni.c
main() <int main (int argc,char *const argv[]) at dalvikvm/Main.c:141>:
setvbuf()
malloc()
memset()
strdup()
strcmp()
fprintf()
assert()
blockSigpipe() <void blockSigpipe () at dalvikvm/Main.c:31>:
sigemptyset()
sigaddset()
sigprocmask()
fprintf()
JNI_CreateJavaVM() <jint JNI_CreateJavaVM (JavaVM **p_vm,JNIEnv **p_env,void *vm_args) at vm/Jni.c:3501>:
memset()
malloc()
dvmInitMutex()
fprintf()
strcmp()
strncmp()
LOGW()
strchr()
dvmUseCheckedJniVm()
dvmCreateJNIEnv() <JNIEnv *dvmCreateJNIEnv (Thread *self) at vm/Jni.c:492>:
assert()
calloc()
dvmSetJniEnvThreadId()
dvmUseCheckedJniEnv()
dvmLockMutex()
dvmUnlockMutex()
dvmStartup()
free()
dvmChangeStatus()
LOGV()
createStringArray() <jobjectArray createStringArray (JNIEnv *env,char *const argv[],int argc) at dalvikvm/Main.c:44>:
FindClass() <jclass FindClass (JNIEnv *env,const char *name) at vm/Jni.c:1171>:
JNI_ENTER()
dvmGetCurrentJNIMethod() <const Method *dvmGetCurrentJNIMethod (void) at vm/Jni.c:976>:
assert()
dvmThreadSelf()
SAVEAREA_FROM_FP()
dvmIsNativeMethod()
...(以下省略)
或者是只想追 JNI_CreateJavaVM() 的細節並省略掉其他不相干的資訊,可以將進入點從 main() 設為 JNI_CreateJavaVM():
$ cflow -m JNI_CreateJavaVM dalvikvm/Main.c vm/Jni.c
cflow:vm/Jni.c:3144: gNativeInterface/-1 redefined
cflow:vm/Jni.c:221: this is the place of previous definition
JNI_CreateJavaVM() <jint JNI_CreateJavaVM (JavaVM **p_vm,JNIEnv **p_env,void *vm_args) at vm/Jni.c:3501>:
memset()
malloc()
dvmInitMutex()
fprintf()
strcmp()
strncmp()
LOGW()
strchr()
dvmUseCheckedJniVm()
dvmCreateJNIEnv() <JNIEnv *dvmCreateJNIEnv (Thread *self) at vm/Jni.c:492>:
assert()
calloc()
dvmSetJniEnvThreadId()
dvmUseCheckedJniEnv()
dvmLockMutex()
dvmUnlockMutex()
dvmStartup()
free()
dvmChangeStatus()
LOGV()
到此目前為止,大概可以看出 JNI_CreateJavaVM() 對 Mutex 機制做了初始化,又使用 dvmCreateJNIEnv() 初始主要 Thread 的環境。當然以上是猜測,為了確認表面的假設,我們可以大致閱讀 dvmCreateJNIEnv() 的程式碼來證明或是推翻。

當然,以上只是演示 cflow 的用法和方便性,並還沒有真的在 Trace Dalvik。透過 cflow 工具的分析,我們可以先行做假設,再針對目標做一步步證明,相較於面對大海茫茫的程式碼而不知從何下手,這卻是一個突破窘境的方法。