GDB调试学习
本文最后更新于:2023年12月17日 下午
简介
GDB是一个由GNU开源组织发布的、UNIX / LINUX操作系统下的、基于命令行的、功能强大的程序调试工具。 对于一名Linux下工作的c/c++程序员,gdb是必不可少的工具。标准的 GDB 是纯命令行式的,但也有一些基于它的图形化工具(比如 DDD、Data Display Debugger),但用好 GDB 命令行调试,还是我们的一项基本素质。
GDB 不仅是一个调试工具,它也是一个学习源码的好工具。单纯的源码是静态的,虽然你可以分析它的整体架构,在头脑里模拟出它的工作流程,但计算机实在是太复杂了,内外部环境因素很多,仅靠“人肉分析”很难完全理解它的逻辑。这个时候,GDB 就派上用场了,以调试模式启动,任意设定外部条件,从指定的入口运行,把程序放慢几万倍,细致地观察每个变量的值,跟踪代码的分支和数据的流向,这样走上几个来回之后,再结合源码,就能够对程序的整体情况“了然于胸”。
本篇文章将简单地介绍 GDB 常见用法,更多内容可以参考下面的文章:
GDB使用流程
1.启动GDB调试
测试程序源代码如下:
1 |
|
启动命令如下:
1 |
|
效果如下:
2.查看源码
**list(简写 l)**: 查看源程序代码,默认显示10行,按回车键或继续输入l
查看下10行。
效果如下所示:
3.运行程序
run(简写 r) :运行程序直到遇到 结束或者遇到断点等待下一个命令;
效果如下所示(这里我没有设置断点,所以程序直接运行到结束):
4.设置断点
break(简写 b) :命令格式 b 行号
,在某行设置断点;
info breakpoints (简写 i b
) :显示断点信息
- Num: 断点编号
- Type:类型,breakpoint 或者 watchpoint
- Disp:断点执行一次之后是否有效 kep:有效 dis:无效
- Enb: 当前断点是否有效 y:有效 n:无效
- Address:内存地址
- What:位置
演示效果如下:
5.单步执行
首先需要输入run
启动程序,运行到第一个断点处,然后可以选择step
单步调试(如果有函数调用则进入函数),next
单步跟踪程序(遇到函数调用,直接调用函数,不会进入函数体内),continue
继续运行到下一个断点处。
程序运行的相关命令如下:
- run:简记为 r ,其作用是运行程序,当遇到断点后,程序会在断点处停止运行,等待用户输入下一步的命令。
- continue (简写c ):继续执行,到下一个断点处(或运行结束)
- next:(简写 n),单步跟踪程序,当遇到函数调用时,也不进入此函数体;此命令同step 的主要区别是,step 遇到用户自定义的函数,将步进到函数中去运行,而 next 则直接调用函数,不会进入到函数体内。
- step (简写s):单步调试如果有函数调用,则进入函数;与命令n不同,n是不进入调用的函数的
- until:当你厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体。
- until+行号: 运行至某行,不仅仅用来跳出循环
- finish: 运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息。
- **call 函数(参数)**:调用程序中可见的函数,并传递“参数”,如:call gdb_test(55)
6.查看变量
- print :打印变量或表达式的值
- whatis :查询变量或函数
- pt :跟whatis作用类似
- display:在单步调试的时候很有用,使用display命令设置一个表达式后,它将在每次单步进行指令后,紧接着输出被设置的表达式及值。
- 可以通过info dispaly,查看当前被设置的变量,然后可以用undisplay num(变量对应的编号)取消设置
7.退出GDB
使用quit
命令退出即可。
GDB常用命令
下面列出最常用的GDB命令:
- pt:查看变量的真实类型,不受 typedef 的影响。
- bt:显示当前调用堆栈。
- up/down:在函数调用栈里上下移动。或者使用
frame 函数帧号
跳转。 - fin:直接运行到函数结束。
- i b:查看所有的断点信息。
- i locals:查看当前堆栈页的所有变量。
- wh:启动“可视化调试”。这个是我最喜欢的命令,可以把屏幕分成上下两个窗口,上面显示源码,下面是 GDB 命令输出,不必再用“l”频繁地列出源码了,能够大大提高调试的效率。
- ctrl x + a :退出或进入可视化模式
- layout regs:显示源代码/汇编和寄存器窗口
- layout split:显示源代码和汇编窗口
分析CoreDump
在真实生产环境中,程序可能会崩溃而产生CoreDump文件,此时我们通常就需要用gdb来调试CoreDump文件,分析程序崩溃的原因。
接下来,来模拟这一过程。首先需要修改下源代码,如下所示:
1 |
|
然后编译生成新的二进制文件:
1 |
|
然后需要修改下配置[3],命令如下:
1 |
|
然后执行修改后的程序,发生段错误:
但奇怪的是,并没有生成core文件,明明我已经修改了ulimit。后来几经波折,发现了原因,参考在Linux上利用core dump和GDB调试segfault[4]。简单来说,就是Ubuntu默认忽略非Ubuntu软件包的二进制文件的崩溃日志,也就不会产生core dump文件。调整的做法也比较简单,就是重新设置kernel.core_pattern的值(该值表示core dump文件的输出目录),执行下面命令:
1 |
|
现在再执行之前的程序,就可以在对应目录下生成core dump文件,并可以用gdb来进行调试分析,如下图所示,可以看到出错原因在源代码的第15行,对一个空指针赋值: