Skip to content

LV030-GDB基础应用

说明:本节所有的操作都是在编译服务器(我这里是VMware中的ubuntu20.04)中进行,并不是在arm开发板。

一、概述

我们知道,GDB 的主要功能就是监控程序的执行流程。这也就意味着,只有当源程序文件编译为可执行文件并执行时,GDB 才会派上用场。

1. 生成可执行文件

Linux 发行版中,经常使用 GCC编译 C、C++ 程序。但需要注意的是,仅使用 gcc(或 g++)命令编译生成的可执行文件,是无法借助 GDB 进行调试的。我们平时的编译命令是:

shell
gcc <src_list>.c -Wall

这样会生成a.out可执行程序,但是此文件不支持使用 GDB 进行调试。这是因为使用 GDB 调试某个可执行文件,该文件中必须包含必要的调试信息(比如各行代码所在的行号、包含程序中所有变量名称的列表(又称为符号表)等),而通过上述命令生成的 a.out 则没有。要生成可用于GDB调试的可执行文件,我们只需要加上 -g 选项即可:

shell
gcc <src_list>.c -Wall -g

GCC 编译器支持 -O(等于同 -O1,优化生成的目标文件)和 -g 一起参与编译。GCC 编译过程对进行优化的程度可分为 5 个等级,分别为 O0~O4,O0 表示不优化(默认选项),从 O1 ~ O4 优化级别越来越高,O4 最高。

所谓优化,例如省略掉代码中从未使用过的变量、直接将常量表达式用结果值代替等等,这些操作会缩减目标文件所包含的代码量,提高最终生成的可执行文件的运行效率。

而相对于 -O -g 选项,对 GDB 调试器更友好的是 -Og 选项,-Og 对代码所做的优化程序介于 O0 ~ O1 之间,真正可做到“在保持快速编译和良好调试体验的同时,提供较为合理的优化级别”。

2. 测试程序

这里写一个简单的测试程序:

c
#include <stdio.h>

int main(int argc, const char *argv[]) {
    int *p = NULL;

    printf("&p = %p, p = %p\n", &p, p);
    *p = 5;
    printf("*p=%d\n", *p);
    return 0;
}

3. 启动GDB

3.1 命令说明

在生成包含调试信息的可执行文件的基础上,启动 GDB 调试器的指令如下:

shell
gdb target.out
gdb target.out --silent # 屏蔽部分免责条款的信息

该指令在启动 GDB 的同时,会打印出一堆免责条款。通过添加 --silent(或者 -q、--quiet)选项,可将比部分信息屏蔽掉。无论使用以上哪种方式,最终都可以启动 GDB 调试器,启动成功的标志就是最终输出的 (gdb)。通过在 (gdb) 后面输入指令,即可调用 GDB 调试进行对应的调试工作。

3.2 使用示例

我们执行下面的命令:

shell
gdb a.out

会有如下打印信息:

shell
 sumu@virtual-machine:~/workspace/c-learning/02-c-basic/21-debug [main  +1 ~0 -0 !]
$ gdb a.out 
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2
Copyright (C) 2020 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 "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.out...
(gdb)

4. 退出GDB

我们调试结束了,需要退出调试器的时候我们可以使用如下命令退出:

shell
(gdb) q

然后按下 Enter 按键就可以退出 gdb 调试了。

二、 基本调试命令

1. list 命令

在调试器中可缩写为 l,可以列出所调试程序的代码。使用格式如下:

shell
(gdb) l [option]
# 或者
(gdb) list [option]

l 选项默认只显示 10 行源代码,如果查看后续代码,继续按 Enter 回车即可。

参数说明

option说明
省略gdb 会将 gdb 当前所处的行以及后面的代码打印在屏幕上。
lineNumber打印指定行附近的代码。如 list 8,gdb 会将 8 行前后的代码打印在屏幕上。
-gdb 会将 gdb 当前所处的行前面的代码打印在屏幕上。
functionName打印名称为 functionName 的函数的上下文的代码。

2. run 命令

