文/張恩勤 姜德軍 程雯 吳海全
Android是目前最為廣泛應用的一個嵌入式操作系統,手機、平板電腦和汽車等電子等各種設備中皆可見其身影。根據IDC的統計,2018年Android在移動平臺的市場占有率為85%,而且預期未來5年內還將保持1.7%的年增長率,越來越多的Android的設備以及應用程序在被開發出來。如何在Android這么一個大的系統中調試這些新的設備驅動或者應用程序一直是開發過程中的一個重要問題。一個優秀的方法將有效節約我們的開發時間和開發費用。
這是一個典型的傳統的軟件調試過程:
(1)在App1中發現一個問題。
(2)調整App1的源程序,添加調試代碼。
(3)重新編譯App1并加載到設備中。
(4)復現App1問題,取得對應的調試信息。
(5)分析發現App1的問題是由于App1中使用了DriverA。
(6)修過DriverA的源程序,加入打開調試信息。
(7)重新編譯DriverA并加載到設備中。
(8)復現DriverA問題,取得對應的調試信息。
(9)發現DriverA的問題又是由其調用DriverB引起...
這個過程我們發現很多時間被浪費在修改代碼增加日志、重新編譯,加載目標程序到設備等操作中。這里會有這樣一個疑問,為什么我們不一次打開所有涉及模塊的調試信息呢?如果我們用這種方法,我們會一次獲得非常大量的信息,對于我們分析問題會不方便,而且過度的調試信息可能會改變程序的運行時序,使得問題沒法重現。
是否可以在不修改程序的情況下動態打開關閉調試日志呢,其實Android內核有一種動態調試技術。pr_debug()或者dev_debug()來代替傳統的printk()輸出日志。內核通過他們可以在運行期動態地打開關閉調試信息。
這是一個典型的基于動態調試技術的調試軟件過程:
(1)在App1中發現一個問題。
(2)運行命令打開App1的調試信息。
(3)復現App1問題,取得對應的調試信息。
(4)分析發現App1的問題是由于App1中使用了DriverA。
(5)運行命令打開DriverA的調試信息,同時如果需要可以關閉App1的的調試信息。
(6)復現DriverA問題,取得對應的調試信息。
(7)發現DriverA的問題又是由其調用DriverB引起。
(8)運行命令打開DriverB的調試信息...
顯而易見,通過這種方法可以使我們的調試過程更加有效率。我們下面了解一下動態調試技術如何在Android內核中使用,然后拓展這個技術到用戶態程序。
Android內核模塊中使用的動態調試技術基于Linux DebugFS。DebugFS是一個虛擬的內存文件系統,和procFS以及sysFS類似.可以用來在用戶空間和內核模塊之間交換數據。
首先,編譯時需要確定內核配置文件中的DebugFS和Dynamic debug是打開的。

接著,就可以在開發內核模塊的過程中使用 pr_debug()或者dev_debug()代替printk(),預先在關鍵點加上各種打印日志。
運行調試時,首先確認debugFS被加載,加載方式可以通過為init.rc腳本增加命令,或者在啟動以后直接運行如下命令:
mount -t debugfs debugfs /sys/kernel/debug
當執行過以上命令后,我們可以在調試目標的文件系統中發現如下文件:
/sys/kernel/debug/dynamic_debug/control
這時pr_debug()或者dev_debug()的行為就會為這個文件所控制,開發者可以通過修改這個文件來控制調試日志是否輸出。默認情況下所有的調試開關是關閉的,只有CONFIG_DYNAMIC_DΕBUG打開時才會建立這一文件,對于非調試階段程序運行效率的影響非常小。
每一個日志都可以被單獨控制或者從更高層統一控制。例如打開一個模塊中的所有打印日志、一個文件中的所有打印日志、一個函數所有打印日志或者僅有指定的一行的日志。
例如:

通過這種方法我們可以在程序運行期打開我們需要的信息,同樣,通過把以上命令中的“+p”變為“-p”。我們可以動態關掉調試信息。
比如關svc_process()函數的所有信息
# echo -n 'func svc_process -p' >/sys/kernel/debug/dynamic_debug/control
這個章節我們走進Android內核代碼,具體分析動態調試技術是如何運行的。dev_dbg()和pr_debug()運行基本一樣,我們使用pr_debug()作為例子。

