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
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<stdio.h>
int square(int x) {
return x * x;
}

int get_num(int x) {
int x_2 = square(x);
return x_2;
}

int main(){
int a = 10;
int b = get_num(a);
}

启动命令如下:

1
2
3
gcc test.cpp -o test # 编译源文件
gdb test
gdb -q test # -q 表示不打印gdb版本信息,界面较为干净;

效果如下:

启动gdb

2.查看源码

**list(简写 l)**: 查看源程序代码,默认显示10行,按回车键或继续输入l查看下10行。

效果如下所示:

list 查看源程序代码

3.运行程序

run(简写 r) :运行程序直到遇到 结束或者遇到断点等待下一个命令;
效果如下所示(这里我没有设置断点,所以程序直接运行到结束):

run 运行程序

4.设置断点

break(简写 b) :命令格式 b 行号,在某行设置断点;
info breakpoints (简写 i b) :显示断点信息

  • Num: 断点编号
  • Type:类型,breakpoint 或者 watchpoint
  • Disp:断点执行一次之后是否有效 kep:有效 dis:无效
  • Enb: 当前断点是否有效 y:有效 n:无效
  • Address:内存地址
  • What:位置

演示效果如下:

b 设置断点

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常用命令

下面列出最常用的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<stdio.h>
int square(int x) {
return x * x;
}

int get_num(int x) {
int x_2 = square(x);
return x_2;
}

int main(){
int a = 10;
int b = get_num(a);
int* p = NULL;
*p = 1;
}

然后编译生成新的二进制文件:

1
gcc -g test.cpp -o test6

然后需要修改下配置[3],命令如下:

1
2
# 作用是让程序崩溃是产生的core dump文件大小没有限制,默认是0表示不生成core dump文件
ulimit -c unlimited

然后执行修改后的程序,发生段错误:

程序崩溃发生段错误

但奇怪的是,并没有生成core文件,明明我已经修改了ulimit。后来几经波折,发现了原因,参考在Linux上利用core dump和GDB调试segfault[4]。简单来说,就是Ubuntu默认忽略非Ubuntu软件包的二进制文件的崩溃日志,也就不会产生core dump文件。调整的做法也比较简单,就是重新设置kernel.core_pattern的值(该值表示core dump文件的输出目录),执行下面命令:

1
sudo sysctl -w kernel.core_pattern=/tmp/core/core-%e.%p.%h.%t

现在再执行之前的程序,就可以在对应目录下生成core dump文件,并可以用gdb来进行调试分析,如下图所示,可以看到出错原因在源代码的第15行,对一个空指针赋值:

调试core文件

备注/参考


GDB调试学习
https://2017zhangyuxuan.github.io/2022/02/16/2022-02/2022-02-16 GDB调试学习/
作者
Zhang Yuxuan
发布于
2022年2月16日
许可协议