使用GDB進行代碼覆蓋率測試

劉 明 ([email protected]), 軟件工程師, 上海交通大學電子與通信工程系


測試工程師經常面對的一個問題就是如何獲得測試的代碼覆蓋率。很多專業軟件可以提供這種專門的代碼覆蓋率檢測。通過對 GDB 的小小改造,也可以令其提供代碼覆蓋率測試功能。這種改動與平臺無關,只要 GDB 支持的平臺,都可以運行。

簡介

熟悉 Excel 的程序員都知道,Excel 不僅是一個應用軟件,還能作爲一個開發平臺。這不僅是因爲 Excel 提供了 VBA,更重要的是 Excel 本身處理了數據庫連接,數據處理以及報表生成等複雜的工作。程序員從而避免了自己實現這些功能的負擔。

同樣,我們認爲 gdb 本身的強大功能也使得它可以成爲一個開發平臺,充分利用它的符號處理能力和進程控制功能,我們可以開發出一些新的功能。

測試工程師經常面對的一個問題就是如何獲得測試的代碼覆蓋率。很多專業軟件可以提供這種專門的代碼覆蓋率檢測。通過對 GDB 的小小改造,也可以令其提供代碼覆蓋率測試功能。這種改動與平臺無關,只要 GDB 支持的平臺,都可以運行。

基本原理

GDB的一個基本功能就是單步運行程序,我們想到,如果在每次單步運行的時候,記錄下運行過的代碼數量,將此數據與總代碼段長度比較,不就可以獲得代碼覆蓋率了嗎?

最初的想法很簡單,但是讓測試人員不停地單步執行顯然是不現實的,因此我們擴充了基本的gdb命令,增加了一條命令叫做covertest。該命令不斷地自動調用單步執行命令,並在每一個單步命令之後,記錄下運行過的代碼行數。直到程序運行結束。然後covertest命令讀取ELF文件頭,得到總的代碼段長度。最後,用記錄下的運行過的代碼數量除以總的代碼段長度,從而得到代碼覆蓋率。

經過幾周的調試,我們在RedHat9.0/x86平臺上,修改GDB5.3,成功地實現了代碼覆蓋率測試功能。

代碼覆蓋率定義和代碼長度

我們把代碼覆蓋率定義爲運行過的代碼長度除以程序總的代碼長度。

代碼長度是二進制代碼長度。而不是在C源文件中的代碼長度。比如一條賦值語句在C語言中就是一條語句,但是編譯爲彙編語言後可能是一條,也可能是多條彙編指令。而且在Intel IA處理器中,指令長度是可變的。因此我們所說的代碼長度是指最終的二進制代碼的字節長度。

這種定義可能不是最佳的定義.但是是最容易實現的定義。在本文中,代碼覆蓋率採用機器指令長度作爲衡量標準。

下面的例子比較了不同的代碼長度的定義:

增加命令covertest

gdb是一個命令行工具,它基本的工作模式類似Shell。接收用戶輸入的命令然後執行相應的處理函數。gdb中CLI(command line interface)子系統負責用戶界面的工作,它顯示提示符,接收用戶輸入,分析用戶輸入並調用相應的處理函數。

CLI子系統的設計非常完善,它爲用戶添加新命令提供了幾個專門函數。add_com()就是最基本的一個。它有四個入口參數,第一個參數是命令的名字,類型爲字符串;第二個參數表明該命令的類型;第三個參數是該命令的處理函數,第四個參數是關於該命令的幫助說明。:


                
struct cmd_list_element *add_com (char *name,
enum command_class class,
void (*fun) (char *, int),
char *doc)

下面的代碼顯示瞭如何添加新的gdb命令.


                
_initialize_mark (void)
{
struct cmd_list_element *c;
c = add_com("covertest",class_breakpoint,set_mark,"test coverage");
}

_initialize_mark函數調用add_com()爲gdb添加新的命令。covertest對應的處理函數爲cover_command()。其中class_breakpoint是一個枚舉變量,表示命令covertest屬於斷點類的命令。當用戶鍵入help breakpoint後,就能看到命令covertest以及對它的說明,即add_com()的第四個參數”test coverage”。

選擇合適的單步命令

GDB提供了幾種不同的單步調試命令:step,stepi,next和nexti。

首先attach到setmark時fork的新進程,該進程ID已經保存在全局變量org_pid中。直接調用gdb函數attach_command()完成attach工作。

我們選擇step命令來單步執行程序。因爲該命令遇到子函數能夠進入子函數內部。step命令不會進入動態鏈接庫函數,比如printf。因爲沒有debug信息。這種特性非常符合代碼覆蓋率測試的要求。用戶使用代碼覆蓋率測試工具只希望瞭解自己編寫的代碼的覆蓋率情況。而不需要了解第三方庫函數以及系統庫函數的覆蓋率.比如下面的代碼片段:


                
void main(){
a = 10;
printf(“a is %d/n”,a);
}

運行該程序的代碼覆蓋率顯然爲100%。但是printf()函數本身非常複雜,用戶並不希望瞭解printf()的覆蓋率。該函數非常複雜,顯然上述調用不可能百分百地覆蓋printf()。如果單步進入printf(),則最終的測試覆蓋率結果就包含了對printf的測試,其結果就不會是100%了。

利用gdb這個特性可以自動區分第三方庫函數和用戶自己編寫的函數,這使得代碼覆蓋率測試的工作更加簡單了。

記錄單步執行的代碼長度

Gdb內部step命令相應的執行函數爲:


                
static void step_1 (int skip_subroutines,
int single_inst,
char *count_string)

爲了讓被調試程序單步執行,可以直接調用step_1(0,0,”1”)。該函數執行結束,目標進程就單步運行了一次,因此我們必須在此時記錄下這次單步所執行的機器指令的長度。

Gdb內部函數find_pc_line_pc_range爲我們完成了計算單步代碼長度的工作。每次調用step_1命令時,gdb都會調用find_pc_line_pc_ragne()函數得到一條C語言語句實際對應的機器代碼的起始地址和結束地址。這兩個值在gdb中分別存放在step_range_start和step_range_end兩個全局變量中。我們只需將兩個值相減就可以得到這次單步執行所運行過的機器指令的長度。

求總的代碼長度

我們把ELF文件中text段的長度作爲總的代碼長度。ELF中還有一些段包含了可執行代碼,但是我們將他們剔除了。理由是這些段中的代碼都不是用戶關心的代碼。比如.init段和.fini段。這些段是編譯器自動生成的。.init的執行在main()函數之前,.fini段代碼的執行在exit()函數之後。而我們執行單步函數是從main()之後開始,到exit()之前結束,因此在統計總代碼長度時將這兩個段的長度剔除。

Gdb將可執行代碼的段信息都放在current_target.to_sections中。Current_target是gdb中非常重要的一個數據結構,代表了被調試的目標。其中to_sections域存放了被調試程序ELF文件中所有section的信息。它的類型爲struct section_table:

Gdb將可執行代碼的段信息都放在current_target.to_sections中。Current_target是gdb中非常重要的一個數據結構,代表了被調試的目標。其中to_sections域存放了被調試程序ELF文件中所有section的信息。它的類型爲struct section_table:


                
struct section_table
{
CORE_ADDR addr; /* Lowest address in section */
CORE_ADDR endaddr; /* 1+highest address in section */
sec_ptr the_bfd_section;
bfd *bfd; /* BFD file pointer */
};

遍歷to_sections,找到section name爲”.text”的段,用endaddr減去addr就得到了該段的長度。

記住曾經走過的路

多數程序都有分支判斷和循環結構。因此covertest必須記住曾經運行過的代碼,當再次運行到這些代碼時,不應該重複記錄。比如下例:


                
int main(){
int i;
for(i=0;i<10;i++)
foo();
}

foo函數被調用了10次,但是在計算代碼覆蓋率時,它只應該被計算一次。

爲了記住程序過去走過的路,我們採用了bitmap數據結構。用指令地址作爲索引。當某指令地址被記錄時,就將相應的bitmap設置爲1。當下次再遇到該指令地址時,由於bimap已經爲一,我們就知道該指令在曾經走過的路徑上,不需要再記錄了。

Prologue統計

爲了實現函數調用,編譯器會在每個子函數頭部加入prologue。Gdb執行step命令進入子函數時,會跳過prologue,將斷點設在prologue後的第一條指令上。比如下例:


                
void foo()
{
int a;
a=10;
}

編譯後的彙編爲:


                
00000000 <_fooh>:
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 04 sub $0x4,%esp
6: c7 45 fc 0a 00 00 00 movl $0xa,0xfffffffc(%ebp)
d: c9 leave
e: c3 ret
f: 90 nop

前三句彙編指令都屬於prologue,主要作用是爲臨時變量a開闢stack中的空間。當使用gdb單步進入該函數時,gdb將第4行,即偏移量爲6的機器代碼作爲該函數的起始地址。而前面6個字節的prologue被跳過。在統計代碼覆蓋率時,必須將prologue也算入被覆蓋的代碼。爲此我們必須記錄下被gdb跳過的prologue的長度。

對於x86平臺,gdb對應prologue的處理在函數i386_skip_prologue()中。我們在該函數中增加了一個全局變量skipped_proglogue_len,記錄被跳過的prologue的長度。

結論

使用covertest命令使用非常簡單,將被測試程序用gdb打開。首先在main函數處設置斷點。然後直接調用covertest命令。下面是一個用covertest進行代碼覆蓋率測試的例子。

被測程序一:


                
//test1.c
void foo()
{
printf(“test/n”);
}
int main(void) {
int a = 1;
if (a ==1) foo();
}

被測程序二:


                
//test2.c
void foo()
{
printf(“test/n”);
}
int main(void) {
int a = 0;
if (a ==1) foo();
}

很顯然test1的覆蓋率應該爲100%,而test2則不到100%。分別編譯他們:


                
$gcc –g –o test1 test1.c
$gcc –g –o test2 test.c

用gdb打開test1


                
$gdb test1
(gdb) b main
(gdb) covertest
test
coverage rate: 100%


(gdb)

同樣的方法測試test2得到覆蓋率爲94%


結論

Gdb本身擁有強大的符號處理和進程控制能力,合理地利用gdb的這些能力,我們還能開發出更多的功能。比如稍微修改一下covertestt命令就可以實現程序執行流程的log功能。測試人員提交defect報告時,如果能將錯誤產生的執行路徑也一起提交對於開發工程師將非常有幫助。


下載

描述 名字 大小 下載方法
covertesti.c covertesti.c 5KB HTTP
i386-tdep.c i386-tdep.c 49KB HTTP
infrun.c infrun.c 143KB HTTP
關於下載方法的信息


參考資料



關於作者


劉明,從事嵌入式軟件開發,熱愛開源軟件。喜歡學習和使用 linux,目前致力於數據庫方面的工作和研究




發佈了10 篇原創文章 · 獲贊 12 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章