在调试器中可缩写为 r,可以开始运行程序,若无断点,则会全部执行一遍,遇到第一个断点会停下。使用格式如下:

shell
(gdb) r
# 或者
(gdb) run

run 命令除了可以启动程序的执行,还可以在任何时候重新启动程序。

3. break 命令

在调试器中可缩写为 b,用于设置断点。使用格式如下:

shell
(gdb) b [option]
# 或者
(gdb) break [option]

参数说明

option说明
lineNumber在指定的代码行打断点
filename:lineNumbergdb 会在名称为 filename 的文件中的第 lineNumber 行打断点。
filename:functiongdb 在名称为 filename 的文件中的 function 函数入口处打断点。
*addressgdb 在程序运行的内存地址处打断点。

4. info 命令

用于查看断点信息,使用格式如下:

shell
(gdb) info b     # 查看所有断点信息
(gdb) info b n   # 查看第 n 个断点信息

断点信息显示格式如下:

shell
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x00005555555546d6 in main at 00GCC.c:6
        breakpoint already hit 1 time
2       breakpoint     keep y   0x00005555555546c8 in main at 00GCC.c:5
3       breakpoint     keep y   0x00005555555546b9 in main at 00GCC.c:3
4       breakpoint     keep y   0x00005555555546b9 in main at 00GCC.c:2

5. delete 命令

用于删除断点。使用格式如下:

shell
(gdb) delete  b     # 删除所有断点信息
(gdb) delete  b n   # 删除第 n 个断点信息

6. next 命令

在调试器中可缩写为 n,用于单步运行程序,但是如果有函数调用的话,它不会进入函数体(直接将函数内容执行完毕)。使用格式如下:

shell
(gdb) n [option]
# 或者
(gdb) next [option]

参数说明

option说明
省略一条一条单步执行。
count执行后边的 count 条指令。

7. step 命令

在调试器中可缩写为 s,用于单步运行程序,如果有函数调用的话,它会进入函数体。使用格式如下:

shell
(gdb) s [option]
# 或者
(gdb) step [option]

参数说明

option说明
省略一条一条单步执行。
n执行后边的 n 条指令。

8. until 命令

在调试器中可缩写为 u,用于运行到某一行程序或者直接运行完循环,如果有函数调用的话,它会进入函数体。使用格式如下:

shell
(gdb) u [option]
# 或者
(gdb) until [option]

参数说明

option说明
省略可以使 GDB 调试器快速运行完当前的循环体,并运行至循环体外停止。
n某行代码的行号,以指示 GDB 调试器直接执行至指定位置后停止。

注意】until 命令并非任何情况下都会发挥这个作用,只有当执行至循环体尾部(最后一行代码)时,until 命令才会发生此作用;反之,until 命令和 next 命令的功能一样,只是单步执行程序。

9. continue 命令

在调试器中可缩写为 c,用于继续运行程序。当程序在某一断点处停止后,用该指令可以继续执行,直至遇到断点或者程序结束。使用格式如下:

shell
(gdb) c
# 或者
(gdb) continue

10. print 命令

10.1 查看变量

在调试器中可缩写为 p,用于打印指定变量的值。使用格式如下:

shell
(gdb) p [option --][/fmt] expr
# 或者
(gdb) print [option --][/fmt] expr

参数说明

  • option:表示该命令所支持的选项,这些选项可以控制 print 命令输出指定内容的变量或者表达式的值,是可选的;
option说明
-address on|off查看某一指针变量的值时,是否同时打印其占用的内存地址,默认值为 on。该选项等同于单独执行 set print address on|off 命令。
-array on|off是否以便于阅读的格式输出数组中的元素,默认值为 off。该选项等同于单独执行 set printf array on|off 命令。
-array-indexes on|off对于非字符类型数组,在打印数组中每个元素值的同时,是否同时显示每个元素对应的数组下标,默认值为 off。该选项等同于单独执行 set print array-indexes on|off 命令。
-pretty on|off以便于阅读的格式打印某个结构体变量的值,默认值为 off。该选项等同于单独执行 set print pretty on|off 命令。
- `fmt`:指定输出变量或表达式值时所采用的格式,是可选的;
/fmt功 能
/x以十六进制的形式打印出整数。
/d以有符号、十进制的形式打印出整数。
/u以无符号、十进制的形式打印出整数。
/o以八进制的形式打印出整数。
/t以二进制的形式打印出整数。
/f以浮点数的形式打印变量或表达式的值。
/c以字符形式打印变量或表达式的值。