如果DΕBUG被定義,我們就會繼續使用printk。如果DΕBUG和CONFIG_DYNAMIC_DΕBUG都沒有被定義,目標代碼就不保護任何調試信息。這兒我們關注dynamic_pr_debug()。
在dynamic_debug.h里面:


Unlikely ()在這里的使用是為了在調試沒有打開時獲得更好的運行效率。GCC編譯器會根據unlikely()做優化,把調試相關代碼放到跳轉語句中,因為更多的情況是調試不打開的情況。
在對應模塊的makef i le文件中定義了DΕBUG_HASH 和 DΕBUG_HASH2。 使 用djb2和r5哈希算法。輸入參數為代碼的路徑,模塊名稱。這兩個哈希值用了加速判斷某一個模塊的調試是打開還是關閉。
debug_f l ags =
-D"DΕBUG_HASH=$(shell ./scripts/basic/hash djb2 $(@D)$(modname))"
-D"DΕBUG_HASH2=$(shell ./scripts/basic/hash r5 $(@D)$(modname))"
可以發現當源程序被編譯后,每一個使用pr_debug()語句的地方,目標代碼中會插入一個_ddebug的結構體,名為descriptor,放入__verbose數據段。它包含所有調試相關信息,比如模塊名稱函數名稱、文件名、DΕBUG_HASH DΕBUG_HASH2和調試標志。換一種說法,每一使用pr_debug()的地方,可執行代碼中都會這么一段。
更加深入一些,我們解釋開一段二進制代碼,就會發現__verbose數據段有這樣的數據:
0e38 72020000 00000000 7b020000 b8020000 0d0f0000 26010000
0e50 72020000 1c000000 7b020000 b8020000 0d0f0000 5f010000
可以發現其實這就是_ddebug數據。
同時Android內核中有個dynamic_debug模塊,當系統啟動起來的時候會被調用。創建一個名為control的debugFS。
在dynamic_debug.c里面:

dynamic_debug用于創建和維持一個名為debug_tables的鏈表。同時創建了兩個哈希表,用于加速查詢過程。(和前面介紹的一樣,使用路徑和模塊名稱作為key)。

當使用pr_debug()的被調試程序裝載的時候,動態調試模塊會裝載位于__verbose數據段的數據,分析并存到鏈表中。從這張表里面可以看出多少pr_debug()是出于打開狀況的。


在module.c里面,當一個內核模塊安裝時候以下函數會被調用。所有驅動模塊的_verbose數據段都會被用來初始化debug_tables。
任何用戶對debugFS “control”的寫操作都會導致ddebug_change ()被調用。相應的 debug_tables、 dynamic_debug_enabled 和dynamic_debug_enabled2會被改動。

以打開調試為例,這三個變量會賦值為True,從而前文提到的pr_debug()里面的__dynamic_dbg_enabled()函數中所有判斷條件都滿足,__dynamic_dbg_enabled()返回真,調試消息被打印出。
在用戶空間里面沒有內核里面的這個預先定義的動態調試模塊,但是當我們理解了其運行模式,我們可以運用相似的方法來進行動態調試。下面是一個簡單的例子。


對應每一個debug語句,我們也加入一個__debug_desc結構到__debug數據段。

提供給用戶一個函數用來打開或者關閉調試:test_debug()可以被設計成寫文件后者寫控制臺。
在需要使用動態調試的應用程序里面加入如下代碼來初始化動態調試。


這樣這段程序在正常運行是不會輸出“test dynamic debug”這行日志的,當我們需要打開調試日志的時候,我們通過控制臺發送一個USR2信號到對應的程序:
Kill -USR2
在接收到這個信號(Signal)以后,該程序中使用DBG()輸出的日志就會被打印出來。
動態調試技術已經被廣泛應用于Android內核模塊開發中,越來越多的應用程序和驅動程序也在使用這樣的技術,它以極少的資源消耗加速了程序開發調試的進程。