程序调试的利器GDB

无论是多么优秀的程序员,都难以保证自己在编写代码时不会出现任何错误,因此调试是软件开发过程中的一个必不可少的 组成部分。当程序完成编译之后,它很可能无法正常运行,或者会彻底崩溃,或者不能实现预期的功能。此时如何通过调试找到问题的症结所在,就变成了摆在开发 人员面前最严峻的问题。通常说来,软件项目的规模越大,调试起来就会越困难,越需要一个强大而高效的调试器作为后盾。对于Linux程序员来讲,目前可供 使用的调试器非常多,GDB(GNU DeBugger)就是其中较为优秀的。

    初识GDB

GDB 是自由软件基金会(Free Software  Foundation,FSF)的软件工具之一。它的作用是协助程序员找到代码中的错误。如果没有GDB的帮助,程序员要想跟踪代码的执行流程,唯一的办 法就是添加大量的语句来产生特定的输出。但这一手段本身就可能会引入新的错误,从而也就无法对那些导致程序崩溃的错误代码进行分析。GDB的出现减轻了开 发人员的负担,他们可以在程序运行的时候单步跟踪自己的代码,或者通过断点暂时中止程序的执行。此外,他们还能够随时察看变量和内存的当前状态,并监视关 键的数据结构是如何影响代码运行的。

调试方法

如果想对程序进行调试,必须先在用GCC编译源代码时加上-g选项,以便产生GDB所需要的调试符号信息。例如,debugme.c是一个存在错误程序,可以使用如下的命令对其进行编译,同时产生调试符号:
# gcc -g debugme.c -o debugme

如 果愿意的话,还可以在编译时使用“-ggdb”选项来生成更多的调试信息。由于这些调试信息中的相当一部分是GDB所特有的,所以生成的代码将无法在其它 调试器中正常调试。对于大多数情况来说,普通的-g选项就足够了。需要注意的是,GCC虽然允许同时使用-g(调试)和-o(优化)选项,但优化会影响最 终生成的代码,导致程序源代码和二进制代码之间的关系变得复杂起来。如果不想为调试制造障碍,建议不要将-g和-o选项一同使用,并且只在程序彻底调试完 后才开始进行代码优化。这样调试过程将变得相对轻松和愉快。

基本应用

现在可以启动GDB来调试已经生成的可执行程序debugme,命令如下:

    

        

            

        

    

                         

如果一切正常,GDB将被启动并在屏幕上输出版权信息,但如果使用了-q或–quiet选项则不会显示它们。启动GDB时另外一个有用的命令行选项是“-d dirname”,其中dirname是一个目录名。该目录名告诉GDB应该到哪里去寻找源代码。

一 旦出现GDB的命令提示符(gdb),就表明GDB已经准备好接收来自用户的各种调试命令了。如果想在调试环境下运行这个程序,可以使用GDB提供的 “run”命令,而程序在正常运行时所需的各种参数可以作为“run”命令的参数传入,或者使用单独的“set  args”命令进行设置。如果在执行“run”命令时没有给出任何参数,GDB将使用上一次“run”或“set  args”命令指定的参数。如果想取消上次设置的参数,可以执行不带任何参数的“set args”命令。下面尝试在调试器中运行这个程序:

    

        

            

        

    

                         

最后一行输出表明程序在调用动态链接库/lib/ld-linux.so.2中的_dl_fini() 函数时出现了错误,地址是0x4000c6ac。这些对调试是非常重要的线索。另外还有一种信息对调试也很重要,就是错误发生时的函数调用层级关系,可以 通过执行“backtrace”命令来获得。在使用GDB调试命令时,用户可以不必输入完整的命令名称,使用任何惟一的缩写都可以。例如 “backtrace”命令就可以缩写成“back”甚至“bt”。GDB还支持很多常用的Shell命令编辑特征,比如可以像在bash或tcsh中那 样按Tab键补齐命令。如果相关命令不惟一的话,则列出所有可能的匹配项。此外键盘上的方向键可用来翻动历史命令。

GDB是一个源代码级的调试器,使用“list”命令可以查看当前调试对象的源代码。该命令的通用格式为“list [m,n]”,表示显示从m行开始到n行结束的代码段,而不带任何参数的“list”命令将显示最近10行源代码。

设置断点
在 调试有问题的代码时,在某一点停止运行往往很管用。这样程序运行到此外时会暂时挂起,等待用户的进一步输入。GDB允许在几种不同的代码结构上设置断点, 包括行号和函数名等,并且还允许设置条件断点,让程序只有在满足一定的条件时才停止执行。要根据行号设置断点,可以使用“ break  linenum”命令。要根据函数名设置断点,则应该使用“break funcname”命令。

在以上两种情况中,GDB将 在执行指定的行号或进入指定的函数之前停止执行程序。此时可以使用“print”显示变量的值,或者使用“list”查看将要执行的代码。对于由多个源文 件组成的项目,如果想在执行到非当前源文件的某行或某个函数时停止执行,可以使用如下形式的命令:

    

        

            

        

    

                         

条件断点允许当一定条件满足时暂时停止程序的执行。它对于调试来讲非常有用。设置条件断点的正确语法如下:

    

        

            

        

    

                         

其中expr是一个逻辑表达式。当该表达式的值为真时,程序将在该断点处暂时挂起。例如,下面的命令将在debugme程序的第38行设置一个条件断点。当程序运行到该行时,如果count的值等于3,就将暂时停止执行:
(gdb) break 38 if count==3

设置断点是调试程序时最常用到的一种手段。它可以中断程序的运行,给程序员一个单步跟踪的机会。使用命令“ break main”在main函数上设置断点可以在程序启动时就开始进行跟踪。

接 下去使用“continue”命令继续执行程序,直到遇到下一个断点。如果在调试时设置了很多断点,可以随时使用“info  breakpoints”命令来查看设置的断点。此外,开发人员还可以使用“delete”命令删除断点,或者使用“disable”命令来使设置的断点 暂时无效。被设置为无效的断点在需要的时候可以用“enable”命令使其重新生效。

观察变量
GDB 最有用的特性之一是能够显示被调试程序中几乎任何表达式、变量或数组的类型和值,并且能够用编写程序所用的语言打印出任何合法表达式的值。查看数据最简单 的办法是使用“print”命令,只需在“print”命令后面加上变量表达式,就可以打印出此变量表达式的当前值,示例如下:

    

        

            

        

    

                         

从输出信息中可以看出,输入字符串被正确地存储在了字符指针str所指向的内存缓冲区中。除了给出变量 表达式的值外,“print”命令的输出信息中还包含变量标号($1)和对应的内存地址(0x40015360)。变量标号保存着被检查数值的历史记录, 如果此后还想访问这些值,就可以直接使用别名而不用重新输入变量表达式。

如果想知道变量的类型,可以使用“whatis”命令,示例如下:

    

        

            

        

    

                         

对于第一次调试别人的代码,或者面对的是一个异常复杂的系统时,“whatis”命令的作用不容忽视。

单步执行
为了单步跟踪代码,可以使用单步跟踪命令“step”,它每次执行源代码中的一行。

在GDB中可以使用许多方法来简化操作,除了可以将“step”命令简化为“s”之外,还可以直接输入回车键来重复执行前面一条命令。

除了可以用“step”命令来单步运行程序之外,GDB还提供了另外一条单步调试命令“next”。两者功能非常相似,差别在于如果将要被执行的代码行中包含函数调用,使用step命令将跟踪进入函数体内,而使用next命令则不进入函数体内。

在进入下一部分之前,使用下面的命令退出GDB:
(gdb) quit

分析核心(core)文件

在 程序发生崩溃时,有时可能无法直接运行GDB来进行调试。比如程序可能是在另外一台机器上运行的,或者因为程序对时间比较敏感,所以手动跟踪调试会产生无 法接受的延迟等。遇到这些情况,就只能等到程序运行结束后才能判断崩溃的原因了。这时需要用到Linux提供的core  dump机制。当程序中出现内存操作错误时,会发生崩溃并产生核心文件。使用GDB可以对产生的核心文件进行分析,找出程序是在什么时候崩溃的和在崩溃之 前程序都做了些什么。当然,如果要用GDB来分析核心文件,也必须在编译时加上-g选项来产生调试符号表。

在分析核心文件之前必须确认系统是否允许生成核心文件,很多Linux发行版在默认时禁止生成核心文件。为了生成核心文件,首先必须执行下面的命令:
# ulimit -c unlimited

然后就可以生成核心文件了。这里仍以前面的debugme程序为例,再次执行下面命令将产生核心文件:

    

        

            

        

    

                         

生成的核心文件名根据系统配置的不同会有所差异。要在GDB中分析核心文件,除了要给出核心文件的文件名外,还必须给出生成该核心文件的可执行程序的名称,示例如下:

    

        

            

        

    

                         

从GDB的输出信息中可以看出,产生这个核心文件的原因是因为程序收到了序号为11的信号。如果想知道程序在崩溃之前运行到了哪里,可以使用“backtrace”或“info stack”命令查看一下堆栈的历史记录。示例如下:

    

        

            

        

    

                         

由上可知,程序崩溃时正处于_dl_fini()函数之中。但很多时候程序员感兴趣的可能并不是这个, 而是exit()或_libc_start_main()函数,因为它们才可能是问题真正的症结所在。GDB提供的“frame”命令可以用来在不同的调 用上下文中切换。例如下面的命令可以查看exit()函数在执行时的状况:

    

        

            

        

    

                         

此外还可以用“up”或“down”命令在不同的函数调用上下文中切换。开发人员使用这三条命令可以很轻松地实现调用栈的遍历。在分析核心文件时,通过将遍历栈的命令和检查变量值的“print”命令结合起来,就能够复原程序运行时的全部景象。

调试其它进程

有 时会遇到一种很特殊的调试需求,对当前正在运行的其它进程进行调试。这种情况有可能发生在那些无法直接在调试器中运行的进程身上,例如有的进程只能在系统 启动时运行。另外如果需要对进程产生的子进程进行调试的话,也只能采用这种方式。GDB可以对正在执行的程序进行调度,它允许开发人员中断程序并查看其状 态,之后还能让这个程序正常地继续执行。

GDB提供了两种方式来调试正在运行的进程:一种是在GDB命令行上指定进程的PID,另一种是在GDB中使用“attach”命令。例如,开发人员可以先启动debugme程序,让其开始等待用户的输入。示例如下:

    

        

            

        

    

                         

接下去在另一个虚拟控制台中用下面的命令查出该进程对应的进程号:

    

        

            

        

    

                         

得到进程的PID后,就可以使用GDB对其进行调试了:

    

        

            

        

    

                         

在上面的输出信息中,以Attaching to program开始的行表明GDB已经成功地附加在PID为555的进程上了。另外一种连接到其它进程的方法是先用file命令加载调试时所需的符号表,然后再通过“attaché”命令进行连接:

    

        

            

        

    

                         

如果想知道程序现在运行到了哪里,同样可以使用“backtrace”命令。当然也可以使用“step”命令对程序进行单步调试。

在完成调试之后,不要忘记用detach命令断开连接,让被调试的进程可以继续正常运行:

GDB是Linux下一个最基本的调试器,其功能非常丰富。完整地介绍GDB的功能可能需要几百页,本文只涵盖了GDB的一些最常见的用法。作为一个合格的Linux程序员,花在GDB上的功夫和时间越多,从调试中获得的益处就越多。

留下评论

邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据