例如,

shell
(gdb) p /x a  # 以十六进制显示变量 a 的值
  • expr:指定要查看的变量或表达式。

注意

(1)options 参数和 /fmt 或者 expr 之间,必须用 --(2 个 - 字符)分隔。

(2)[ ] 里边的内容是可选的,可以没有。

10.2 修改变量值

print 命令还支持修改变量值,一般格式如下:

shell
(gdb) print variable=value
  • variable:表示要修改的变量名。
  • value:表示修改后的值。

10.3 查看数组

print 命令还支持 @ 运算符,多用于查看数组,一般格式如下:

shell
(gdb) print first@len
  • first:用于指定数组查看区域内的首个元素的值。
  • len:用于指定自 first 元素开始查看的元素个数。

10.4 查看不同文件同名变量

当程序中包含多个作用域不同但名称相同的变量或表达式时,可以借助 :: 运算符明确指定要查看的目标变量或表达式。:: 运算符的语法格式如下:

shell
(gdb) print file::variable
(gdb) print function::variable
  • file:用于指定具体的文件名。
  • function:用于指定具体所在函数的函数名。
  • variable:表示要查看的目标变量或表达式。

11. finish 命令

实际调试时,在某个函数中调试一段时间后,可能不需要再一步步执行到函数返回处,希望直接执行完当前函数,这时可以使用 finish 命令。finish 命令会执行函数到正常退出,并在跳出函数后暂停程序的执行。

shell
(gdb) finish
(gdb) fi

12. return 命令

return同样是结束当前调用函数,不过是立即结束执行当前函数并返回,也就是说,如果当前函数还有剩余的代码未执行完毕,也不会执行了。除此之外,return 命令还有一个功能,即可以指定该函数的返回值。到上一层函数调用处停止程序执行。

shell
(gdb) return 
(gdb) return val # 指定函数返回值为 val

13. jump 命令

jump 命令的功能是直接跳到指定行继续执行程序,其语法格式为:

shell
(gdb) jump location

其中,location 通常为某一行代码的行号。

也就是说,jump 命令可以略过某些代码,直接跳到 location 处的代码继续执行程序。这意味着,如果我们跳过了某个变量(对象)的初始化代码,直接执行操作该变量(对象)的代码,很可能会导致程序崩溃或出现其它 Bug。另外,如果 jump 跳转到的位置后续没有断点,那么 GDB 会直接执行自跳转处开始的后续代码。

三、tui模式

先上一张图吧,简单来说就是可以对照源码进行调试,这就是 GDB 自带的 GDB TUI ,它看起来比直接使用 list 要更美观一点:

image-20260503100731157

1. 启动方式

  • (1)直接使用命令启动
shell
gdbtui -q 需要调试的程序名
# 或者
gdb -tui -q 需要调试的程序名
  • (2)快捷键
shell
Ctrl + x + a

正常开启 GDB 调试,然后按快捷键就可以启动啦。

2. 相关命令

2.1 layout

shell
# 命令
(gdb) layout src  # 显示源代码窗口
(gdb) layout asm  # 显示汇编窗口
(gdb) layout regs # 显示源代码/汇编和寄存器窗口
(gdb) layout split  # 显示源代码和汇编窗口
(gdb) layout next # 显示下一个layout
(gdb) layout prev # 显示上一个layout
(gdb) winheight src + num # 将代码窗口的高度扩大 num 行代码
(gdb) winheight src - num # 将代码窗口的高度降低 num 行代码

