9

GDB 调试程序 详解 使用实例

 3 years ago
source link: https://blog.csdn.net/china_video_expert/article/details/70164483
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

GDB 调试程序 详解 使用实例

用GDB调试程序

GDB概述
————

GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具。或许,各位比较喜欢那种图形界面方式的,像VC、BCB等IDE的调试,但如果你是在UNIX平台下做软件,你会发现GDB这个调试工具有比VC、BCB的图形化调试器更强大的功能。所谓“寸有所长,尺有所短”就是这个道理。

一般来说,GDB主要帮忙你完成下面四个方面的功能:

   1、启动你的程序,可以按照你的自定义的要求随心所欲的运行程序。
   2、可让被调试的程序在你所指定的调置的断点处停住。(断点可以是条件表达式)
   3、当程序被停住时,可以检查此时你的程序中所发生的事。
   4、动态的改变你程序的执行环境。

从上面看来,GDB和一般的调试工具没有什么两样,基本上也是完成这些功能,不过在细节上,你会发现GDB这个调试工具的强大,大家可能比较习惯了图形化的调试工具,但有时候,命令行的调试工具却有着图形化工具所不能完成的功能。让我们一一看来。

一个调试示例
——————

源程序:tst.c

    1 #include <stdio.h>
    2
    3 int func(int n)
    4 {
    5        int sum=0,i;
    6        for(i=0; i<n; i++)
    7        {
    8                sum+=i;
    9        }
   10        return sum;
    11 }
    12
    13
    14main()
    15 {
   16        int i;
   17        long result = 0;
   18        for(i=1; i<=100; i++)
   19        {
   20                result += i;
   21        }
    22
   23       printf("result[1-100] = %d \n", result );
   24       printf("result[1-250] = %d \n", func(250) );
    25}

编译生成执行文件:(Linux下)
   hchen/test> cc -g tst.c -o tst

使用GDB调试:

hchen/test> gdbtst  <---------- 启动GDB
GNU gdb 5.1.1
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License,and you are
welcome to change it and/or distribute copies of it under certainconditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type"show warranty" for details.
This GDB was configured as "i386-suse-linux"...
(gdb)l    <-------------------- l命令相当于list,从第一行开始例出原码。
1       #include <stdio.h>
2
3       int func(int n)
4       {
5               int sum=0,i;
6               for(i=0; i<n; i++)
7               {
8                       sum+=i;
9               }
10              return sum;
(gdb)      <-------------------- 直接回车表示,重复上一次命令
11      }
12
13
14      main()
15      {
16              int i;
17              long result = 0;
18              for(i=1; i<=100; i++)
19              {
20                      result +=i;   
(gdb) break16   <-------------------- 设置断点,在源程序第16行处。
Breakpoint 1 at 0x8048496: file tst.c, line 16.
(gdb) break func <-------------------- 设置断点,在函数func()入口处。
Breakpoint 2 at 0x8048456: file tst.c, line 5.
(gdb) info break <-------------------- 查看断点信息。
NumType          Disp EnbAddress   What
1  breakpoint    keep y   0x08048496 in main attst.c:16
2  breakpoint    keep y   0x08048456 in func attst.c:5
(gdb)r          <--------------------- 运行程序,run命令简写
Starting program: /home/hchen/test/tst

Breakpoint 1, main () attst.c:17   <---------- 在断点处停住。
17              long result = 0;
(gdb)n         <--------------------- 单条语句执行,next命令简写。
18              for(i=1; i<=100; i++)
(gdb) n
20                      result += i;
(gdb) n
18              for(i=1; i<=100; i++)
(gdb) n
20                      result += i;
(gdb)c         <--------------------- 继续运行程序,continue命令简写。
Continuing.
result[1-100] =5050      <----------程序输出。

Breakpoint 2, func (n=250) attst.c:5
5               int sum=0,i;
(gdb) n
6               for(i=1; i<=n; i++)
(gdb) pi       <--------------------- 打印变量i的值,print命令简写。
$1 = 134513808
(gdb) n
8                       sum+=i;
(gdb) n
6               for(i=1; i<=n; i++)
(gdb) p sum
$2 = 1
(gdb) n
8                       sum+=i;
(gdb) p i
$3 = 2
(gdb) n
6               for(i=1; i<=n; i++)
(gdb) p sum
$4 = 3
(gdb)bt       <--------------------- 查看函数堆栈。
#0  func (n=250) at tst.c:5
#1  0x080484e4 in main () at tst.c:24
#2  0x400409ed in __libc_start_main () from/lib/libc.so.6
(gdb) finish   <--------------------- 退出函数。
Run till exit from #0  func (n=250) attst.c:5
0x080484e4 in main () at tst.c:24
24             printf("result[1-250] = %d \n", func(250) );
Value returned is $6 = 31375
(gdb)c    <--------------------- 继续运行。
Continuing.
result[1-250] =31375   <----------程序输出。

Program exited with code 027.<--------程序退出,调试结束。
(gdb)q    <--------------------- 退出gdb。
hchen/test>

好了,有了以上的感性认识,还是让我们来系统地认识一下gdb吧。

使用GDB
————

一般来说GDB主要调试的是C/C++的程序。要调试C/C++的程序,首先在编译时,我们必须要把调试信息加到可执行文件中。使用编译器(cc/gcc/g++)的-g 参数可以做到这一点。如:

   > cc -g hello.c -o hello
   > g++ -g hello.cpp -o hello

如果没有-g,你将看不见程序的函数名、变量名,所代替的全是运行时的内存地址。当你用-g把调试信息加入之后,并成功编译目标代码以后,让我们来看看如何用gdb来调试他。

启动GDB的方法有以下几种:

   1、gdb <program>
      program也就是你的执行文件,一般在当然目录下。

   2、gdb <program> core
      用gdb同时调试一个运行程序和core文件,core是程序非法执行后core dump后产生的文件。

   3、gdb <program><PID>
      如果你的程序是一个服务程序,那么你可以指定这个服务程序运行时的进程ID。gdb会自动attach上去,并调试他。program应该在PATH环境变量中搜索得到。

GDB启动时,可以加上一些GDB的启动开关,详细的开关可以用gdb-help查看。我在下面只例举一些比较常用的参数:

   -symbols <file>
    -s<file>
   从指定文件中读取符号表。

   -se file
   从指定文件中读取符号表信息,并把他用在可执行文件中。

   -core <file>
    -c<file>
    调试时coredump的core文件。

   -directory <directory>
    -d<directory>
   加入一个源文件的搜索路径。默认搜索路径是环境变量中PATH所定义的路径。

GDB的命令概貌
———————

启动gdb后,就你被带入gdb的调试环境中,就可以使用gdb的命令开始调试程序了,gdb的命令可以使用help命令来查看,如下所示:

   /home/hchen> gdb
    GNU gdb5.1.1
    Copyright2002 Free Software Foundation, Inc.
    GDB is freesoftware, covered by the GNU General Public License, and youare
    welcome tochange it and/or distribute copies of it under certainconditions.
    Type "showcopying" to see the conditions.
    There isabsolutely no warranty for GDB.  Type "showwarranty" for details.
    This GDB wasconfigured as "i386-suse-linux".
    (gdb)help
    List ofclasses of commands:

   aliases -- Aliases of other commands
    breakpoints-- Making program stop at certain points
    data --Examining data
    files --Specifying and examining files
    internals --Maintenance commands
    obscure --Obscure features
    running --Running the program
    stack --Examining the stack
    status --Status inquiries
    support --Support facilities
    tracepoints-- Tracing of program execution without stopping the program
    user-defined-- User-defined commands

   Type "help" followed by a class name for a list of commands in thatclass.
    Type "help"followed by command name for full documentation.
    Command nameabbreviations are allowed if unambiguous.
   (gdb)

gdb的命令很多,gdb把之分成许多个种类。help命令只是例出gdb的命令种类,如果要看种类中的命令,可以使用help <class> 命令,如:helpbreakpoints,查看设置断点的所有命令。也可以直接help<command>来查看命令的帮助。

gdb中,输入命令时,可以不用打全命令,只用打命令的前几个字符就可以了,当然,命令的前几个字符应该要标志着一个唯一的命令,在Linux下,你可以敲击两次TAB键来补齐命令的全称,如果有重复的,那么gdb会把其例出来。

示例一:在进入函数func时,设置一个断点。可以敲入break func,或是直接就是b func
    (gdb) bfunc
    Breakpoint 1at 0x8048458: file hello.c, line 10.

示例二:敲入b按两次TAB键,你会看到所有b打头的命令:
    (gdb)b
   backtrace break     bt
   (gdb)

   示例三:只记得函数的前缀,可以这样:
    (gdb) bmake_ <按TAB键>
   (再按下一次TAB键,你会看到:)
   make_a_section_from_file    make_environ
   make_abs_section            make_function_type
   make_blockvector            make_pointer_type
   make_cleanup                make_reference_type
   make_command                make_symbol_completion_list
    (gdb) bmake_
   GDB把所有make开头的函数全部例出来给你查看。

   示例四:调试C++的程序时,有可以函数名一样。如:
    (gdb) b'bubble( M-?
   bubble(double,double)   bubble(int,int)
    (gdb) b'bubble(
   你可以查看到C++中的所有的重载函数及参数。(注:M-?和“按两次TAB键”是一个意思)

要退出gdb时,只用发quit或命令简称q就行了。

GDB中运行UNIX的shell程序
————————————

在gdb环境中,你可以执行UNIX的shell的命令,使用gdb的shell命令来完成:

   shell <command string>
   调用UNIX的shell来执行<commandstring>,环境变量SHELL中定义的UNIX的shell将会被用来执行<commandstring>,如果SHELL没有定义,那就使用UNIX的标准shell:/bin/sh。(在Windows中使用Command.com或cmd.exe)

还有一个gdb命令是make:
    make<make-args>
   可以在gdb中执行make命令来重新build自己的程序。这个命令等价于“shell make<make-args>”。

在GDB中运行程序
————————

当以gdb<program>方式启动gdb后,gdb会在PATH路径和当前目录中搜索<program>的源文件。如要确认gdb是否读到源文件,可使用l或list命令,看看gdb是否能列出源代码。

在gdb中,运行程序使用r或是run命令。程序的运行,你有可能需要设置下面四方面的事。

1、程序运行参数。
    set args可指定运行时参数。(如:set args 10 20 30 40 50)
    show args命令可以查看设置好的运行参数。

2、运行环境。
    path<dir> 可设定程序的运行路径。
    show paths查看程序的运行路径。
    setenvironment varname [=value] 设置环境变量。如:set env USER=hchen
    showenvironment [varname] 查看环境变量。

3、工作目录。
    cd<dir> 相当于shell的cd命令。
    pwd显示当前的所在目录。

4、程序的输入输出。
    infoterminal 显示你程序用到的终端的模式。
   使用重定向控制程序输出。如:run > outfile
   tty命令可以指写输入输出的终端设备。如:tty /dev/ttyb

调试已运行的程序
————————

两种方法:
1、在UNIX下用ps查看正在运行的程序的PID(进程ID),然后用gdb<program> PID格式挂接正在运行的程序。
2、先用gdb<program>关联上源代码,并进行gdb,在gdb中用attach命令来挂接进程的PID。并用detach来取消挂接的进程。

暂停 / 恢复程序运行
—————————

调试程序中,暂停程序运行是必须的,GDB可以方便地暂停程序的运行。你可以设置程序的在哪行停住,在什么条件下停住,在收到什么信号时停往等等。以便于你查看运行时的变量,以及运行时的流程。

当进程被gdb停住时,你可以使用info program来查看程序的是否在运行,进程号,被暂停的原因。

在gdb中,我们可以有以下几种暂停方式:断点(BreakPoint)、观察点(WatchPoint)、捕捉点(CatchPoint)、信号(Signals)、线程停止(ThreadStops)。如果要恢复程序运行,可以使用c或是continue命令。

一、设置断点(BreakPoint)

我们用break命令来设置断点。正面有几点设置断点的方法:

break<function>
       在进入指定函数时停住。C++中可以使用class::function或function(type,type)格式来指定函数名。

   break <linenum>
       在指定行号停住。

   break +offset
    break-offset
       在当前行号的前面或后面的offset行停住。offiset为自然数。

   break filename:linenum
       在源文件filename的linenum行处停住。

   break filename:function
       在源文件filename的function函数的入口处停住。

   break *address
       在程序运行的内存地址处停住。

   break
       break命令没有参数时,表示在下一条指令处停住。

   break ... if <condition>
       ...可以是上述的参数,condition表示条件,在条件成立时停住。比如在循环境体中,可以设置break ifi=100,表示当i为100时停住程序。

   查看断点时,可使用info命令,如下所示:(注:n表示断点号)
    infobreakpoints [n]
    info break[n]

二、设置观察点(WatchPoint)

观察点一般来观察某个表达式(变量也是一种表达式)的值是否有变化了,如果有变化,马上停住程序。我们有下面的几种方法来设置观察点:

watch<expr>
       为表达式(变量)expr设置一个观察点。一量表达式值有变化时,马上停住程序。

rwatch<expr>
       当表达式(变量)expr被读时,停住程序。

awatch<expr>
       当表达式(变量)的值被读或被写时,停住程序。

infowatchpoints
       列出当前所设置了的所有观察点。

三、设置捕捉点(CatchPoint)

   你可设置捕捉点来补捉程序运行时的一些事件。如:载入共享库(动态链接库)或是C++的异常。设置捕捉点的格式为:

catch<event>
       当event发生时,停住程序。event可以是下面的内容:
       1、throw 一个C++抛出的异常。(throw为关键字)
       2、catch 一个C++捕捉到的异常。(catch为关键字)
       3、exec 调用系统调用exec时。(exec为关键字,目前此功能只在HP-UX下有用)
       4、fork 调用系统调用fork时。(fork为关键字,目前此功能只在HP-UX下有用)
       5、vfork 调用系统调用vfork时。(vfork为关键字,目前此功能只在HP-UX下有用)
       6、load 或 load <libname>载入共享库(动态链接库)时。(load为关键字,目前此功能只在HP-UX下有用)
       7、unload 或 unload <libname>卸载共享库(动态链接库)时。(unload为关键字,目前此功能只在HP-UX下有用)

   tcatch <event>
       只设置一次捕捉点,当程序停住以后,应点被自动删除。

使用gdb调试

我们将会使用GNU调试器,gdb,来调试这个程序。这是一个可以免费得到并且可以用于多个Unix平台的功能强大的调试器。他也是Linux系统上的默认调试器。gdb已经被移植到许多其他平台上,并且可以用于调试嵌入式实时系统。

启动gdb

让我们重新编译我们的程序用于调试并且启动gdb。

$ cc -g -o debug3 debug3.c
$ gdb debug3
GNU gdb 5.2.1
Copyright 2002 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License,and you are
welcome to change it and/or distribute copies of it under certainconditions.
Type “show copying” to see the conditions.
There is absolutely no warranty for GDB. Type “show warranty” fordetails.
This GDB was configured as “i586-suse-linux”...
(gdb)

gdb具有丰富的在线帮助,以及可以使用info程序进行查看或是在Emacs中进行查看的完整手册。

(gdb) help
List of classes of commands:
aliases — Aliases of other commands
breakpoints — Making program stop at certain points
data — Examining data
files — Specifying and examining files
internals — Maintenance commands
obscure — Obscure features
running — Running the program
stack — Examining the stack
status — Status inquiries
support — Support facilities
tracepoints — Tracing of program execution without stopping theprogram
user-defined — User-defined commands
Type “help” followed by a class name for a list of commands in thatclass.
Type “help” followed by command name for full documentation.
Command name abbreviations are allowed if unambiguous.
(gdb)

gdb本身是一个基于文本的程序,但是他确实了一些有助于重复任务的简化操作。许多版本具有一个命令行编辑历史,从而我们可以在命令历史中进行滚动并且再次执行相同的命令。所有的版本都支持一个"空白命令",敲击Enter会再次执行上一条命令。当我们使用step或是next命令在一个程序中分步执行特殊有用。

运行一个程序

我们可以使用run命令执行这个程序。我们为run命令所指定的所有命令都作为参数传递给程序。在这个例子中,我们并不需要任何参数。

我们在这里假设我们的系统与作者的类似,也产生了内存错误的错误信息。如果不是,请继续阅读。我们就会发现当我们自己的程序生成一个内存错误时应怎么办。如果我们并不没有得到内存错误信息,但是我们在阅读本书时希望运行这个例子,我们可以拾起第一个内存访问问题已经被修复的debug4.c。

(gdb) run
Starting program: /home/neil/BLP3/chapter10/debug3
Program received signal SIGSEGV, Segmentation fault.
0x080483c0 in sort (a=0x8049580, n=5) at debug3.c:23
23 

                                                      if(a[j].key > a[j+1].key) {
(gdb)

如前面一样,我们程序并没有正确运行。当程序失败时,gdb会向我们显示原因以及位置。现在我们可以检测问题背后的原因。

依据于我们的内核,C库,以及编译器选项,我们所看到的程序错误也许有所不同,例如,也许当数组元素交换时是在25行,而不是数组元素比较时的23行。如果是这种情况,我们也许会看到如下的输出:

Program received signal SIGSEGV, Segmentation fault.
0x8000613 in sort (a=0x8001764, n=5) at debug3.c:25
25                                                                        a[j] = a[j+1];

我们仍然可以遵循如下的gdb例子会话。

栈追踪

程序已经在源文件debug3.c的第23行处的sort函数停止。如果我们并没有使用额外的调试信息来编译这个程序,我们就不能看到程序在哪里失败,也不能使用变量名来检测数据。

我们可以通过使用backstrace命令来查看我们是如何到达这个位置的。

(gdb) backtrace
#0 0x080483c0 in sort (a=0x8049580, n=5) at debug3.c:23
#1 0x0804849b in main () at debug3.c:37
#2 0x400414f2 in __libc_start_main () from /lib/libc.so.6
(gdb)

这个是一个非常简单的程序,而且追踪信息很短小,因为我们并没有在其他的函数内部来调用许多函数。我们可以看到sort是由同一个文件debug3.c中37行处的main来调用的。通常,问题会更为复杂,而我们可以使用backtrace来发现我们到达错误位置的路径。

backtrace命令可以简写为bt,而且为了与其他调试器兼容,where命令也具有相同的功能。

检测变量

当程序停止时由gdb所输出的信息以及在栈追踪中的信息向我们显示了函数能数的值。

sort函数是使用一个参数a来调用的,而其值为0x8049580。这是数组的地址。依据于所使用的编译器以及操作系统,这个值在不同的操作系统也会不同。

所影响的行号23,是一个数组元素与另一个数组元素进行比较的地方。

if(a[j].key > a[j+1].key) {

我们可以使用调试器来检测函数参数,局部变量以及全局数据的内容。print命令可以向我们显示变量以及其他表达式的内容。

(gdb) print j
$1 = 4

在这里我们可以看到局部变量j的值为4。类似这样由gdb命令所报告的所有值都会保存在伪变量中以备将来使用。在这里变量$1赋值为4以防止我们在以后使用。以后的命令将他们的结果存储为$2,$3,依次类推。

j的值为4的事实意味着程序试着执行语句

if(a[4].key > a[4+1].key)

我们传递给sort的数组,array,只有5个元素,由0到4进行索引。所以这条语句读取并不存在的array[5]。循环变量j已经读取一个错误的值。

如果我们尝试这个例子,而我们程序在25行发生错误,我们系统只有在交互元素时才会检测到一个超过数组边界的读取,执行

a[j] = a[j+1];

此时将j设置为4,结果为

a[4] = a[4+1];

我们可以使用print通过表达式来查看所传递的数组元素。使用gdb,我们几乎可以使用任何合法的C表达式来输出变量,数组元素,以及指针的值。

(gdb) print a[3]
$2 = {data = “alex”, ‘\000’ <repeats 4091times>, key = 1}
(gdb)

gdb将命令的结果保存在一个伪变量中,$<number>。上一个结果总是为$,而之前的一个为$$。这可以使得在一个结果可以用在另一个命令中。例如,

(gdb) print j
$3 = 4
(gdb) print a[$-1].key
$4 = 1

列出程序

我们可以使用list命令在gdb内查看程序源代码。这会打印出当前位置周围的部分代码。持续的使用list会输出更多的代码。我们也可以为list指定一个行号或是函数名作为一个参数,而gdb就会显示那个位置的代码。

(gdb) list
18          int s = 1;
19         
20          for(; i < n && s !=0; i++) {
21                          s = 0;
22                          for(j = 0; j < n; j++) {
23                                          if(a[j].key > a[j+1].key) {
24                                                          item t = a[j];
25      a[j] =a[j+1];
26      a[j+1] =t;
27      s++;
(gdb)

我们可以看到在22行循环设置为当变量j小于n时才会执行。在这个例子中,n为5,所以j的最终值为4,总是小1。4会使得a[4]与a[5]进行比较并且有可能进行交换。这个问题的解决方法就是修正循环的结束条件为j< n-1。

让我们做出修改,将这个新程序称之为debug4.c,重新编译,并再次运行。

for(j = 0; j < n-1; j++) {

$ cc -g -o debug4 debug4.c
$ ./debug4
array[0] = {john, 2}
array[1] = {alex, 1}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}

程序仍不能正常工作,因为他输出了一个不正确的排序列表。下面我们使用gdb在程序运行时分步执行。

设置断点

查找出程序在哪里失败,我们需要能够查看程序运行他都做了什么。我们可以通过设置断点在任何位置停止程序。这会使得程序停止并将控制权返回调试器。我们将能够监视变量并且允许程序继续执行。

在sort函数中有两个循环。外层循环,使用循环变时i,对于数组中的每一个元素运行一次。内层循环将其与列表中的下一个元素进行交换。这具有将最小的元素交换到最上面的效果。在外层循环的每一次执行之后,最大的元素应位置底部。我们可通过在外层循环停止程序进行验证并且检测数组状态。

有许多命令可以用于设置断点。通过gdb的help breakpoint命令可以列表这些命令:

(gdb) help breakpoint
Making program stop at certain points.
List of commands:
awatch — Set a watchpoint for an expression
break — Set breakpoint at specified line or function
catch — Set catchpoints to catch events
clear — Clear breakpoint at specified line or function
commands — Set commands to be executed when a breakpoint ishit
condition — Specify breakpoint number N to break only if COND istrue
delete — Delete some breakpoints or auto-display expressions
disable — Disable some breakpoints
enable — Enable some breakpoints
hbreak — Set a hardware assisted breakpoint
ignore — Set ignore-count of breakpoint number N to COUNT
rbreak — Set a breakpoint for all functions matching REGEXP
rwatch — Set a read watchpoint for an expression
tbreak — Set a temporary breakpoint
tcatch — Set temporary catchpoints to catch events
thbreak — Set a temporary hardware assisted breakpoint
watch — Set a watchpoint for an expression
Type “help” followed by command name for full documentation.
Command name abbreviations are allowed if unambiguous.

让我们在20行设置一个断点并且运行这个程序:

$ gdb debug4
(gdb) break 20
Breakpoint 1 at 0x804835d: file debug4.c, line 20.
(gdb) run
Starting program: /home/neil/BLP3/chapter10/debug4
Breakpoint 1, sort (a=0x8049580, n=5) at debug4.c:20
20                        for(; i < n && s !=0; i++) {

我们可以输出数组值并且使用cont可以使得程序继续执行。这个会使得程序继续运行直到遇到下一个断点,在这个例子中,直到他再次执行到20行。在任何时候我们都可以有多个活动断点。

(gdb) print array[0]
$1 = {data = “bill”, ‘\000’ <repeats 4091times>, key = 3}

要输出多个连续的项目,我们可以使用@<number>结构使得gdb输出多个数组元素。要输出array的所有五个元素,我们可以使用

(gdb) print array[0]@5
$2 = {{data = “bill”, ‘\000’ <repeats 4091times>, key = 3}, {
      data =“neil”, ‘\000’ <repeats 4091 times>,key = 4}, {
      data =“john”, ‘\000’ <repeats 4091 times>,key = 2}, {
      data =“rick”, ‘\000’ <repeats 4091 times>,key = 5}, {
      data =“alex”, ‘\000’ <repeats 4091 times>,key = 1}}

注意,输出已经进行简单的处理从而使其更易于阅读。因为这是第一次循环,数组并没有发生变量。当我们允许程序继续执行,我们可以看到当处理执行时array的成功修改:

(gdb) cont
Continuing.
Breakpoint 1, sort (a=0x8049580, n=4) at debug4.c:20
20                        for(; i < n && s !=0; i++) {
(gdb) print array[0]@5
$3 = {{data = “bill”, ‘\000’ <repeats 4091times>, key = 3}, {
      data =“john”, ‘\000’ <repeats 4091 times>,key = 2}, {
      data =“neil”, ‘\000’ <repeats 4091 times>,key = 4}, {
      data =“alex”, ‘\000’ <repeats 4091 times>,key = 1}, {
      data =“rick”, ‘\000’ <repeats 4091 times>,key = 5}}
(gdb)

我们可以使用display命令来设置gdb当程序在断点处停止时自动显示数组:

(gdb) display array[0]@5
1: array[0] @ 5 = {{data = “bill”, ‘\000’ <repeats4091 times>, key = 3}, {
      data =“john”, ‘\000’ <repeats 4091 times>,key = 2}, {
      data =“neil”, ‘\000’ <repeats 4091 times>,key = 4}, {
      data =“alex”, ‘\000’ <repeats 4091 times>,key = 1}, {
      data =“rick”, ‘\000’ <repeats 4091 times>,key = 5}}

而且我们可以修改断点,从而他只是简单的显示我们所请求的数据并且继续执行,而不是停止程序。在这样做,我们可以使用commands命令。这会允许我们指定当遇到一个断点时执行哪些调试器命令。因为我们已经指定了一个display命令,我们只需要设置断点命令继续执行。

(gdb) commands
Type commands for when breakpoint 1 is hit, one per line.
End with a line saying just “end”.
> cont
> end

现在我们允许程序继续,他会运行完成,在每次运行到外层循环时输出数组的值。

(gdb) cont
Continuing.
Breakpoint 1, sort (a=0x8049684, n=3) at debug4.c:20
20                        for(; i < n && s !=0; i++) {
1: array[0] @ 5 = {{data = “john”, ‘\000’ <repeats4091 times>, key = 2}, {
      data =“bill”, ‘\000’ <repeats 4091 times>,key = 3}, {
      data =“alex”, ‘\000’ <repeats 4091 times>,key = 1}, {
      data =“neil”, ‘\000’ <repeats 4091 times>,key = 4}, {
      data =“rick”, ‘\000’ <repeats 4091 times>,key = 5}}
Breakpoint 1, sort (a=0x8049684, n=2) at debug4.c:20
20                        for(; i < n && s !=0; i++) {
1: array[0] @ 5 = {{data = “john”, ‘\000’ <repeats4091 times>, key = 2}, {
      data =“alex”, ‘\000’ <repeats 4091 times>,key = 1}, {
      data =“bill”, ‘\000’ <repeats 4091 times>,key = 3}, {
      data =“neil”, ‘\000’ <repeats 4091 times>,key = 4}, {
      data =“rick”, ‘\000’ <repeats 4091 times>,key = 5}}
array[0] = {john, 2}
array[1] = {alex, 1}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}
Program exited with code 044.
(gdb)

gdb报告程序并没有以通常的退出代码退出。这是因为程序本身并没有调用exit也没有由main返回一个值。在这种情况下,这个退出代码是无意义的,而一个有意义的退出代码应由调用exit来提供。

这个程序看起来似乎外层循环次数并不是我们所期望的。我们可以看到循环结束条件所使用的参数值n在每个断点处减小。这意味着循环并没有执行足够的次数。问题就在于30行处n的减小。

n--;

这是一个利用在每一次外层循环结束时array的最大元素都会位于底部的事实来优化程序的尝试,所以就会有更少的排序。但是,正如我们所看到的,这是与外层循环的接口,并且造成了问题。最简单的修正方法就是删除引起问题的行。让我们通过使用调试器来应用补丁测试这个修正是否有效。

使用调试进行补丁

我们已经看到了我们可以使用调试器来设置断点与检测变量的值。通过使用带动作的断点,我们可以在修改源代码与重新编译之前试验一个修正,称之为补丁。在这个例子中,我们需要在30行设置断点,并且增加变量n。然后,当30行执行,这个值将不会发生变化。

让我们从头启动程序。首先,我们必须删除我们的断点与显示。我们可以使用info命令来查看我们设置了哪些断点与显示:

(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
1:    y array[0] @ 5
(gdb) info break
NumType                      Disp EnbAddress      What
1    breakpoint          keep y    0x0804835d in sort atdebug4.c:20
                breakpoint already hit 4 times
                cont

我们可以禁止这些或是完全删除他们。如果我们禁止他们,那么我们可以在以后需要他们时重新允许这些设置:

(gdb) disable break 1
(gdb) disable display 1
(gdb) break 30
Breakpoint 2 at 0x8048462: file debug4.c, line 30.
(gdb) commands 2
Type commands for when breakpoint 2 is hit, one per line.
End with a line saying just “end”.
>set variable n = n+1
>cont
>end
(gdb) run
Starting program: /home/neil/BLP3/chapter10/debug4
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30                                        n--;
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30                                        n--;
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30                                        n--;
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30                                        n--;
Breakpoint 2, sort (a=0x8049580, n=5) at debug4.c:30
30                                      n--;
array[0] = {alex, 1}
array[1] = {john, 2}
array[2] = {bill, 3}
array[3] = {neil, 4}
array[4] = {rick, 5}
Program exited with code 044.
(gdb)

这个程序运行结束并且会输出正确的结果。现在我们可以进行修正并且继续使用更多的数据进行测试。

了解更多有关gdb的内容

GNU调试器是一个强大的工具,可以提供大量的有关运行程序内部状态的信 息。在支持一个名叫硬件断点(hardwarebreakpoint)的实用程序的系统上,我们可以使用gdb来实时的查看变量的改变。硬件断点是某些CPU的一个特性;如果出现特定的条件,通常是在指定区域的内存访问,这些处理器能够自动停止。相对应的,gdb可以使用watch表达式。这就意味着,出于性能考虑,当一个表达式具有一个特定的值时,gdb可以停止这个程序,而不论计算发生在程序中的哪个位置。

断点可以使用计数以及条件进行设置,从而他们只在一定的次数之后或是当满足一个条件时才会被引发。

gdb也能够将其本身附在已经运行的程序中。这对于我们调试客户端/服务器系统时是非常有用的,因为我们可以调试一个正在运行的行为不当的服务器进程,而不需要停止与重启服务器。例如,我们可以使用gcc -O-g选项来编译我们的程序,从而得到优化与调试的好处。不足之处就是优化也许会重新组织代码,所以当我们分步执行时,我们也许会发出我们自身的跳转来达到与原始源代码相同的效果。

我们也可以使用gdb来调试已经崩溃的程序。Linux与Unix经常会在一个程序失败时在一个名为core的文件中生成一个核心转储信息。这是一个程序内存的图象并且会包含失败时全局变量的值。我们可以使用gdb来查看当程序崩溃时程序运行到哪里。查看gdb手册页我们可以得到更为详细的信息。

gdb以GPL许可证发布并且绝大多数的Unix系统都会支持他。我们强烈建议了解gdb。

更多的调试工具

除了强大的调试工具,例如gdb,Linux系统通常还会提供一些其他们的我们可以用于诊治调试进程的工具。其中的一些会提供关于一个程序的静态信息;其他的会提供动态分析。

静态分析只由程序源代码提供信息。例如ctags,cxref,与cflow这样的程序与源代码一同工作,并且会提供有关函数调用与位置的有用信息。

动态会析会提供有关一个程序在执行过程中如何动作的信息。例如prof与gprof这样的程序会提供有关执行了哪个函数并且执行多长时间的信息。

下面我们来看一下其中的一些工具及其输出。并不是所有的这些工具都可以在所有的系统上得到,尽管其中的一些都有自由版本。

Lint:由我们的程序中移除Fluff

原始的Unix系统提供了一个名为lint的实用程序。他实质是一个C编译的前端,带有一个测试设计来适应一些常识并且生成警告。他会检测变量在设计之前在哪里使用以及哪里函数参数没有被使用,以及其他的一些情况。

更为现代的C编译器可以编译性能为代价提供类似的警告。lint本身已经被C标准所超越。因为这个工具是基于早期的C编译器,他并不能处理所有的ANSI语法。有一些商业版本的lint可以用于Unix,而且在网络上至少有一个名为splint可以用于Linux。这就是过去所知的LClint,他是MIT一个工程的一部分,来生成用于通常规范的工具。一个类似于lint的工具splint可以提供查看注释的有用代码。splint可以在htt://www.splin.org处得到。

下面是一个编辑过的splint例子输出,这是运行我们在前面调试的例子程序的早期版本中所产生的输出:

neil@beast:~/BLP3/chapter10> splint -strictdebug0.c
Splint 3.0.1.6 --- 27 Mar 2002
debug0.c:14:22: Old style function declaration

Function definition is in old style syntax.Standard prototype syntax is
  preferred. (Use -oldstyle to inhibitwarning)
debug0.c: (in function sort)
debug0.c:20:31: Variable s used before definition
  An rvalue is used that may not be initialized toa value on some execution
  path. (Use -usedef to inhibit warning)
debug0.c:20:23: Left operand of & is not unsignedvalue (boolean):
                                    i < n & s != 0
  An operand to a bitwise operator is not anunsigned values. This may have
  unexpected results depending on the signedrepresentations. (Use
  -bitwisesigned to inhibit warning)
debug0.c:20:23: Test expression for for not boolean, type unsignedint:
                                    i < n & s != 0
  Test expression type is not boolean or int. (Use-predboolint to inhibit
  warning)
debug0.c:20:23: Operands of & are non-integer(boolean) (in post loop test):
                                    i < n & s != 0
  A primitive operation does not type checkstrictly. (Use -strictops to
  inhibit warning)
debug0.c:32:14: Path with no return in function declared to returnint
  There is a path through a function declared toreturn a value on which there
  is no return statement. This means the executionmay fall through without
  returning a meaningful result to the caller.(Use -noret to inhibit warning)
debug0.c:34:13: Function main declared without parameter list
  A function declaration does not have a parameterlist. (Use -noparams to
  inhibit warning)
debug0.c: (in function main)
debug0.c:36:17: Return value (type int) ignored: sort(array,5)
  Result returned by function call is not used. Ifthis is intended, can cast
  result to (void) to eliminate message. (Use-retvalint to inhibit warning)
debug0.c:37:14: Path with no return in function declared to returnint
debug0.c:14:13: Function exported but not used outside debug0:sort
    debug0.c:15:17: Definition ofsort
Finished checking --- 22 code warnings
$

这个程序报告旧风格的函数定义以及函数返回类型与他们实际返回类型之间的不一致。这些并不会影响程序的操作,但是应该注意。

他还在下面的代码片段中检测到两个实在的bug:

int s;

for(; i < n & s != 0; i++) {
                s = 0;

splint已经确定在20行使用了变量s,但是并没有进行初始化,而且操作符&已经被更为通常的&&所替代。在这个例子中,操作符优先级修改了测试的意义并且是程序的一个问题。

所有这些错误都在调试开始之前在代码查看中被修正。尽管这个例子有一个故意演示的目的,但是这些错误真实世界的程序中经常会出现的。

函数调用工具

三个实用程序-ctags,cxref与cflow-形成了X/Open规范的部分,所以必须在具有软件开发功能的Unix分枝系统上提供。

ctags

ctags程序创建函数索引。对于每一个函数,我们都会得到一个他在何处使用的列表,与书的索引类似。

ctags [-a] [-f filename] sourcefile sourcefile ...
ctags -x sourcefile sourcefile ...

默认情况下,ctags在当前目录下创建一个名为tags的目录,其中包括在输入源文件码中所声明的每一个函数,如下面的格式

announce app_ui.c /^static void announce(void) /

文件中的每一行由一个函数名,其声明所在的文件,以及一个可以用在文件中查找到函数定义所用的正则表达式所组成。一些编辑器,例如Emacs可以使用这种类型的文件在源码中遍历。

相对应的,通过使用ctags的-x选项,我们可以在标准输出上产生类似格式的输出:

find_cat 403 app_ui.c static cdc_entry find_cat(

我们可以通过使用-ffilename选项将输出重定向到另一个不同的文件中,或是通过指定-a选项将其添加到一个已经存在的文件中。

cxref

cxref程序分析C源代码并且生成一个交叉引用。他显示了每一个符号在程序中何处被提到。他使用标记星号的每一个符号定义位置生成一个排序列表,如下所示:

SYMBOL    FILE      FUNCTIONLINE
  BASENID  prog.c          —      *12 *96 124126 146 156 166
  BINSIZE  prog.c          —      *30 197 198199 206
  BUFMAX                            prog.c  —    *44  45 90
  BUFSIZ /usr/include/stdio.h  —      *4
        EOF /usr/include/stdio.h  —    *27
      argc                            prog.c  —      36
                                            prog.c main *37  61 81
      argv                            prog.c  —      36
                                            prog.c main *38  61
calldata                            prog.c  —      *5
                                            prog.c main  64 188
    calls                            prog.c  —    *19
                                            prog.c main  54

在作者的机子上,前面的输入在程序的源码目录中使用下面的命令来生成的:

$ cxref *.c *.h

但是实际的语法因为版本的不同而不同。查看我们系统的文档或是man手册可以得到更多的信息。

cflow

cflow程序会输出一个函数调用树,这是一个显示函数调用关系的图表。这对于查看程序结构来了解他是如何操作的以及了解对于一个函数有哪些影响是十分有用的。一些版本的cflow可以同时作用于目标文件与源代码。查看手册页我们可以了解更为详细的操作。

下面是由一个cflow版本(cflow-2.0)所获得的例子输出,这个版本的cflow版本是由MartyLeisner维护的,并且可以网上得到。

1  file_ungetc {prcc.c 997}
2  main {prcc.c 70}
3                  getopt {}
4                  show_all_lists {prcc.c 1070}
5                                    display_list {prcc.c 1056}
6                                                    printf {}
7                                    exit {}
8                  exit {}
9                  usage {prcc.c 59}
10                                  fprintf {}
11                                  exit {}

从这个输出中我们可以看到main函数调用show_all_lists,而show_all_lists调用display_list,display_list本身调用printf。

这个版本cflow的一个选项就是-i,这会生成一个反转的流程图。对于每一个函数,cflow列出调用他的其他函数。这听起来有些复杂,但是实际上并不是这样。下面是一个例子。

19 display_list {prcc.c 1056}
20                show_all_lists {prcc.c 1070}
21 exit {}
22                main {prcc.c 70}
23                  show_all_lists {prcc.c 1070}
24                  usage {prcc.c 59}
...
74  printf {}
75                  display_list {prcc.c 1056}
76                  maketag {prcc.c 487}
77  show_all_lists {prcc.c 1070}
78                  main {prcc.c 70}
...
99  usage {prcc.c 59}
100                main {prcc.c 70}

例如,这告诉我们调用exit的函数有main,show_all_lists与usage。

使用prof/gprof执行性能测试

当我们试着追踪一个程序的性能问题时一个十分有用的技术就是执行性能测试(executionprofiling)。通常被特殊的编译器选项以及辅助程序所支持,一个程序的性能显示他在哪里花费时间。

prof程序(以及其GNU版本gprof)会由性能测试程序运行时所生成的执行追踪文件中输出报告。一个可执行的性能测试是由指定-p选项(对prof)或是-pg选项(对gprof)所生成的:

$ cc -pg -o program program.c

这个程序是使用一个特殊版本的C库进行链接的并且被修改来包含监视代码。对于不同的系统结果也许不同,但是通常是由安排频繁被中断的程序以及记录执行位置来做到的。监视数据被写入当前目录中的一个文件,mon.out(对于gprof为gmon.out)。

$ ./program
$ ls -ls
    2 -rw-r--r-- 1 neil users 1294Feb 4 11:48 gmon.out

prof/gprof程序读取这些监视数据并且生成一个报告。查看其手册页可以详细了解其程序选项。下面以gprof输出作为一个例子:

cumulative    self      self    total
    time      secondsseconds  calls ms/callms/call                      name
    18.5            0.10      0.10    8664      0.01      0.03        _doscan [4]
    18.5            0.20      0.10                                                      mcount (60)
    14.8            0.28      0.08  43320      0.00      0.00        _number [5]
      9.3            0.33      0.05    8664      0.01      0.01_format_arg [6]
      7.4            0.37      0.04112632      0.00      0.00        _ungetc [8]
      7.4            0.41      0.04    8757      0.00      0.00      _memccpy[9]
      7.4            0.45      0.04          1    40.00  390.02            _main [2]
      3.7            0.47      0.02        53      0.38      0.38          _read [12]
      3.7            0.49      0.02                                                        w4str [10]
      1.9            0.50      0.01  26034      0.00      0.00      _strlen[16]
      1.9            0.51      0.01    8664      0.00      0.00      strncmp[17]

断言

在程序的开发过程中,通常使用条件编译的方法引入调试代码,例如printf,但是在一个发布的系统中保留这些信息是不实际的。然而,经常的情况是问题出现与不正确的假设相关的程序操作过程中,而不是代码错误。这些都是"不会发生"的事件。例如,一个函数也许是在认为其输入参数总是在一定范围下而编写的。如果给他传递一些不正确的数据,也许整个系统就会崩溃。

对于这些情况,系统的内部逻辑在哪里需要验证,X/Open提供了assert宏,可以用来测试一个假设是否正确,如果不正确则会停止程序。

#include <assert.h>
void assert(int expression)

assert宏会计算表达式的值,如果不为零,则会向标准错误上输出一些诊断信息,并且调用abort来结束程序。

头文件assert.h依据NDEBUG的定义来定义宏。如果头文件被处理时定义了NDEBUG,assert实质上被定义为空。这就意味着我们可以通过使用-DNDEBUG在编译时关闭断言或是在包含assert.h文件之前包含下面这行:

#define NDEBUG

这种方法的使用是assert的一个问题。如果我们在测试中使用assert,但是却对生产代码而关闭,比起我们测试时的代码,我们的生产代码就不会太安全。在生产代码中保留断言开启状态并不是通常的选择,我们希望我们的代码向用户显示一条不友好的错误assertfailed与一个停止的程序吗?我们也许会认为最好是编写我们自己的检测断言的错误追踪例程,而不必在我们的生产代码中完全禁止。

我们同时要小心在assert断言没有临界效果。例如,如果我们在一个临界效果中调用一个函数,如果移除了断言,在生产代码中就不会出现这个效果。

试验--assert

下面的程序assert.c定义了一个必须传递正值参数的函数。通过使用一个断言可以避免不正常参数的可能。

在包含assert.h头文件和检测参数是否为正的平方根函数之后,我们可以编写如下的函数:

#include <stdio.h>
#include <math.h>
#include <assert.h>
double my_sqrt(double x)
{
      assert(x>= 0.0);
      returnsqrt(x);
}
int main()
{
      printf(“sqrt+2 = %g\n”, my_sqrt(2.0));
      printf(“sqrt-2 = %g\n”, my_sqrt(-2.0));
      exit(0);
}

当我们运行这个程序时,我们就会看到当我们传递一个非法值时就会违背这个断言。事实上的断言失败的消息格式会因系统的不同而不同。

$ cc -o assert assert.c -lm
$ ./assert
sqrt +2 = 1.41421
assert: assert.c:7: my_sqrt: Assertion `x >= 0.0’failed.
Aborted
$

工作原理

当我们试着使用一个负数来调用函数my_sqrt时,断言就会失败。assert宏会提供违背断言的文件和行号,以及失败的条件。程序以一个退出陷井结束。这是assert调用abort的结果。

如果我们使用-DNDEBUG选项来编译这个程序,断言就会被编译在外,而当我们由my_sqrt中调用sqrt函数时我们就会得到一个算术错误。

$ cc -o assert -DNDEBUG assert.c -lm
$ ./assert
sqrt +2 = 1.41421
Floating point exception
$

一些最近的算术库版本会返回一个NaN(Not a Number)值来表示一个不可用的结果。

sqrt –2 = nan

内存调试

富含bug而且难于跟踪调试的一个区域就是动态内存分配。如果我们编译一个使用malloc与free来分配内存的程序,很重要的一点就是我们要跟踪我们所分配的内存块,并且保证不要使用已经释放的内存块。

通常,内存是由malloc分配并且赋给一个指针变量的。如果指针变量被修改了,而又没有其他的指针来指向这个内存块,他就会变为不可访问的内存块。这就是一个内存泄露,而且会使得我们程序尺寸变大。如果我们泄露了大量的内存,那么我们的系统就会变慢并且会最终用尽内存。

如果我们在超出一个分配的内存块的结束部分(或是在一个内存块的开始部分)写入数据,我们很有可能会破坏malloc库来跟踪分配所用的数据结构。在这种情况下,在将来的某个时刻,调用malloc,或者甚至是free,就会引起段错误,而我们的程序就会崩溃。跟踪错误发生的精确点是非常困难的,因为很可能他在引起崩溃的事件发生以前很一段时间就已经发生了。

不必奇怪的是,有一些工具,商业或是自由的,可以有助于处理这两种问题类型。例如,有许多不同的malloc与free版本,其中的一些包含额外的代码在分配与回收上进行检测尝试检测一个内存块被释放两次或是其他一些滥用类型的情况。

ElectricFence

ElectricFence 库是由BrucePerens开发的,并且在一些Linux发行版本中作为一个可选的组件来提供,例如RedHat,而且已经可以在网络上获得。他尝试使用Linux的虚拟内存工具来保护malloc与free所使用的内存,从而在内存被破坏时终止程序。

试验--ElectricFence

下面的程序,efence.c,使用malloc分配一个内存块,然后在超出块结束处写入数据。让我们看一下会发生什么情况。

#include <stdio.h>
#include <stdlib.h>
int main()
{
      char *ptr =(char *) malloc(1024);
      ptr[0] =0;

ptr[1024] =0;
      exit(0);
}

当我们编译运行这个程序时,我们并不会看到进一步的行为。然而,似乎malloc所分配的内存区域有一些问题,而我们实际上已经遇到了麻烦。

$ cc -o efence efence.c
$ ./efence
$

然而,如果我们使用ElectricFence库,libefence.a来链接这个程序,我们就会得到一个即时的响应。

$ cc -o efence efence.c -lefence
$ ./efence
  Electric Fence 2.2.0 Copyright (C) 1987-1999Bruce Perens <[email protected]>
Segmentation fault
$

在调试器下运行可以定位这个问题:

$ cc -g -o efence efence.c -lefence
$ gdb efence
  (gdb) run
Starting program: /home/neil/BLP3/chapter10/efence
[New Thread 1024 (LWP 1869)]
    Electric Fence 2.2.0 Copyright(C) 1987-1999 Bruce Perens<[email protected]>
Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 1024 (LWP 1869)]
0x080484ad in main () at efence.c:10
10                    ptr[1024] = 0;
(gdb)

工作原理

Electric替换malloc并且将函数与计算机处理器的虚拟内存特性相关联来阻止非法的内存访问。当这样的访问发生时,就会抛出一个段错误信息从而可以终止程序。

valgrind

valgrind是一个可以检测我们已经讨论过的许多问题的工具。事实上,他可以检测数据访问错误与内存泄露。也许他并没有被包含在我们的Linux发行版本中,但是我们可以在http://developer.kde.org/~sewardj处得到。

程序并不需要使用valgrind重新编译,而我们甚至可以调用一个正在运行的程序的内存访问。他很值得一看,他已经用在主要的开发上,包含KDE版本3。

试验--valgrind

下面的程序,checker.c,分配一些内存,读取超过那块内存限制的位置,在其结束处之外写入数据,然后使其不能访问。

#include <stdio.h>
#include <stdlib.h>
int main()
{
      char *ptr =(char *) malloc(1024);
      charch;

ch =ptr[1024];

ptr[1024] =0;

ptr = 0;
  exit(0);
}

要使用valgrind,我们只需要简单的运行valgrind命令,传递我们希望检测的选项,其后是使用其参数运行的程序。

当我们使用valgrind来运行我们的程序时,我们可以看到诊断出许多问题:

$ valgrind --leak-check=yes -v ./checker
==3436== valgrind-1.0.4, a memory error detector for x86GNU/Linux.
==3436== Copyright (C) 2000-2002, and GNU GPL’d, by JulianSeward.
==3436== Estimated CPU clock rate is 452 MHz
==3436== For more details, rerun with: -v
==3436==
==3436== Invalid read of size 1
==3436==      at0x8048397: main (checker.c:10)
==3436==      by0x402574F2: __libc_start_main (in /lib/libc.so.6)
==3436==      by0x80482D1: exit@@GLIBC_2.0 (in/home/neil/BLP3/chapter10/checker)
==3436==      Address 0x42AD1424 is 0 bytes after a block of size 1024alloc’d
==3436==      at0x4003CA75: malloc (vg_clientfuncs.c:100)
==3436==      by0x8048389: main (checker.c:6)
==3436==      by0x402574F2: __libc_start_main (in /lib/libc.so.6)
==3436==      by0x80482D1: exit@@GLIBC_2.0 (in/home/neil/BLP3/chapter10/checker)
==3436==
==3436== Invalid write of size 1
==3436==      at0x80483A4: main (checker.c:13)
==3436==      by0x402574F2: __libc_start_main (in /lib/libc.so.6)
==3436==      by0x80482D1: exit@@GLIBC_2.0 (in/home/neil/BLP3/chapter10/checker)
==3436==      Address 0x42AD1424 is 0 bytes after a block of size 1024alloc’d
==3436==      at0x4003CA75: malloc (vg_clientfuncs.c:100)
==3436==      by0x8048389: main (checker.c:6)
==3436==      by0x402574F2: __libc_start_main (in /lib/libc.so.6)
==3436==      by0x80482D1: exit@@GLIBC_2.0 (in/home/neil/BLP3/chapter10/checker)
==3436==
==3436== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0from 0)
==3436== malloc/free: in use at exit: 1024 bytes in 1 blocks.
==3436== malloc/free: 1 allocs, 0 frees, 1024 bytesallocated.
==3436== For counts of detected errors, rerun with: -v
==3436== searching for pointers to 1 not-freed blocks.
==3436== checked 3468724 bytes.
==3436==
==3436== definitely lost: 1024 bytes in 1 blocks.
==3436== possibly lost:    0 bytesin 0 blocks.
==3436== still reachable: 0 bytes in 0 blocks.
==3436==
==3436== 1024 bytes in 1 blocks are definitely lost in loss record1 of 1
==3436==      at0x4003CA75: malloc (vg_clientfuncs.c:100)
==3436==      by0x8048389: main (checker.c:6)
==3436==      by0x402574F2: __libc_start_main (in /lib/libc.so.6)
==3436==      by0x80482D1: exit@@GLIBC_2.0 (in/home/neil/BLP3/chapter10/checker)
==3436==
==3436== LEAK SUMMARY:
==3436==      definitely lost: 1024 bytes in 1 blocks.
==3436==      possibly lost:    0 bytes in 0blocks.
==3436==      still reachable: 0 bytes in 0 blocks.
==3436== Reachable blocks (those to which a pointer was found) arenot shown.
==3436== To see them, rerun with: --show-reachable=yes
==3436== $

这里我们可以看到错误的读取与写入已经被捕获,而所关注的内存块与他们被分配的位置相关联。我们可以使用调试器在出错点断开程序。

valgrind有许多选项,包含特定的错误类型表达式与内存泄露检测。要检测我们的例子泄露,我们必须使用一个传递给valgrind的选项。当程序结束时要检测内存泄露,我们需要指定 --leak-check=yes。我们可以使用valgrind --help得到一个选项列表。

工作原理

我们的程序在valgrind的控制下执行,这会检测我们程序所执行的各种动作,并且执行许多检测,包括内存访问。如果程序访问一个已分配的内存块并且访问是非法的,valgrind就会输出一条信息。在程序结束时,一个垃圾收集例程就会运行来检测是否在存在分配的内存块没有被释放。这些孤儿内存也会被报告。

小结

在这一章,我们了解了一些调试工具与技术。Linux提供了一些强大的工具可以用于由程序中移除缺陷。我们使用gdb来消除程序中的bug,并且了解了如cflow与splint这样的数据分析工具。最后我们了解了当我们使用动态分配内存时会出现的问题,以及一些用于类似问题诊断的工具,例如ElectricFence与valgrind。

查看运行时数据
———————


在你调试程序时,当程序被停住时,你可以使用print命令(简写命令为p),或是同义命令inspect来查看当前程序的运行数据。print命令的格式是:

print<expr>
    print/<f><expr>
       <expr>是表达式,是你所调试的程序的语言的表达式(GDB可以调试多种编程语言),<f>是输出的格式,比如,如果要把表达式按16进制的格式输出,那么就是/x。


一、表达式

   print和许多GDB的命令一样,可以接受一个表达式,GDB会根据当前的程序运行的数据来计算这个表达式,既然是表达式,那么就可以是当前程序运行中的const常量、变量、函数等内容。可惜的是GDB不能使用你在程序中所定义的宏。

表达式的语法应该是当前所调试的语言的语法,由于C/C++是一种大众型的语言,所以,本文中的例子都是关于C/C++的。(而关于用GDB调试其它语言的章节,我将在后面介绍)

在表达式中,有几种GDB所支持的操作符,它们可以用在任何一种语言中。

@
       是一个和数组有关的操作符,在后面会有更详细的说明。

::
       指定一个在文件或是一个函数中的变量。

{<type>}<addr>
       表示一个指向内存地址<addr>的类型为type的一个对象。


二、程序变量

   在GDB中,你可以随时查看以下三种变量的值:
       1、全局变量(所有文件可见的)
       2、静态全局变量(当前文件可见的)
       3、局部变量(当前Scope可见的)

如果你的局部变量和全局变量发生冲突(也就是重名),一般情况下是局部变量会隐藏全局变量,也就是说,如果一个全局变量和一个函数中的局部变量同名时,如果当前停止点在函数中,用print显示出的变量的值会是函数中的局部变量的值。如果此时你想查看全局变量的值时,你可以使用“::”操作符:

file::variable
   function::variable
   可以通过这种形式指定你所想查看的变量,是哪个文件中的或是哪个函数中的。例如,查看文件f2.c中的全局变量x的值:

gdb) p'f2.c'::x

当然,“::”操作符会和C++中的发生冲突,GDB能自动识别“::”是否C++的操作符,所以你不必担心在调试C++程序时会出现异常。

另外,需要注意的是,如果你的程序编译时开启了优化选项,那么在用GDB调试被优化过的程序时,可能会发生某些变量不能访问,或是取值错误码的情况。这个是很正常的,因为优化程序会删改你的程序,整理你程序的语句顺序,剔除一些无意义的变量等,所以在GDB调试这种程序时,运行时的指令和你所编写指令就有不一样,也就会出现你所想象不到的结果。对付这种情况时,需要在编译程序时关闭编译优化。一般来说,几乎所有的编译器都支持编译优化的开关,例如,GNU的C/C++编译器GCC,你可以使用“-gstabs”选项来解决这个问题。关于编译器的参数,还请查看编译器的使用说明文档。

三、数组

   有时候,你需要查看一段连续的内存空间的值。比如数组的一段,或是动态分配的数据的大小。你可以使用GDB的“@”操作符,“@”的左边是第一个内存的地址的值,“@”的右边则你你想查看内存的长度。例如,你的程序中有这样的语句:

int *array = (int *) malloc (len * sizeof (int));

于是,在GDB调试过程中,你可以以如下命令显示出这个动态数组的取值:

       p *array@len

   @的左边是数组的首地址的值,也就是变量array所指向的内容,右边则是数据的长度,其保存在变量len中,其输出结果,大约是下面这个样子的:

(gdb) p *array@len
       $1 = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32,34, 36, 38, 40}

   如果是静态数组的话,可以直接用print数组名,就可以显示数组中所有数据的内容了。

四、输出格式

   一般来说,GDB会根据变量的类型输出变量的值。但你也可以自定义GDB的输出的格式。例如,你想输出一个整数的十六进制,或是二进制来查看这个整型变量的中的位的情况。要做到这样,你可以使用GDB的数据显示格式:

x  按十六进制格式显示变量。
   d  按十进制格式显示变量。
   u  按十六进制格式显示无符号整型。
   o  按八进制格式显示变量。
   t  按二进制格式显示变量。
   a  按十六进制格式显示变量。
   c  按字符格式显示变量。
   f  按浮点数格式显示变量。

       (gdb) p i
       $21 =101   

(gdb) p/a i
       $22 = 0x65

(gdb) p/c i
       $23 = 101 'e'

(gdb) p/f i
       $24 = 1.41531145e-43

(gdb) p/x i
       $25 = 0x65

(gdb) p/t i
       $26 = 1100101

五、查看内存

   你可以使用examine命令(简写是x)来查看内存地址中的值。x命令的语法如下所示:

x/<n/f/u><addr>

n、f、u是可选的参数。

n是一个正整数,表示显示内存的长度,也就是说从当前地址向后显示几个地址的内容。
    f表示显示的格式,参见上面。如果地址所指的是字符串,那么格式可以是s,如果地十是指令地址,那么格式可以是i。
    u表示从当前地址往后请求的字节数,如果不指定的话,GDB默认是4个bytes。u参数可以用下面的字符来代替,b表示单字节,h表示双字节,w表示四字节,g表示八字节。当我们指定了字节长度后,GDB会从指内存定的内存地址开始,读写指定字节,并把其当作一个值取出来。

<addr>表示一个内存地址。

   n/f/u三个参数可以一起使用。例如:

命令:x/3uh0x54320 表示,从内存地址0x54320读取内容,h表示以双字节为一个单位,3表示三个单位,u表示按十六进制显示。


六、自动显示

   你可以设置一些自动显示的变量,当程序停住时,或是在你单步跟踪时,这些变量会自动显示。相关的GDB命令是display。

display<expr>
   display/<fmt><expr>
   display/<fmt><addr>

expr是一个表达式,fmt表示显示的格式,addr表示内存地址,当你用display设定好了一个或多个表达式后,只要你的程序被停下来,GDB会自动显示你所设置的这些表达式的值。

格式i和s同样被display支持,一个非常有用的命令是:

display/i $pc

$pc是GDB的环境变量,表示着指令的地址,/i则表示输出格式为机器指令码,也就是汇编。于是当程序停下后,就会出现源代码和机器指令码相对应的情形,这是一个很有意思的功能。

下面是一些和display相关的GDB命令:

undisplay<dnums...>
    deletedisplay <dnums...>
   删除自动显示,dnums意为所设置好了的自动显式的编号。如果要同时删除几个,编号可以用空格分隔,如果要删除一个范围内的编号,可以用减号表示(如:2-5)

disabledisplay <dnums...>
    enabledisplay <dnums...>
   disable和enalbe不删除自动显示的设置,而只是让其失效和恢复。

infodisplay
   查看display设置的自动显示的信息。GDB会打出一张表格,向你报告当然调试中设置了多少个自动显示设置,其中包括,设置的编号,表达式,是否enable。

七、设置显示选项

   GDB中关于显示的选项比较多,这里我只例举大多数常用的选项。

   set print address
    set printaddress on
       打开地址输出,当程序显示函数信息时,GDB会显出函数的参数地址。系统默认为打开的,如:

(gdb) f
       #0  set_quotes (lq=0x34c78"<<", rq=0x34c88">>")
           at input.c:530
       530        if (lquote != def_lquote)

   set print address off
       关闭函数的参数地址显示,如:

(gdb) set print addr off
       (gdb) f
       #0  set_quotes(lq="<<",rq=">>") at input.c:530
       530        if (lquote != def_lquote)

   show print address
       查看当前地址显示选项是否打开。

set printarray
    set printarray on
       打开数组显示,打开后当数组显示时,每个元素占一行,如果不打开的话,每个元素则以逗号分隔。这个选项默认是关闭的。与之相关的两个命令如下,我就不再多说了。

set printarray off
    show printarray

   set print elements<number-of-elements>
       这个选项主要是设置数组的,如果你的数组太大了,那么就可以指定一个<number-of-elements>来指定数据显示的最大长度,当到达这个长度时,GDB就不再往下显示了。如果设置为0,则表示不限制。

show printelements
       查看print elements的选项信息。

set printnull-stop <on/off>
       如果打开了这个选项,那么当显示字符串时,遇到结束符则停止显示。这个选项默认为off。

set printpretty on
       如果打开printf pretty这个选项,那么当GDB显示结构体时会比较漂亮。如:

           $1 = {
             next = 0x0,
             flags = {
               sweet = 1,
               sour = 1
             },
             meat = 0x54 "Pork"
           }

   set print pretty off
       关闭printf pretty这个选项,GDB显示结构体时会如下显示:

$1 = {next = 0x0, flags = {sweet = 1, sour = 1}, meat = 0x54"Pork"}

show printpretty
       查看GDB是如何显示结构体的。


    set printsevenbit-strings <on/off>
       设置字符显示,是否按“\nnn”的格式显示,如果打开,则字符串或字符数据按\nnn显示,如“\065”。

show printsevenbit-strings
       查看字符显示开关是否打开。

set printunion <on/off>
       设置显示结构体时,是否显式其内的联合体数据。例如有以下数据结构:

typedef enum {Tree, Bug} Species;
       typedef enum {Big_tree, Acorn, Seedling} Tree_forms;
       typedef enum {Caterpillar, Cocoon, Butterfly}
                     Bug_forms;

struct thing {
         Species it;
         union {
           Tree_forms tree;
           Bug_forms bug;
         } form;
       };

struct thing foo = {Tree, {Acorn}};

       当打开这个开关时,执行 p foo 命令后,会如下显示:
           $1 = {it = Tree, form = {tree = Acorn, bug = Cocoon}}

当关闭这个开关时,执行 p foo 命令后,会如下显示:
           $1 = {it = Tree, form = {...}}

   show print union
       查看联合体数据的显示方式

set printobject <on/off>
       在C++中,如果一个对象指针指向其派生类,如果打开这个选项,GDB会自动按照虚方法调用的规则显示输出,如果关闭这个选项的话,GDB就不管虚函数表了。这个选项默认是off。

show printobject
       查看对象选项的设置。

set printstatic-members <on/off>
       这个选项表示,当显示一个C++对象中的内容是,是否显示其中的静态数据成员。默认是on。

show printstatic-members
       查看静态数据成员选项设置。

set printvtbl <on/off>
       当此选项打开时,GDB将用比较规整的格式来显示虚函数表时。其默认是关闭的。

show printvtbl
       查看虚函数显示格式的选项。

       
八、历史记录

   当你用GDB的print查看程序运行时的数据时,你每一个print都会被GDB记录下来。GDB会以$1, $2, $3.....这样的方式为你每一个print命令编上号。于是,你可以使用这个编号访问以前的表达式,如$1。这个功能所带来的好处是,如果你先前输入了一个比较长的表达式,如果你还想查看这个表达式的值,你可以使用历史记录来访问,省去了重复输入。


九、GDB环境变量

   你可以在GDB的调试环境中定义自己的变量,用来保存一些调试程序中的运行数据。要定义一个GDB的变量很简单只需。使用GDB的set命令。GDB的环境变量和UNIX一样,也是以$起头。如:

set $foo =*object_ptr

使用环境变量时,GDB会在你第一次使用时创建这个变量,而在以后的使用中,则直接对其賦值。环境变量没有类型,你可以给环境变量定义任一的类型。包括结构体和数组。

showconvenience
       该命令查看当前所设置的所有的环境变量。

这是一个比较强大的功能,环境变量和程序变量的交互使用,将使得程序调试更为灵活便捷。例如:

set $i = 0
       print bar[$i++]->contents

于是,当你就不必,print bar[0]->contents, printbar[1]->contents地输入命令了。输入这样的命令后,只用敲回车,重复执行上一条语句,环境变量会自动累加,从而完成逐个输出的功能。


十、查看寄存器

   要查看寄存器的值,很简单,可以使用如下命令:

inforegisters
       查看寄存器的情况。(除了浮点寄存器)

infoall-registers
       查看所有寄存器的情况。(包括浮点寄存器)

inforegisters <regname ...>
       查看所指定的寄存器的情况。

寄存器中放置了程序运行时的数据,比如程序当前运行的指令地址(ip),程序的当前堆栈地址(sp)等等。你同样可以使用print命令来访问寄存器的情况,只需要在寄存器名字前加一个$符号就可以了。如:p$eip。
改变程序的执行
———————

   一旦使用GDB挂上被调试程序,当程序运行起来后,你可以根据自己的调试思路来动态地在GDB中更改当前被调试程序的运行线路或是其变量的值,这个强大的功能能够让你更好的调试你的程序,比如,你可以在程序的一次运行中走遍程序的所有分支。

   
一、修改变量值

   修改被调试程序运行时的变量值,在GDB中很容易实现,使用GDB的print命令即可完成。如:

(gdb) print x=4

x=4这个表达式是C/C++的语法,意为把变量x的值修改为4,如果你当前调试的语言是Pascal,那么你可以使用Pascal的语法:x:=4。

在某些时候,很有可能你的变量和GDB中的参数冲突,如:

(gdb) whatis width
       type = double
       (gdb) p width
       $4 = 13
       (gdb) set width=47
       Invalid syntax in expression.

   因为,set width是GDB的命令,所以,出现了“Invalid syntax inexpression”的设置错误,此时,你可以使用setvar命令来告诉GDB,width不是你GDB的参数,而是程序的变量名,如:

(gdb) set var width=47

另外,还可能有些情况,GDB并不报告这种错误,所以保险起见,在你改变程序变量取值时,最好都使用setvar格式的GDB命令。

二、跳转执行

   一般来说,被调试程序会按照程序代码的运行顺序依次执行。GDB提供了乱序执行的功能,也就是说,GDB可以修改程序的执行顺序,可以让程序执行随意跳跃。这个功能可以由GDB的jump命令来完:

jump<linespec>
   指定下一条语句的运行点。<linespce>可以是文件的行号,可以是file:line格式,可以是+num这种偏移量格式。表式着下一条运行语句从哪里开始。

jump<address>
   这里的<address>是代码行的内存地址。

注意,jump命令不会改变当前的程序栈中的内容,所以,当你从一个函数跳到另一个函数时,当函数运行完返回时进行弹栈操作时必然会发生错误,可能结果还是非常奇怪的,甚至于产生程序CoreDump。所以最好是同一个函数中进行跳转。

熟悉汇编的人都知道,程序运行时,有一个寄存器用于保存当前代码所在的内存地址。所以,jump命令也就是改变了这个寄存器中的值。于是,你可以使用“set$pc”来更改跳转执行的地址。如:

set $pc =0x485

三、产生信号量

   使用singal命令,可以产生一个信号量给被调试的程序。如:中断信号Ctrl+C。这非常方便于程序的调试,可以在程序运行的任意位置设置断点,并在该断点用GDB产生一个信号量,这种精确地在某处产生信号非常有利程序的调试。

语法是:signal<singal>,UNIX的系统信号量通常从1到15。所以<singal>取值也在这个范围。

single命令和shell的kill命令不同,系统的kill命令发信号给被调试程序时,是由GDB截获的,而single命令所发出一信号则是直接发给被调试程序的。

四、强制函数返回

   如果你的调试断点在某个函数中,并还有语句没有执行完。你可以使用return命令强制函数忽略还没有执行的语句并返回。

return
    return<expression>
   使用return命令取消当前函数的执行,并立即返回,如果指定了<expression>,那么该表达式的值会被认作函数的返回值。


五、强制调用函数

   call <expr>
   表达式中可以一是函数,以此达到强制调用函数的目的。并显示函数的返回值,如果函数返回值是void,那么就不显示。

另一个相似的命令也可以完成这一功能——print,print后面可以跟表达式,所以也可以用他来调用函数,print和call的不同是,如果函数返回void,call则不显示,print则显示函数返回值,并把该值存入历史数据中。

在不同语言中使用GDB
——————————

GDB支持下列语言:C, C++, Fortran, PASCAL,Java, Chill, assembly, 和Modula-2。一般说来,GDB会根据你所调试的程序来确定当然的调试语言,比如:发现文件名后缀为“.c”的,GDB会认为是C程序。文件名后缀为“.C, .cc, .cp, .cpp, .cxx, .c++”的,GDB会认为是C++程序。而后缀是“.f,.F”的,GDB会认为是Fortran程序,还有,后缀为如果是“.s, .S”的会认为是汇编语言。

也就是说,GDB会根据你所调试的程序的语言,来设置自己的语言环境,并让GDB的命令跟着语言环境的改变而改变。比如一些GDB命令需要用到表达式或变量时,这些表达式或变量的语法,完全是根据当前的语言环境而改变的。例如C/C++中对指针的语法是*p,而在Modula-2中则是p^。并且,如果你当前的程序是由几种不同语言一同编译成的,那到在调试过程中,GDB也能根据不同的语言自动地切换语言环境。这种跟着语言环境而改变的功能,真是体贴开发人员的一种设计。

下面是几个相关于GDB语言环境的命令:

   show language
       查看当前的语言环境。如果GDB不能识为你所调试的编程语言,那么,C语言被认为是默认的环境。

infoframe
       查看当前函数的程序语言。

infosource
       查看当前文件的程序语言。

如果GDB没有检测出当前的程序语言,那么你也可以手动设置当前的程序语言。使用setlanguage命令即可做到。

   当set language命令后什么也不跟的话,你可以查看GDB所支持的语言种类:

(gdb) set language
       The currently understood settings are:

local or auto   Automatic setting based on source file
       c               Use the C language
       c++             Use the C++ language
       asm             Use the Asm language
       chill           Use the Chill language
       fortran         Use the Fortran language
       java            Use the Java language
       modula-2        Use the Modula-2 language
       pascal          Use the Pascal language
       scheme          Use the Scheme language

于是你可以在setlanguage后跟上被列出来的程序语言名,来设置当前的语言环境。

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK