GDB快速指南

版本 2 (2009 6月 14)

原文

这是一个非常快速的GDB指南,旨在让你开始在终端的命令行下熟练使用GNU调试器gdb。

更多详细信息,查看官方GDB 文档

此外,一个好的GNU GDB前端是数据显示调试器DDD

1.编译

您必须告诉编译器使用包含符号调试的信息来编译代码。

下面介绍如何使用gcc,使用**-g**开关:

$ gcc -g hello.c -o hello		//编译C代码

$ g++ -g hello.cpp -o hello		//编译C++代码

完成此操作后,您应该能够在调试器中查看生成的文件列表,如果需要,可以添加编译选项“-v”查看具体编译步骤。

1.1启动调试器

首先:您可以在任何gdb提示符处输入help并获取更多信息。

此外,还可以输入quit以退出调试器。

最后,只需点击RETURN将重复输入最后一个命令。

有几种方法可以启动调试器(例如,如果您是 IDE,则可以使用不太人性化的特定模式启动调试器),但在此我将提及其中两种方法:vanilla的控制台模式和curses的 GUI 模式。虽然GUI 界面更友好,但还是让我们快速介绍一个简单的程序,并在控制台调试器中启动一个名为hello的程序:

$ gdb hello
GNU gdb 6.8
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-slackware-linux"...
(gdb) run
Starting program: /home/beej/hello 
Hello, world!

Program exited normally.
(gdb) 

最后一行是gdb提示符,等待您告诉它该怎么做。键入rrun以运行程序。(gdb允许您缩短命令,除非缩短的命令会使它们的意思变得模棱两可)。

使用 GUI 模式,可以输入gdb-tui启动调试器即可。

下面是您将看到的屏幕截图,大致包括:

所有gdb命令都可以正常的在 GUI 模式下工作,此外箭头键和 pgup/pgdown 键将滚动源窗口。此外,还可以通过list命令准确定位显示的文件或函数,例如**,"list hello.c:5可以在窗口中显示文件hello.c。的第 5 行内容。定位方法同样适用于断点命令b**。

开启对一个程序的调试有两种方法

一是,直接在命令行键入“gdb 可执行程序名”;

二是,先键入“gdb”启动调试器,而后键入“file 可执行程序名”

如果需要向调试的程序传递参数,可在开始执行时,将它们作为参数传递给run命令。

$ gdb hello
GNU gdb 6.8
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-slackware-linux"...
(gdb) run arg1 arg2
Starting program: /home/beej/hello arg1 arg2
Hello, world!

Program exited normally.
(gdb) 
  • 上例中显示了将参数"arg1"和"arg2"传递给"hello"。

1.2断点

启动调试器直接运行程序并不是很有用,更多的时候我们需要停止执行并进入步进模式

  • 在发出run命令之前,需要在要停止的位置设置断点。您可以使用breakb命令,并指定一个位置(可以是函数名称、行号或源文件:行号)。
  • 下表是一些位置的示例,这些位置也可以被各种其他命令以及break使用:
断点命令 含义
break 5 在当前文件的第 5 行处中断
break hello.c:5 hello.c的 5 行中断
break main 在main函数的开头断开

因此,对于此测试,让我们在main()处设置一个断点,然后启动程序:

$ gdb hello
GNU gdb 6.8
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-slackware-linux"...
(gdb) b main
Breakpoint 1 at 0x8048395: file hello.c, line 5.
(gdb) r
Starting program: /home/beej/hello 

Breakpoint 1, main () at hello.c:5
5		printf("Hello, world!\n");
(gdb)

如您所见,我们到达了main(),执行已停止在我们设置的断点。如果您在dumb终端模式下运行**,gdb**将输出下一步执行的行。如果在GUI 模式下运行,它将接下来执行的行将在源窗口中高亮显示。

  • 要列出当前断点,请使用info命令,如下所示:“info breakpoints”(或较短的**“i b”):**
(gdb) b main
Breakpoint 1 at 0x8048395: file hello.c, line 5.
(gdb) i b
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x08048395 in main at hello.c:5
  • 要清除断点,请使用带有断点位置的clear命令,还可以使用delete命令按数字清除断点。

  • 启用/禁用断点,请使用带有断点号的enabledisable命令,注意这两个命令将断点号作为参数,而不是断点位置!断点列表中的"Enb"列下可以看到断点的启用/禁用状态。

