GDB调试

本文介绍GDB调试器的基本使用方法及高级技巧,包括设置断点、观察点、使用堆栈跟踪等功能,帮助开发者高效定位和解决程序中的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

gdb(1)
       GNU工具软件       gdb(1)
工具名称:
  gdb — GNU调试器

用法简述:
  gdb [-help] [-nx] [-q] [-batch] [-cd=dir] [-f] [-b bps] [-tty=dev]
    [-s symfile] [-e prog] [-se prog] [-c core] [-x cmds] [-d dir]
    [prog[core|procID]]

工具介绍:
   GDB是一种程序调试工具,它可以让你知道一个正在运行中的程序内部发生的事情
  ,或者崩溃的时候它到底做了什么。

   GDB主要可以做四类事情来助你除错:
   - 启动程序,显示运行状况。
   - 在设置的中断条件下,终止程序运行。    
   - 检测程序中止时所发生的事情。
   - 修改程序文件。

   GDB可以用来调试C、C++以及Modua-2写成的程序。对Fortran的支持将在GNU版的
  Fortran编译器出现以后加入。

   GDB通过shell命令gdb调用。一旦运行,它就从终端读取命令直到quit命令结束程序
  。你还可以用help命令获取gdb的在线帮助信息。

   运行gdb可以不输入参数和选项;但是更通常的用法是采用一两个参数,同时可执行
  文件名作为参数具体给出:
  例:gdb program
   在给出可执行文件名的同时还可以指定一个相应的core文件:
  例:gdb program core
   如果想调试一个正在运行中的程序可以经进程ID作为第二个参数给出:
  例:gdb program 1234
  gdb将处理ID号为1234的进程(除非你还有一个名为1234的文件;GDB将会首先查找一个
core文件)。
   
   以下是一些GDB经常用到的命令:

  break [文件名:] 函数名
    在文件内的某函数设置断点。
  run [参数列表]
    如果给定参数列表,那么运行程序的时候带上参数表给出的参数。
  bt Backtrace(调试后台跟踪):显示程序堆栈。
  print expr(表达式)
    显示表达式的结果值。
  c  继续运行程序(程序终止以后,比如,在断点处。)
  next 接着执行下一程序行;单步跳过行中所有函数。
  step 接着执行下一程序行;单步跳入行中所有函数。
  help [name]
    显示输出关于GDB命令的信息,或者总体的用法说明。
  quit 离开GDB。

   要得到全面详细的GDB信息,可查阅《Using GDB:A Guide to the GNU
  Source-level Debugger 》,作者:Richard M. Stallman和Roland H.Pesch。
  info程序提供了gdb的在线使用文档。

选 项:
   除了选项所有参数指定了一个程序文件和core文件(或者进程ID);也就是说,
  第一个参数如果没有参数其实与一个‘-se’选项等同,同样,第二个参数如果是一
  个文件名则等同一个‘-c’选项。许多选项有长短两种格式;这两种在这里都将给出。
  只要你的参数够明确,缩减的长格式同样会被识别。(如果愿意可以用‘+’、‘-’
  来标记选项参数,尽管我们会阐明更常用的约定。)
   
   所有你给出的选项、命令行参数会依次被读取执行,如果用到‘-x’选项, 那么,
  其前后次序不同将会有所不同执行结果。
   
 1. -help
  -h   
  列出所有选项,并附带间断的解释。
 2. -symbols=文件名
  -s file  
  从文件中读取符号表。
 3. -write  
  可执行文件和core文件功能开启。
 4. -exec=文件名
  -e 文件名  
  如果合适的话将用文件(file)作为一个可执行文件运行,来检测和 core 转储
  文件连接的纯数据。  
 5. -core=文件名
  -c 文件名
  用指定文件来作为core转储文件来检测。
 6. -command=文件名
  -x 文件名
  执行来自指定文件的GDB命令。  
 7. -directory=目录名
  -d 目录名
  在搜索源文件的路径中加入指定目录。
 8. -nx
  -n
  不执行来自任何‘.gdbinit’类初始化文件的命令。通常,这类文件中的命令
  会在所有命令选项和参数被执行后才会被运行。  
 9. -quiet
  -q
  “Quiet”。禁止显示介绍和版权信息,批处理模式同样禁止。
 10. -batch
  在批处理模式下运行。在指定-x的条件下,执行完所有命令文件后‘0’状态
  退出(若无禁止,还包括‘.gdbninit’类文件。)。在执行命令文件中的gdb
  命令时如果发生错误,则以非‘0’状态退出。
  批处理模式在将GDB作为筛选器时是很有用的。比如在下载或在另台计算机上
  运行一个程序;为了让它起到作用,批处理模式下不会再送出“Program exited
  normallly.”信息。而该信息在GDB的控制终端运行程序时通常是要显示的。
 11. -cd=目录名
  用制定目录名作为运行GDB时的工作目录代替当前目录。
 12. -b bps
  用来在远程调试程序的状态下设置串口的比特率和波特率。   
 13. tty=设备名
  采用制定的设备作为程序的标准输入、输出。
 14. -fullname
  -f
  Emacs通过设置该选项来运行GDB子进程。它告诉GDB在每次显示堆栈帧( 包括每次
  程序运行终止)时以一个可识别的标准形式输出完整的文件名、行号。该可识别格式
  看起来是两个‘32’字符跟在用冒号隔开的文件名、行号和字符位置后面。Emacs、
  GDB间的接口程序用两个‘32’字符作为显示源代码帧的信号。  

参 阅:
  info工具中提供的gdb在线文档条目;
  Using GDB:A Guide to the GNU Source-level Debugger,Richard M. Stallman
  and ROland H. Pesch,1991.7。

内容:

编译
运行 gdb
调试会话示例
使用断点
更多断点和观察点
Core 文件
堆栈跟踪
连接到其它进程
其它小技巧
结束语
参考资料
关于作者

GNU 调试器简介
作者:David Seager

Linux 的大部分特色源自于 shell 的 GNU 调试器,也称作 gdb。gdb 可以让您查看程序的内部结构、打印变量值、设置断点,以及单步调试源代码。它是功能极其强大的工具,适用于修复程序代码中的问题。在本文中,我将尝试说明 gdb 有多棒,多实用。

编译
开始调试之前,必须用程序中的调试信息编译要调试的程序。这样,gdb 才能够调试所使用的变量、代码行和函数。如果要进行编译,请在 gcc(或 g++)下使用额外的 '-g' 选项来编译程序:

gcc -g eg.c -o eg

运行 gdb
在 shell 中,可以使用 'gdb' 命令并指定程序名作为参数来运行 gdb,例如 'gdb eg';或者在 gdb 中,可以使用 file 命令来装入要调试的程序,例如 'file eg'。这两种方式都假设您是在包含程序的目录中执行命令。装入程序之后,可以用 gdb 命令 'run' 来启动程序。

调试会话示例
如果一切正常,程序将执行到结束,此时 gdb 将重新获得控制。但如果有错误将会怎么样?这种情况下,gdb 会获得控制并中断程序,从而可以让您检查所有事物的状态,如果运气好的话,可以找出原因。为了引发这种情况,我们将使用一个示例程序:

代码示例 eg1.c
#include

int wib(int no1, int no2)
{
int result, diff;
diff = no1 - no2;
result = no1 / diff;
return result;
}

int main(int argc, char *argv[])
{
int value, div, result, i, total;

value = 10;
div = 6;
total = 0;

for(i = 0; i < 10; i++)
{
result = wib(value, div);
total += result;
div++;
value--;
}

printf("%d wibed by %d equals %dn", value, div, total);
return 0;
}

这个程序将运行 10 次 for 循环,使用 'wib()' 函数计算出累积值,最后打印出结果。

在您喜欢的文本编辑器中输入这个程序(要保持相同的行距),保存为 'eg1.c',使用 'gcc -g eg1.c -o eg1' 进行编译,并用 'gdb eg1' 启动 gdb。使用 'run' 运行程序可能会产生以下消息:

Program received signal SIGFPE, Arithmetic exception.
0x80483ea in wib (no1=8, no2=8) at eg1.c:7
7 result = no1 / diff;
(gdb)

gdb 指出在程序第 7 行发生一个算术异常,通常它会打印这一行以及 wib() 函数的自变量值。要查看第 7 行前后的源代码,请使用 'list' 命令,它通常会打印 10 行。再次输入 'list'(或者按回车重复上一条命令)将列出程序的下 10 行。从 gdb 消息中可以看出,第 7 行中的除法运算出了错,程序在这一行中将变量 "no1" 除以 "diff"。

要查看变量的值,使用 gdb 'print' 命令并指定变量名。输入 'print no1' 和 'print diff',可以相应看到 "no1" 和 "diff" 的值,结果如下:

(gdb) print no1
$5 = 8
(gdb) print diff
$2 = 0

gdb 指出 "no1" 等于 8,"diff" 等于 0。根据这些值和第 7 行中的语句,我们可以推断出算术异常是由除数为 0 的除法运算造成的。清单显示了第 6 行计算的变量 "diff",我们可以打印 "diff" 表达式(使用 'print no1 - no2' 命令),来重新估计这个变量。gdb 告诉我们 wib 函数的这两个自变量都等于 8,于是我们要检查调用 wib() 函数的 main() 函数,以查看这是在什么时候发生的。在允许程序自然终止的同时,我们使用 'continue' 命令告诉 gdb 继续执行。

(gdb) continue
Continuing.

Program terminated with signal SIGFPE, Arithmetic exception.
The program no longer exists.

使用断点
为 了查看在 main() 中发生了什么情况,可以在程序代码中的某一特定行或函数中设置断点,这样 gdb 会在遇到断点时中断执行。可以使用命令 'break main' 在进入 main() 函数时设置断点,或者可以指定其它任何感兴趣的函数名来设置断点。然而,我们只希望在调用 wib() 函数之前中断执行。输入 'list main' 将打印从 main() 函数开始的源码清单,再次按回车将显示第 21 行上的 wib() 函数调用。要在那一行上设置断点,只需输入 'break 21'。gdb 将发出以下响应:

(gdb) break 21
Breakpoint 1 at 0x8048428: file eg1.c, line 21.

以显示它已在我们请求的行上设置了 1 号断点。'run' 命令将从头重新运行程序,直到 gdb 中断为止。发生这种情况时,gdb 会生成一条消息,指出它在哪个断点上中断,以及程序运行到何处:

Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:21
21 result = wib(value, div);

发 出 'print value' 和 'print div' 将会显示在第一次调用 wib() 时,变量分别等于 10 和 6,而 'print i' 将会显示 0。幸好,gdb 将显示所有局部变量的值,并使用 'info locals' 命令保存大量输入信息。

从以上的调查中可以看出,当 "value" 和 "div" 相等时就会出现问题,因此输入 'continue' 继续执行,直到下一次遇到 1 号断点。对于这次迭代,'info locals' 显示了 value=9 和 div=7。

与其再次继续,还不如使用 'next' 命令单步调试程序,以查看 "value" 和 "div" 是如何改变的。gdb 将响应:

(gdb) next
22 total += result;

再按两次回车将显示加法和减法表达式:

(gdb)
23 div++;
(gdb)
24 value--;

再 按两次回车将显示第 21 行,wib() 调用。'info locals' 将显示目前 "div" 等于 "value",这就意味着将发生问题。如果有兴趣,可以使用 'step' 命令(与 'next' 形成对比,'next' 将跳过函数调用)来继续执行 wib() 函数,以再次查看除法错误,然后使用 'next' 来计算 "result"。

现在已完成了调试,可以使用 'quit' 命令退出 gdb。由于程序仍在运行,这个操作会终止它,gdb 将提示您确认。

更多断点和观察点
由 于我们想要知道在调用 wib() 函数之前 "value" 什么时候等于 "div",因此在上一示例中我们在第 21 行中设置断点。我们必须继续执行两次程序才会发生这种情况,但是只要在断点上设置一个条件就可以使 gdb 只在 "value" 与 "div" 真正相等时暂停。要设置条件,可以在定义断点时指定 "break if "。将 eg1 再次装入 gdb,并输入:

(gdb) break 21 if value==div
Breakpoint 1 at 0x8048428: file eg1.c, line 21.

如果已经在第 21 行中设置了断点,如 1 号断点,则可以使用 'condition' 命令来代替在断点上设置条件:

(gdb) condition 1 value==div

使 用 'run' 运行 eg1.c 时,如果 "value" 等于 "div",gdb 将中断,从而避免了在它们相等之前必须手工执行 'continue'。调试 C 程序时,断点条件可以是任何有效的 C 表达式,一定要是程序所使用语言的任意有效表达式。条件中指定的变量必须在设置了断点的行中,否则表达式就没有什么意义!

使用 'condition' 命令时,如果指定断点编号但又不指定表达式,可以将断点设置成无条件断点,例如,'condition 1' 就将 1 号断点设置成无条件断点。

要查看当前定义了什么断点及其条件,请发出命令 'info break':


(gdb) info break
Num Type Disp Enb Address What
1 breakpoint keep y 0x08048428 in main at eg1.c:21
stop only if value == div
breakpoint already hit 1 time

除了所有条件和已经遇到断点多少次之外,断点信息还在 'Enb' 列中指定了是否启用该断点。可以使用命令 'disable '、'enable ' 或 'delete ' 来禁用、启用和彻底删除断点,例如 'disable 1' 将阻止在 1 号断点处中断。

如 果我们对 "value" 什么时候变得与 "div" 相等更感兴趣,那么可以使用另一种断点,称作监视。当指定表达式的值改变时,监视点将中断程序执行,但必须在表达式中所使用的变量在作用域中时设置监视 点。要获取作用域中的 "value" 和 "div",可以在 main 函数上设置断点,然后运行程序,当遇到 main() 断点时设置监视点。重新启动 gdb,并装入 eg1,然后输入:

(gdb) break main
Breakpoint 1 at 0x8048402: file eg1.c, line 15.
(gdb) run
...
Breakpoint 1, main (argc=1, argv=0xbffff954) at eg1.c:15
15 value = 10;

要了解 "div" 何时更改,可以使用 'watch div',但由于要在 "div" 等于 "value" 时中断,那么应输入:

(gdb) watch div==value
Hardware watchpoint 2: div == value

如果继续执行,那么当表达式 "div==value" 的值从 0(假)变成 1(真)时,gdb 将中断:

(gdb) continue
Continuing.
Hardware watchpoint 2: div == value

Old value = 0
New value = 1
main (argc=1, argv=0xbffff954) at eg1.c:19
19 for(i = 0; i < 10; i++)

'info locals' 命令将验证 "value" 是否确实等于 "div"(再次声明,是 8)。

'info watch' 命令将列出已定义的监视点和断点(此命令等价于 'info break'),而且可以使用与断点相同的语法来启用、禁用和删除监视点。

core 文件
在 gdb 下运行程序可以使俘获错误变得更容易,但在调试器外运行的程序通常会中止而只留下一个 core 文件。gdb 可以装入 core 文件,并让您检查程序中止之前的状态。

在 gdb 外运行示例程序 eg1 将会导致核心信息转储:

$ ./eg1
Floating point exception (core dumped)

要 使用 core 文件启动 gdb,在 shell 中发出命令 'gdb eg1 core' 或 'gdb eg1 -c core'。gdb 将装入 core 文件,eg1 的程序清单,显示程序是如何终止的,并显示非常类似于我们刚才在 gdb 下运行程序时看到的消息:

...
Core was generated by `./eg1'.
Program terminated with signal 8, Floating point exception.
...
#0 0x80483ea in wib (no1=8, no2=8) at eg1.c:7
7 result = no1 / diff;

此 时,可以发出 'info locals'、'print'、'info args' 和 'list' 命令来查看引起除数为零的值。'info variables' 命令将打印出所有程序变量的值,但这要进行很长时间,因为 gdb 将打印 C 库和程序代码中的变量。为了更容易地查明在调用 wib() 的函数中发生了什么情况,可以使用 gdb 的堆栈命令。

堆栈跟踪
程序“调用堆栈”是当前函数之前的所有已调用函数的列表(包括当前函数)。每个函数及其变量都被分配了一个“帧”,最近调用的函数在 0 号帧中(“底部”帧)。要打印堆栈,发出命令 'bt'('backtrace' [回溯] 的缩写):


(gdb) bt
#0 0x80483ea in wib (no1=8, no2=8) at eg1.c:7
#1 0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21

此结果显示了在 main() 的第 21 行中调用了函数 wib()(只要使用 'list 21' 就能证实这一点),而且 wib() 在 0 号帧中,main() 在 1 号帧中。由于 wib() 在 0 号帧中,那么它就是执行程序时发生算术错误的函数。

实 际上,发出 'info locals' 命令时,gdb 会打印出当前帧中的局部变量,缺省情况下,这个帧中的函数就是被中断的函数(0 号帧)。可以使用命令 'frame' 打印当前帧。要查看 main 函数(在 1 号帧中)中的变量,可以发出 'frame 1' 切换到 1 号帧,然后发出 'info locals' 命令:

(gdb) frame 1
#1 0x8048435 in main (argc=1, argv=0xbffff9c4) at eg1.c:21
21 result = wib(value, div);
(gdb) info locals
value = 8
div = 8
result = 4
i = 2
total = 6

此信息显示了在第三次执行 "for" 循环时(i 等于 2)发生了错误,此时 "value" 等于 "div"。

可以通过如上所示在 'frame' 命令中明确指定号码,或者使用 'up' 命令在堆栈中上移以及 'down' 命令在堆栈中下移来切换帧。要获取有关帧的进一步信息,如它的地址和程序语言,可以使用命令 'info frame'。

gdb 堆栈命令可以在程序执行期间使用,也可以在 core 文件中使用,因此对于复杂的程序,可以在程序运行时跟踪它是如何转到函数的。

连接到其它进程
除了调试 core 文件或程序之外,gdb 还可以连接到已经运行的进程(它的程序已经过编译,并加入了调试信息),并中断该进程。只需用希望 gdb 连接的进程标识替换 core 文件名就可以执行此操作。以下是一个执行循环并睡眠的示例程序:

eg2 示例代码
#include
int main(int argc, char *argv[])
{
int i;
for(i = 0; i < 60; i++)
{
sleep(1);
}

return 0;
}

使用 'gcc -g eg2.c -o eg2' 编译该程序并使用 './eg2 &' 运行该程序。请留意在启动该程序时在背景上打印的进程标识,在本例中是 1283:

./eg2 &
[3] 1283


启动 gdb 并指定进程标识,在我举的这个例子中是 'gdb eg2 1283'。gdb 会查找一个叫作 "1283" 的 core 文件。如果没有找到,那么只要进程 1283 正在运行(在本例中可能在 sleep() 中),gdb 就会连接并中断该进程:

...
/home/seager/gdb/1283: No such file or directory.
Attaching to program: /home/seager/gdb/eg2, Pid 1283
...
0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6
(gdb)

此时,可以发出所有常用 gdb 命令。可以使用 'backtrace' 来查看当前位置与 main() 的相对关系,以及 mian() 的帧号是什么,然后切换到 main() 所在的帧,查看已经在 "for" 循环中运行了多少次:

(gdb) backtrace
#0 0x400a87f1 in __libc_nanosleep () from /lib/libc.so.6
#1 0x400a877d in __sleep (seconds=1) at ../sysdeps/unix/sysv/linux/sleep.c:78
#2 0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7
(gdb) frame 2
#2 0x80483ef in main (argc=1, argv=0xbffff9c4) at eg2.c:7
7 sleep(1);
(gdb) print i
$1 = 50

如果已经完成了对程序的修改,可以 'detach' 命令继续执行程序,或者 'kill' 命令杀死进程。还可以首先使用 'file eg2' 装入文件,然后发出 'attach 1283' 命令连接到进程标识 1283 下的 eg2。

其它小技巧
gdb 可以让您通过使用 shell 命令在不退出调试环境的情况下运行 shell 命令,调用形式是 'shell [commandline]',这有助于在调试时更改源代码。

最 后,在程序运行时,可以使用 'set ' 命令修改变量的值。在 gdb 下再次运行 eg1,使用命令 'break 7 if diff==0' 在第 7 行(将在此处计算结果)设置条件断点,然后运行程序。当 gdb 中断执行时,可以将 "diff" 设置成非零值,使程序继续运行直至结束:

Breakpoint 1, wib (no1=8, no2=8) at eg1.c:7
7 result = no1 / diff;
(gdb) print diff
$1 = 0
(gdb) set diff=1
(gdb) continue
Continuing.
0 wibed by 16 equals 10

Program exited normally.

结束语
GNU 调试器是所有程序员工具库中的一个功能非常强大的工具。在本文中,只介绍了 gdb 的一小部分功能。要了解更多知识,建议您阅读 GNU 调试器手册。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值