ChinaUnix.net 首页 | 博客 | Linux | 论坛 | 人才 | 培训 | 知识库 | 资料 | 读书 | 手册 | 精华 | 下载 | 沙龙 | 搜索
Linux首页 | Linux论坛 | 论坛精华 | 开源新闻 | 技术文章 | 专题专栏 | 新手指南 | 迁移方案 | 产品方案 | 开源项目 | 开源图书 | 软件下载 | 人才招聘 | Linux博客
  搜索

  产品与方案
·中科红旗全面打造现代化邮政体系
·红旗助力“网上审批服务” 推动电子政务
·红旗正版化开创呼和浩特网吧建设新起点
·红旗Linux助信息产业部邮件服务器“快跑”
·中标普华Linux 为电子政务信息化保驾护航
·中标普华Linux助力基金产业
·中标普华Office率先支持UOF标准
·中标普华邮件系统助力西藏政府信息化建设
·红旗Linux助力国库集中支付系统改革
·红旗助中信卫星 掀起GIS通信应用风暴
·红旗软件助力烟草总局 全面建设“数字烟草”
·红旗助力“信访阳光工程”打造畅通信访渠道
·红帽联合FIS发布下一代实时核心银行平台
·红旗助力金盾 打造全无忧出入境信息系统
·红旗Linux全力打造中国邮政总局名址信息库
·爱尔兰证交所从Unix迁移到红帽企业Linux
·一流的意大利银行选择使用红帽企业Linux
·PLUS Finanzservice选择使用红帽企业Linux
·红帽助力TransACT Communications 公司
·法国零售业巨头Lapeyre采用Redhat Linux
·旅游预订网站选择使用红帽企业Linux
·马哈拉施特拉邦政府的红帽解决之道
·美国联邦政府案例
·红帽为慕尼黑展览会提供现代化集群系统
·Yuba郡用开源软件和红帽产品提高了效率
·红帽企业Linux助印度理工建立高性能计算中心
·采用红帽Linux 将系统维护时间缩短了65%
·从UNIX迁移到Linux使Peñoles公司获益非浅
·Hikal公司用红帽企业Linux开展任务关键的ERP项目
·KDE3.5.4新版本发布
·芝加哥商业交易所从Unix向Linux迁移
·南方基金管理有限公司成功案例 Red Hat Linux
·广东北电通讯设备有限公司成功案例
·挪威国家石油公司从UNIX迁移到红帽Linux,成本减半
·中央电视台CCTV动画部案例 Red Hat Linux

  图书

鸟哥的Linux私房菜基础学..


Linux程序设计.第3版


Linux设备驱动开发详解


  下载
·Endian Firewall
·linux kernel(Linux 内核)
·CentOS
·Fedora Core 6
·Scientific Linux
·Slackware 11.0
·Gentoo Linux
·ubuntu-6.10-i386服务器版本
·ubuntu-6.10-amd64服务器版
·ubuntu-6.10-i386桌面版
·ubuntu-6.10-amd64桌面版
·Engarde Linux
您的位置: Linux时代 > 技术文档 > 程序开发 >

使用GDB进行代码覆盖率测试

日期:2007-12-04 作者:刘明 来自:IBM DW中国


测试工程师经常面对的一个问题就是如何获得测试的代码覆盖率。很多专业软件可以提供这种专门的代码覆盖率检测。通过对 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

原文链接:http://www.ibm.com/developerworks/cn/linux/l-cn-gdb/index.html

本文被浏览



 相关新闻

使用 GDB 调试多进程程序2007-08-06 15:40:27
使用kgdb调试linux内核及内核模块2006-08-03 10:26:37


 相关评论
关于我们 | 联系方式 | 广告合作 | 诚聘英才 | 网站地图 | 免费注册

Copyright © 2001-2006 ChinaUnix.net All Rights Reserved

感谢所有关心和支持过ChinaUnix的朋友们

京ICP证041476号