(gdb) i b
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x08048395 in main at hello.c:5
(gdb) disable 1
(gdb) i b
Num     Type           Disp Enb Address    What
1       breakpoint     keep n   0x08048395 in main at hello.c:5
(gdb) clear main
Deleted breakpoint 1 
(gdb) i b
No breakpoints or watchpoints.

1.3步进命令

执行停止在断点后,您可以告诉调试器执行一些操作。

  • 单步执行next (或n )。此命令将您移动到当前函数中的下一个语句(或者,如果已退出函数的末尾,则返回到函数的调用方)。

    下面是一个示例:请记住**,gdb当前输出的是前一个"(gdb)"前导符接下来要执行的行。例如,当我们在printf()行上运行next**时,结果是我们会看到printf的输出。

(gdb) b main
Breakpoint 1 at 0x8048395: file hello.c, line 5.
(gdb) r
Starting program: /home/beej/hello 

Breakpoint 1, main () at hello.c:5
5		printf("Hello, world!\n");
(gdb) next
Hello, world!
7		return 0;
(gdb) next
8	}
(gdb) next
0xb7d6c6a5 in __libc_start_main () from /lib/libc.so.6
(gdb) next
Single stepping until exit from function __libc_start_main, 
which has no line number information.

Program exited normally.
(gdb) 

