測試工程師經常面對的一個問題就是如何獲得測試的代碼覆蓋率。很多專業軟體可以提供這種專門的代碼覆蓋率檢測。通過對 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報告時,如果能將錯誤產生的執行路徑也一起提交對於開發工程師將非常有幫助。
(責任編輯:A6)
[火星人 ] 使用GDB進行代碼覆蓋率測試已經有524次圍觀