【cpp查漏补缺】4-gdb的使用
为什么要使用GDB,而不是打印大法?
这是来自于ChatGPT的回答:
GDB(GNU Debugger)是一个功能强大的调试工具,相对于简单地通过打印输出来进行调试,它具有以下优势:
1.实时交互性: GDB允许您在程序执行期间随时停止程序的执行,检查变量的值,执行特定的命令(如单步执行、继续执行、设置断点等),并进行实时交互。这可以帮助您更深入地了解程序的内部状态,而不仅仅是静态地查看输出。
2.非侵入性: 使用打印调试时,您需要在代码中插入打印语句,然后重新编译和运行程序。这可能会导致不必要的代码污染和额外的工作。相比之下,GDB允许您在不修改源代码的情况下进行调试。
3.回溯和堆栈跟踪: GDB可以帮助您查找程序中的错误并跟踪它们的根本原因。它提供了堆栈跟踪功能,允许您查看调用堆栈,找出错误发生的位置,以及如何到达错误点的路径。这通常比简单的打印输出更容易找到问题。
4.变量监视: GDB允许您监视变量的值,而不需要在代码中插入打印语句。这对于跟踪特定变量的值在程序执行期间如何变化非常有用。
5.条件断点: GDB允许您设置条件断点,只有当特定条件满足时才会触发断点。这对于跟踪特定情况下的程序行为非常有用。
6.内存检查: GDB可以帮助您检查程序中的内存错误,如缓冲区溢出、内存泄漏等。这些问题很难通过简单的打印调试来发现。
7.多线程调试: 如果程序涉及多线程,GDB能够帮助您调试多个线程之间的交互和竞争条件,以及线程的状态和调用堆栈。
8.脚本支持: GDB支持Python脚本,您可以使用它们来自动化和扩展调试过程。
总之,GDB相对于简单的打印调试提供了更强大、更灵活和更全面的调试工具,可以帮助您更快速、更有效地发现和解决程序中的问题。当您需要深入了解程序执行过程、排除复杂问题或进行更高级的调试时,GDB通常是一个更好的选择。但对于某些简单问题,打印调试仍然可以是一个有效的快速解决方案。
总的来说,最为方便的地方是只用编译一次代码。打印大法确实舒服,但是每添加一个打印语句就需要重新编译一次项目,这对于大项目来说是非常耗时的。除此之外,脚本也是值得留意的。这为进一步提升效率提供了可能。
ELF文件
待补
GDB基本使用
调试信息
首先,编译程序需要开启-g
选项,否则不会带上调试信息。
你可以尝试一下
readelf -S <目标文件> | grep debug
指令,尝试带上-g
和不带上-g
选项,观察区别。你会发现少了几段。
同时你也可以试一下file <目标文件>
,如果提示是stripped
,那么说明符号表信息和调试信息被去除,不能调试。但没有去除不代表可以调试(因为除了debug段还有其他段)。
之后,你可以输入gdb <目标程序>
,然后输入run
,开始运行程序并进行调试。
输入设置
如果需要命令行参数,你可以在run
指令后面跟上命令行参数,或者使用set args
,后面跟上命令行参数,然后run
并执行。注意,set args
可以使用重定向,从一个文件读取输入。
如果需要从标准输入里面读取数据,可以使用set args
并重定向。
进入调试
直接调试
1 | # case 1 |
调试已运行程序
1 | pidof <obj_file> # note that <obj_file> is running, using `pidof` to get the pid |
这里值得提醒的是,第三种方式可以解决已运行程序没有debug段的问题:首先重新编译一个有debug段的程序,然后attach到正在运行的,没有debug段的程序,这样就可以调试已运行的程序。
断点设置
1 | (gdb) info breakpoints # 查看已设置断点相关信息 |
查看变量
手动查看变量
1 | p ['file'::]['func'::]var # 查看[file文件中][func函数中]变量var的值 |
查看内存内容
1 | # n内存单元数 |
自动显示变量
1 | # 程序中断时就会显示 display 定义的变量 |
查看寄存器内容
1 | info registers |
源码比对
1 | l # 看 1 - listsize 行 |
调试
基本的步进调试
1 | gdb gdbStep # 启动调试 |
堆栈调用调试
1 | bt # backtrace,可以查看完整的调用堆栈 |
指定源码路径
1 | # 切换至源码移动后的路径 |
源代码比对
1 | layout src |
core文件调试
首先,core文件是core dump时产生的。首先需要修改core dump文件的大小:
1 | ulimit -c # 查看core文件大小,单位为块,一块大小默认为512字节 |
core文件的具体使用见【熟悉core文件调试】一节。
然后可以这样调试core文件:
1 | gdb program_file core_file |
一些示例
熟悉基本操作——并查集
下面是并查集的代码:
1 | // dsu.cpp |
下面是测试用例:
1 | 1 4 5 |
下面是期望输出:
1 | 4 |
现在,我们完成以下任务: 从这个例子可以看出,gdb所停顿的语句,是暂时没有执行的。这个要注意。其次,在给dsu加上watchpoint后,可以发现在init()函数里面时,都会出现一大堆不可名状的内容。原因在于vector内部的实现,会动态改变vector的大小,并重新分配空间。 也许用递归程序能够更好地体现。比较重要的是
1.发生merge操作时,观察dsu的变化情况:
参考答案
1
2
3
4
5
6
7
8
9
10gdb dsu
set args < input.in
watch dsu
b merge
b 32 # Note that b 32 is to skip the init(), which would be boring for reallocate vector dsu. You may use disable to disable some breakpoints.
info b
# repeat below for n times
c # You may use `c 10` to skip some allocation for vector
finish # when stop for entering the function merge
p dsu # show the content of dsu array
2.每次发生findfa时,查看函数调用帧
参考答案
1
2
3
4
5
6
7b findfa
run
c
bt
frame 1
info local
info argsinfo local
,可以展现所有的局部变量。
熟悉core文件调试
这次是个很简单的例子:
1 |
|
由于作者是边学边做,这里作者遇到了一些问题:作者使用的发行版本为ubuntu22.04,经过查阅资料,发现在
/proc/sys/kernel/core_pattern
有保存存放地址:|/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -- %E
,经过再进一步的查阅,发现这个地址跟apport这个东西有关。查到的大多数的资料都是修改/etc
下面某些文件的配置。我们这里不这样做,而是使用gcore:1 | gdb |
生成core文件并执行后,我们可以看到gdb直接报出错误原因和位置:
1 | Program received signal SIGSEGV, Segmentation fault. |
那么core文件是什么呢?core文件就是程序崩溃时的内存快照,记录了崩溃瞬间的内部状态。
为什么会这样?
1 |
|
打印的结果是3和0,也许你大概猜到是什么原因了,但我们现在希望能探究一下,这究竟是什么原因。
参考答案
1 | watch *p # 这是最快的做法 |
不难知道这个p所在的地址被反复发生未知读写的原因是g1
函数的栈帧释放后,后面调用的函数又使用了这一块内存,导致内容被反复读写。从调试结果可以看出来,这个值的改变有两个来源:一个是来自../sysdeps/x86_64/dl-trampoline.h
,一个是来自/lib/x86_64-linux-gnu/libstdc++.so.6
。后者结合输出不难猜出是cout的原因。那前者是什么呢?经过查阅资料,发现是管理动态链接库用的。
那么我们继续修改代码如下:
1 | int* p = nullptr; |
emm,发现还是有
../sysdeps/x86_64/dl-trampoline.h
参与,不知道是什么原因,动态链接库是哪里来的?是不是跟加载有关?还是?先把疑问留在这里吧。
其他
【编译原理】课程项目——未修复的bug
参考资料
大佬们都是怎么用gdb的?或者用吗? - 张小方的回答 (介绍了GDB在大型项目里面的实战?有空看看。)
GDB调试入门指南
- Title: 【cpp查漏补缺】4-gdb的使用
- Author: spiritTrance
- Created at: 2023-10-03 22:33:07
- Updated at: 2024-01-06 20:07:30
- Link: https://spirittrance.github.io/2023/10/03/cpp_4_gdb调试/
- License: This work is licensed under CC BY-NC-SA 4.0.