结尾的内容显示,还有另一个库函数(__libc_start_main()在调用main()函数!但因为它不是使用调试信息编译的,因此我们看不到源代码,但我们仍然可以通过next单步执行,并且程序会正常退出。

!!!请注意next命令遇到函数调用时会直接一步执行完函数调用,然后返回到当前函数中的下一行。但如果您想要进入并逐行调试该函数,该怎么办?使用step (或s ) 命令执行此操作。它的工作原理与next基本一样,除了它会进入到函数内部。

  • 继续continue(或c):假设您厌倦了单步执行,只是希望程序再次运行,可以使用该命令继续执行。如果程序正在运行,但您忘记了设置断点,该怎么办?您可以点击CTRL-C,这将停止程序,无论它碰巧在哪里,并返回到"(gdb)"提示。此时,您可以在某处设置一个适当的断点,并continue到该断点。

  • 重复RETURN,只要点击RETURN将重复输入的最后一个命令,这将避免一遍又一遍的键入next

1.4 变量

  • 重复显示变量:display(disp): 如果希望在调试过程中每次都检查一些变量,则可以用该命令设置某些变量,但仅当变量当前在作用域中时才能显示它们。如果变量在作用域中,每次执行代码时,都会显示变量的值。

为了清楚起见,以下输出缺少行之间的源代码输出,但在 GUI 模式下是看到的。

(gdb) b main
Breakpoint 1 at 0x8048365: file hello.c, line 5.
(gdb) r
Starting program: /home/beej/hello 

Breakpoint 1, main () at hello.c:5
(gdb) disp i
1: i = -1207447872
(gdb) next
1: i = 1
(gdb) next
1: i = 2
(gdb) next
1: i = 4
(gdb) 
  • 取消重复显示变量:undisplay(undisp)。上面"i"左侧的数字是变量的显示编号,使用此数字作为参数的undisplay命令,就可以取消对相应变量设置。

  • 显示已设置的重复显示变量:info display。如果您忘记了变量对应的显示数字,可以键入info display重新查看它们。

(gdb) b main
Breakpoint 1 at 0x8048365: file hello.c, line 5.
(gdb) r
Starting program: /home/beej/hello 

Breakpoint 1, main () at hello.c:5
(gdb) display i
1: i = -1207447872
(gdb) info display
Auto-display expressions now in effect:
Num Enb Expression
1:   y  i
(gdb) undisplay 1
(gdb)
  • 单次显示变量:print(p)。如果只想一次性知道变量的值,可以键入“print或(p) 变量”。例如,我们看到"i"的值是 40:
(gdb) print i
$1 = 40
(gdb)

"$"与数字的组合意味着什么?对初学者来说它并不重要。

  • 格式化单次显示变量:printf。一个方便的格式化输出命令,例如:
(gdb) printf "%d\n", i
40
(gdb) printf "%08X\n", i
00000028
(gdb)

2.杂项命令

虽然以下命令并非常用,但它足够有趣,列出一二。

2.1堆栈操作

  • 回溯命令:backtrace(或bt) 。显示当前函数调用堆栈,当前函数位于顶部,调用方按其调用顺序显示在下方:
(gdb) backtrace
#0  subsubfunction () at hello.c:5
#1  0x080483a7 in subfunction () at hello.c:10
#2  0x080483cf in main () at hello.c:16
(gdb)

键入** help stack**,了解详细信息。

2.2其他步进方法

  • finish :要退出当前函数并返回到调用函数,请使用命令。

  • stepi : 步进到某个语句。

  • advance:继续运行,直到指定的某临时断点。advance 位置标记

    下面是一个从当前位置前进到子函数处的示例:

Breakpoint 1, main () at hello.c:15
15		printf("Hello, world!\n");
(gdb) advance subsubfunction
Hello, world!
subsubfunction () at hello.c:5
5		printf("Deepest!\n");
(gdb) 

2.3 跳转到代码的任意部分

jump命令的工作方式与**“continue”**完全一样,不同的是它需要一个位置作为参数。(有关位置的详细信息,请参阅上面的"breakpoints"部分。但如果需要在跳转目标处停止,应先在那里设置断点。

2.4 在运行时更改变量的值

set variable 赋值表达式 :在运行期间更改变量的值。您也可以在set后使用带有括号表达式的形式。

Breakpoint 1, main () at hello.c:15
15		int i = 10;
(gdb) print i
$1 = -1208234304
(gdb) set (i = 20)
(gdb) print i
$2 = 20
(gdb) set variable i = 40
(gdb) print i
$3 = 40
(gdb) 

这与jump命令一起可以帮助您在不重新启动程序的情况下重复代码部分。

2.5 硬件观察点

  • watch 变量名 :硬件观察点是一个特殊的断点,每当表达式更改时都会触发。通常用在你想知道何时变量被更改(写入)的。
Breakpoint 1, main () at hello.c:5
5		int i = 1;
(gdb) watch i
Hardware watchpoint 2: i
(gdb) continue
Continuing.
Hardware watchpoint 2: i

Old value = -1208361280
New value = 2
main () at hello.c:7
7		while (i < 100) {
   
(gdb) continue
Continuing.
Hardware watchpoint 2: i

Old value = 2
New value = 3
main () at hello.c:7
7		while (i < 100) {
   
(gdb)

!!!请注意**,watch**可以将表达式作为它的参数,因此您可以将变量名或者更复杂的东西放在表达式中,例如 *(p+5)a[5]。我甚至尝试过条件表达式,如i > 10,但结果是不确定的。

  • 您可以获取带有info breakinfo watch的监视点列表,并且可以使用delete命令按编号删除它们。

  • 最后,可以使用rwatch来检测何时读取变量,并且可以使用watch来检测何时读取或写入变量。

2.6 附加到正在运行的进程

如果程序已经启动,并且想要停止并调试它,首先您需要进程 ID (可以从 Unix 的ps命令获取它)。然后,用attach命令加 PID 附加到(并中断)正在运行的程序。

首先,运行不带参数的gdb。

如下图所示,我附加到了运行的进程3490,但它告诉我它位于称为__nanosleep_nocancel()的函数里。但这不稀奇,因为我在我的代码中调用了sleep()。实际上,请求backtrace正好可以显示此调用堆栈。所以finish几次,就可以回到main函数。

$ gdb
GNU gdb 6.8
Copyright (C) 2008 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i486-slackware-linux".
(gdb) attach 3490
Attaching to process 3490
Reading symbols from /home/beej/hello...done.
Reading symbols from /lib/libsafe.so.2...done.
Loaded symbols for /lib/libsafe.so.2
Reading symbols from /lib/libc.so.6...done.
Loaded symbols for /lib/libc.so.6
Reading symbols from /lib/libdl.so.2...done.
Loaded symbols for /lib/libdl.so.2
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
0xb7eab21b in __nanosleep_nocancel () from /lib/libc.so.6
(gdb) backtrace 
#0 0xb7eab21b in __nanosleep_nocancel () from /lib/libc.so.6
#1 0xb7eab05f in sleep () from /lib/libc.so.6
#2 0x080483ab in main () at hello.c:10
(gdb) finish
Run till exit from #0 0xb7eab21b in __nanosleep_nocancel ()
   from /lib/libc.so.6
0xb7eab05f in sleep () from /lib/libc.so.6
(gdb) finish
Run till exit from #0 0xb7eab05f in sleep () from /lib/libc.so.6
0x080483ab in main () at hello.c:10
10			sleep(1);
(gdb) list
5	{
   
6		int i = 1;
7	
8		while (i < 60) {
   
9			i++;
10			sleep(1);
11		}
12	
13		return 0;
14	}
(gdb) print i
$1 = 19
(gdb) quit
The program is running.  Quit anyway (and detach it)? (y or n) y
Detaching from program: /home/beej/hello, process 3490

‎请注意,当我回到‎‎main() ‎时‎,我将打印‎‎i‎‎的值,并且它的值是 19,因为在这种情况下,程序已运行了 19 秒,‎‎并且我‎‎每秒获得一次增量。‎一旦我们退出调试器并脱离程序,程序将恢复正常运行。‎

结合set variable命令‎一起使用,你将获得更多的调试选项!

2.7 ‎使用核心转储进行事后分析‎

‎假设您生成并运行了一个程序,并且由于某种原因使用了核心转储:‎

$ cc -g -o foo foo.c
$ ./foo
Segmentation fault (core dumped)

‎这意味着核心文件(崩溃时内存快照)已创建名称为"core"。如果您没有获取核心文件(即,它只显示"分段错误",并且未创建核心文件),则可能是 ulimit 设置得太低,可以尝试在 shell 提示符下键入‎‎ulimit -c。‎

‎您也可以使用‎‎-c‎‎选项启动‎‎gdb‎‎以指定核心文件:

$ gdb -tui -c core foo

‎而且,如果在 TUI 模式下,您将看到一个信息屏,告诉您程序退出的原因(“信号 11,分段错误”),并且高亮显示违规行,在dumb模式下,有问题的行用下划线打印出来。

在此示例中,我将导致问题的变量打印了出来。事实上,它是 NULL:

‎即使您没有所有源代码,从程序崩溃点获取‎‎backtrace‎‎通常也很有用。‎

2.8 窗口函数

‎在 TUI 模式下,您可以通过命令info win获取现有窗口的列表。然后,您可以使用focus (or fs)命令更改聚焦到哪个窗口。focus‎‎以窗口名称或"prev"或"next"作为参数。

有效的窗口名称有:“SRC”(源窗口)、“CMD”(命令窗口)、“REGS”(注册窗口)和"ASM"(程序集窗口)。当 SRC 窗口具有焦点时,箭头键将移动源代码;当 CMD 窗口具有焦点时,箭头键将在命令历史记录中选择上一个和下一个命令。(移动 SRC 窗口单行和单个页面的命令为‎‎+‎‎、 - ‎‎、<‎‎ 和 ‎ >‎‎)。

(gdb) info win
        SRC     (36 lines)  <has focus>
        CMD     (18 lines)
(gdb) fs next
Focus set to CMD window.
(gdb) info win
        SRC     (36 lines)
        CMD     (18 lines)  <has focus>
(gdb) fs SRC
Focus set to SRC window.
(gdb)

‎(窗口名称区分大小写。)‎

‎winheight(‎‎或‎‎wh)‎‎命令:设定窗口的高度,但有时工作的不是很好。‎

2.9 寄存器和汇编窗口

‎在 TUI 模式下,layout‎‎命令控制窗口的显示与否。此外‎‎,tui reg‎‎命令打开寄存器窗口。‎

命令 描述
layout src ‎标准布局 — 顶部显示SRC窗口,底部显示CMD窗口‎
layout asm 顶部显示ASM窗口,底部显示CMD窗口‎
layout split 三个窗口:SRC在顶部,ASM在中间,CMD在底部
layout reg 将REG窗口显示在SRC或ASM窗口的上面
tui reg general 显示通用寄存器
tui reg float 显示浮点寄存器
tui reg system 显示"系统"寄存器
tui reg next 显示下一页REG窗口 , 这一点很重要,因为可能有一些不属于"通用"、"浮点"或"系统"的寄存器页面

下面是一个在"split"模式下显示SRC和ASM窗口:

英特尔机器可以显示两种风格的汇编代码:Intel和 AT&T。您可以通过命令set disassembly-flavor设置在拆解窗口中显示哪种风格。flavor的有效值为"intel"和"att"。如果已打开了ASM窗口,则必须先关闭它并重新打开它(例如,先执行layout src命令,然后执行 layout split命令)。

要在dumb模式下显示寄存器,请键入info registers,它默认仅显示整数寄存器的信息,或键入info all-registers命令显示所有整数、浮点和系统寄存器。

2.10 编写前端

GDB 支持"机器接口解释器"或 GDB/MI。可以通过在gdb命令行上添加**–interpreter**选项打开它。

基本上,您将启动gdb并读取命令和结果(结合管道使用更方便),非常简单。

有关所有详细信息,请参阅 GDB 文档

3. 快速参考

命令参数以斜体表示。可选参数位于方括号中。所有命令都可以缩写,直到它们变得模棱两可。

这个列表是非常不完整的,只显示在本教程中谈论的东西!

命令 描述
帮助命令
help command 获取有关特定命令的帮助
apropos keyword 搜索特定关键字的帮助
启动和退出
gdb [-tui] [-c core] [exename] 在可执行文件或独立文件上启动gdb;指定"-tui"以启动 TUI GUI;
指定"-c core文件名称"以查看发生崩溃的位置。
run [arg1] [arg2] […] 使用给定的命令行参数运行当前加载的程序
quit 退出调试器
file exename 按名称加载可执行文件
断点和观察点
break location 在特点位置、行号或文件处设置断点(例如"main",“5"或"hello.c:23”)
watch expression 当变量被写入时产生中断
rwatch expression 当变量被读取时产生中断
awatch expression 当变量被写入或读取时产生中断
info break 显示断点和观察点信息和数字
info watch 同上
clear location 从特定位置处清除断点
delete num 通过数字清除断点或观察点
步进和运行
next 运行到此函数的下一行
step 如果可能,将步进到此行上的函数的内部
stepi 单步执行一条汇编指令
continue 从此处继续运行
CTRL-C 停止程序运行
finish 运行到函数结束
advance location 前进到某个位置(e.g. “somefunction”, “5”, or “hello.c:23”)
jump location 跳到某个位置继续运行
检查和修改变量
display expression 在程序的每一步中显示变量或表达式的值, 当然前提是表达式必须在当前作用域中有意义
info display 显示当前显示的表达式及其编号的列表
undisplay num 停止显示由其编号标识的表达式
print expression 打印变量或表达式的值
printf formatstr expressionlist 使用printf()执行一些格式化的输出,(printf "i = %d, p = %s\n", i, p
set variable expression 给变量赋值,(set variable x=20
set (expression) 同上
窗口命令
info win 显示当前窗口信息
focus winname 将焦点设置为特定窗口
fs winname 同上
layout type 设置窗口类型(“src”、“asm”、“split"或"reg”)
tui reg type 设置寄存器窗口类型(“general”, “float”, “system”, or “next”)
winheight val 设置窗口高度(利用“+”或“-”相对值或绝对值)
wh val 同上
set disassembly-flavor flavor 设置汇编指令格式(Intel或att)
其它命令
RETURN 点击RETURN重复上一次命令
backtrace 追溯当前指令的函数嵌套调用情况
bt 同上
attach pid 通过 PID 连接到已运行的进程
info registers 显示整数寄存器信息
info all-registers 显示整数、浮点数、系统等所有寄存器信息

获取更多知识,请点击关注:
嵌入式Linux&ARM
CSDN博客
简书博客
知乎专栏