# 快捷键
Ctrl + L # 刷新窗口
Ctrl + x + 1 # 单窗口模式,显示一个窗口
Ctrl + x + 2 # 双窗口模式,显示两个窗口
Ctrl + x + a # 回到传统模式,即退出layout,回到执行layout之前的调试窗口。

2.2 focus

在默认设置下,方向键和 PageUp/PageDown 都是用来控制 GDB TUI 的 src 窗口的,所以我们在终端中常用上下键显示前一条命令和后一条命令的功能就没有了。但是我们可以通过命令将窗口焦点移动到命令窗口,这样就可以啦。我们可以在 GDB 调试中查看 focus 命令的帮助如下:

shell
(gdb) help focus
# 然后我们会得到如下提示
focus, fs
Set focus to named window or next/prev window.
Usage: focus [WINDOW-NAME | next | prev]
Use "info win" to see the names of the windows currently being di
splayed.

其中的 WINDOW-NAME 可选的有如下几种:

shell
src  : the source window
asm  : the disassembly window
regs : the register display
cmd  : the command window

当我们执行:

shell
(gdb) help cmd

这个时候,窗口焦点就在命令行窗口啦,我们就可以正常使用上下键来选择历史命令啦

四、调试示例

1. 要先知道的

(1)gdb 在执行完一个命令后不输入任何命令直接回车, gdb 会默认执行上一个命令。

(2)只有在代码处于运行或暂停状态时才能查看变量值。

(3)设置断点后程序在指定行之前停止 。

2. 调试过程

我们用上面的测试文件来体验一下调试流程:

shell
 sumu@virtual-machine:~/workspace/c-learning/02-c-basic/21-debug [main  +1 ~0 -0 !]
$ gdb a.out -q
Reading symbols from a.out...
(gdb) l                   # <=== 1.显示带行号的源代码
1       #include <stdio.h>
2
3       int main(int argc, const char *argv[]) {
4           int *p = NULL;
5
6           printf("&p = %p, p = %p\n", &p, p);
7           *p = 5;
8           printf("*p=%d\n", *p);
9           return 0;
10      }                # <=== 默认情况下,l 选项只显示 10 行源代码,如果查看后续代码,按 Enter 回车即可 
(gdb) b 6                # <=== 2.在源码第6行打断点
Breakpoint 1 at 0x1193: file 001-segmentation-fault.c, line 6.
(gdb) b 9                # <=== 3.在源码第9行打断点
Breakpoint 2 at 0x11d2: file 001-segmentation-fault.c, line 9.
(gdb) b                
No default breakpoint address now.
(gdb) info b             # <=== 4.查看所有断点
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000001193 in main at 001-segmentation-fault.c:6
2       breakpoint     keep y   0x00000000000011d2 in main at 001-segmentation-fault.c:9
(gdb) r                  # <=== 5.运行程序,遇到断点停止,断点这一行并未执行
Starting program: /home/sumu/workspace/c-learning/02-c-basic/21-debug/a.out 

Breakpoint 1, main (argc=1, argv=0x7fffffffcda8) at 001-segmentation-fault.c:6
6           printf("&p = %p, p = %p\n", &p, p);
(gdb) p p               # <=== 6.查看指针变量p的值
$1 = (int *) 0x0        # <=== p为null
(gdb) p &p              # <=== 7. 查看p的地址
$2 = (int **) 0x7fffffffcca0
(gdb) n                 # <=== 8.单步运行
&p = 0x7fffffffcca0, p = (nil) # <=== 源码第6行程序执行完毕,这里是打印的信息
7           *p = 5;     # <=== 下一步执行的源码第7行程序
(gdb) n                 # <=== 9.单步运行

Program received signal SIGSEGV, Segmentation fault. # <=== 执行产生崩溃
main (argc=1, argv=0x7fffffffcda8) at 001-segmentation-fault.c:7
7           *p = 5;
(gdb) c                 # <=== 10.继续执行程序,遇到下一个断点结束
Continuing.

Program terminated with signal SIGSEGV, Segmentation fault. # <=== 因为已经崩溃了,所以直接结束了
The program no longer exists.
(gdb) q                 # <=== 11.退出调试