1. 程式人生 > >使用Valgrind找出Android中Native程式記憶體洩露問題

使用Valgrind找出Android中Native程式記憶體洩露問題

轉自 https://blog.csdn.net/roland_sun/article/details/46049485

 

Android程式通常使用Java程式編寫,由於Dalvik虛擬機器集成了垃圾回收機制,所以記憶體使用比較不容易出錯,通常就是一個本該被釋放的物件卻被另一個物件長時間持有著。對於這類問題,可以使用MAT工具,在Eclipse下結合DDMS進行分析。

但是,目前任然有很多Android的應用程式,出於效能或者是安全的考慮,還包含了通過JNI呼叫的Native程式。這些Native程式使用C或C++語言編寫,並沒有Java語言的垃圾回收機制,而且可以使用指標直接對記憶體進行操作,因此更容易出現記憶體洩露、緩衝區溢位等記憶體使用方面的錯誤。對於這類問題,也有工具可以幫忙查詢,它就是Valgrind。

安裝Valgrind

Valgrind是基於原始碼釋出的,官網上並沒有編譯好的直接給Android平臺的版本可以使用,因此需要自己編譯。筆者預先編譯了一個版本,可以從這裡拿到。

將壓縮包解包,裡面包含有許多檔案,安裝步驟如下:

1)確保你是root使用者;

2)請將名字為“valgrind”的檔案,拷貝到“/system/xbin”目錄下;

3)在“/system/lib”目錄下建立一個名為“valgrind”的新目錄,並將壓縮包中剩下的所有檔案都拷貝到這個目錄下。

4)將以上所有檔案的許可權屬性都改成775。

特別說明,如果/system目錄在系統啟動時,被mount成read only模式的話,請用read write模式重新mount一下就行了:

mount -o remount,rw /system

執行Valgrind

如果想用Valgrind工具進行測試,必須滿足兩個要求:

1)被測程式最好是Debug版本的(當然release版本的也可以,但是發現問題後定位問題程式碼會比較麻煩);

2)被測試程式必須要由Valgrind工具來啟動。

另外,目前Valgrind只能在支援armv7及以上指令集的ARM CPU上工作,所以不能在較老的裝置上執行。

測試可執行檔案

如果你要測試的是一個可執行的原生程式的話,那麼非常簡單,只需要直接鍵入以下命令:

 

valgrind <YOUR PROGRAM NAME> -v --error-limit=no --trace-children=yes --track-fds=yes --log-fie=/sdcard/<YOUR PROGRAM NAME>.%p.valgrind.log --tool=memcheck --leak-check=full --track-origins=yes

 

稍微說明一下這些引數的意思:

1)       -v或者--verbose,如果帶這個引數的話,那麼Valgrind會將測試過程中的詳細資訊打印出來;

2)       --eror-limit=<no|yes>,若為yes,則當這輪測試檢測出了超過10,000,000個錯誤,或者有1,000個不同型別的錯誤的話,就停止報錯;如果是no的話,則沒有這個限制,無論有多少個錯誤或多少型別的錯誤,都會報告出來。預設,如果不特別指定的話,這個選項的值是yes;

3)       --trace-children=<no|yes>,如果是yes的話,則Valgrind不僅會檢查被啟動程式的主程序,還會檢查由主程序fork出來的子程序。預設的值是no;

4)       --track-fds=<no|yes>,若為yes則Valgrind還會記錄所有開啟的檔案控制代碼,這樣有助於查詢到潛在的控制代碼洩露問題,預設值是no;

5)       --log-file=<LOG FILE NAME>,通過設定該引數,將Valgrind發現的問題記錄到指定的檔案中。特別說明一下,引數中有一個“%p”的引數,Valgrind會自動將其替換成當前程序的ID。前面也提到了,Valgrind會追蹤子程序,因此如果檔名不區分程序號的話,所有的問題都會被記錄到一個檔案中,會比較混亂,不利於後面的分析;

6)       --tool=<TOOL NAME>,Valgrind除了可以用來檢查記憶體錯誤外,還有其它功能。所以,這個引數是指定Valgrind到底使用哪個分析工具。預設情況下,Valgrind預設會執行memcheck工具。因此,這個引數指不指定無所謂;

7)       --leak-check=<no|summary|full>,如果設定成summary,則只會顯示發現了多少個問題,不會顯示細節;如果設定成yes或full,則會顯示每個錯誤的細節。預設值是summary。

8)       --track-origins=<no|yes>,預設值是no,則Valgrind只知道某個變數是否未被初始化就被使用了,但並不知道這個未初始化變數定義在哪裡。如果設定成yes,則會儲存每個未初始化變數的位置;

當啟動起來之後,讓你的程式執行一會,這樣可以儘可能多的發現潛在的問題。

特別提一下,Valgrind是一個動態掃描工具,只有在當有問題的程式碼被執行到的情況下,才能發現問題。這點完全不同於其它的一些靜態程式碼掃描工具。

測試JNI呼叫的原生程式

如果你要測試的程式是一個JNI呼叫的原生程式的話,那麼測試的方法會有一點複雜。

首先,要建立一個指令碼,本例將其命名為“start_valgrind.sh”,內容如下:

 
  1. #!/system/bin/sh

  2.  
  3. PACKAGE="<YOUR PACKAGE NAME>"

  4.  
  5. VGPARAMS='-v --error-limit=no --trace-children=yes --track-fds=yes --log-file=/sdcard/<YOUR PACKAGE NAME>.%p.valgrind.log --tool=memcheck --leak-check=full --track-origins=yes'

  6.  
  7. export TMPDIR=/data/data/$PACKAGE

  8.  
  9. exec valgrind $VGPARAMS $*

請將<YOUR PACKAGE NAME>替換成你要測試程式的包名。

然後,將這個指令碼拷貝到裝置上的“/data/local”目錄下,並將其許可權屬性設定成775。

接下來,在裝置上使用“setprop”命令,設定一個屬性,這個屬性的名字就是在你的包名前面加上“wrap.”

setprop wrap.<YOUR PACKAGE NAME> “logwrapper /data/local/start_valgrind.sh”

這個命令必須要在root使用者下執行才能起效果。

一個Android應用程式在啟動的時候會查詢有沒有屬性名為“wrap.<本應用程式包名>”的屬性。如果存在的話,則用這個屬性的值來重新啟動自己。但是,這種做法有一個小的限制,屬性名不能夠太長,不能超過31個字元,這也就意味著你應用程式的包名最好不要超過26字元,否則會面臨被截斷的問題。如果想詳細瞭解其實現流程,可以參考這裡

“logwrapper”是Android系統中自帶的一個小程式,用來將後面引數中指定程式的標準輸出重定向到logcat上。如需瞭解詳情,請參考這裡

在測試之前還要確保你要測試的應用程式沒有在偷偷的在後臺跑,可以使用下面的命令強制讓它退出:

adb shell am force-stop <YOUR PACKAGE NAME>

好了,到此一切都準備就緒了,可以開始動手測試了,直接點選你應用程式的圖示就行了。使用Valgrind對程式進行測試,會讓程式啟動和相應速度都慢很多,請耐心等待。這時,在“/sdcard”目錄下,應該就可以看到Valgrind生成的測試日誌了。

Example

 

下面舉個例子來具體操作一下,這裡使用Android-NDK中自帶的,最簡單的“hello-jni”範例程式,稍作一點修改。

在“hello-jni.c”檔案中,故意埋下一些錯誤的記憶體使用,並在“Java_com_example_hellojni_HelloJni_stringFromJNI”函式中呼叫它們:

 
  1. static void memory_overrun()

  2. {

  3. char *p = malloc(1);

  4. *(short*)p = 2;

  5.  
  6. free(p);

  7. }

  8.  
  9. static void strcpy_overrun()

  10. {

  11. char *p = malloc(sizeof(char) * 5);

  12. strcpy(p, "hello");

  13. }

  14.  
  15. static void memory_free_wild_pointer()

  16. {

  17. char *p;

  18. free(p);

  19. }

  20.  
  21. jstring

  22. Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,

  23. jobject thiz )

  24. {

  25. memory_overrun();

  26. strcpy_overrun();

  27. memory_free_wild_pointer();

  28. return (*env)->NewStringUTF(env, "Hello from JNI ! Compiled with ABI " ABI ".");

  29. }

還有一點,要特別強調一下,Valgrind是通過掃描程式退出之前的堆記憶體使用情況來判斷有沒有記憶體洩露問題的,所以你的程式必須要退出之後,Valgrind才會生成最終的測試報告。但是Android應用一般是不會退出的,只會切換到後臺,因此可以在你程式碼適當的地方加上退出的程式碼(“System.exit(0)”),讓你的程式正常退出。

最後,把Valgrind工具生成的完整日誌開啟來看看,發現了什麼:

可以看到,三個問題都發現了,並且還指出了出錯的位置,還是非常方便的。

當然,Valgrind也不是萬能的,它說你的程式沒問題,不代表真的沒有問題。但是,比起閱讀原始碼來發現潛在問題來說,通過這種方法要容易許多。