0%

gdb 调试详解

一直用惯了 IDE 集成的 debug 工具,忽略了 gdb 这个命令行 debug 工具。而最近在做 csapp 的 bomblab, 就不得不来学习它了。所以特此记录.

1. 基本使用 ·

考虑以下我们在 IDE 中要进行 debug 一般需要哪些功能?

  1. 设置断点 (包含条件断点)
  2. 开启 debug
  3. step in, step over, continue
  4. 观察某些值的变化,打印数组 value, 打印某个地址 value
  5. 函数调用 stack, 切换 stack
  6. 临时更改某个变量,参数的值

下面讲解如何用 gdb 实现这些功能。

为了更方便讲解,这里提前把所有常用命令贴出,读者可不用一一记住,在逐渐使用的过程中,自然就能形成记住了.

命令 简写 含义
list l 列出 10 行代码
break b 设置断点
break if b if 设置条件断点
delete [break id] d 删除断点 047 (按照 break id) 删除,没有 break id, 删除所有段 6
disable 禁用断点
enable 允许断点
info i 显示程序状态. info b (列出断点), info regs (列出寄存器) 等
run [args] r 开始运行程序,可带参数
display disp 跟踪查看那某个变量,每次停下来都显示其值
print p 打印内部变量值
watch 监视变量值新旧的变化
step s 执行下一条语句,如果该语句为函数调用,则进入函数执行第一条语句
next n 执行下一条语句,如果该语句为函数调用,不会进入函数内部执行 (即不会一步步地调试函数内部语句)
continue c 继续程序的运行,直到遇到下一个断点
finish 如果进入了某个函数,返回到调用调用它的函数,jump out
set var name = v 设置变量的值
backtrace bt 查看函数调用信息(堆栈)
start st 开始执行程序,在 main 函数中的第一条语句前停下
frame f 查看栈帧,比如 frame 1 查看 1 号栈帧
up 查看上一个栈帧
down 查看那下一个栈帧
quit q 离开 gdb
edit 在 gdb 中进行编辑
whatis 查看变量的类型
search 搜索源文件中的文本
file 装入需要调试的程序
kill k 终止正在调试的程序
layout 改变当前布局 (必备命令)
examine x 查看内存空间 (必备命令)
checkpoint ch debug 快照,需要反复调试某一段代码时,非常有用
disassemble disas 反汇编
stepi si 下一行指令 (遇到函数,进入函数)
nexti ni 下一行指令

这么多命令,但是不要紧,看完一个例子,就掌握其中大半了.

example:

例子来自: https://www.geeksforgeeks.org/gdb-command-in-linux-with-examples/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// gfg.cpp
#include <iostream>
#include <stdlib.h>
#include <string.h>
using namespace std;

int findSquare(int a)
{
return a * a;
}

int main(int n, char** args)
{
for (int i = 1; i < n; i++)
{
int a = atoi(args[i]);
cout << findSquare(a) << endl;
}
return 0;
}

通过 g++ 进行编译,注意编译参数需要添加 "-g":

1
g++ -g -o gfg gfg.cpp

执行:

1
gdb gfg

开启 gdb:

首先介绍的是 break (b) 命令,这是用来设置断点的命令,它的使用格式如下:

1
2
3
4
5
6
7
b
break [function name]
break [file name]:[line number]
break [line number]
break *[address] # 这个用来调试汇编很有用
break ***any of the above arguments*** if [condition]
b ***any of the above arguments***

使用:b main 对 main 函数设置断点.

然后执行:r 1 10 100 命令,把程序跑起来. 1 10 100 是要传入的参数

可以看到,程序停在了 Breakpoint 1 这里,break point 1 中的 1 是什么?gdb 为每个断点设定了一个 id.

怎么查看当前设置了哪些断点?

1
info b

第一列 Num 就是 break point id. Enb 表示当前断点是 enable 的,可以通过 disable breakpoint id disable 一个断点. What 字段表明了当前断点的位置.

ok, 现在我们做到了. 1. 设置断点.2. 查看断点. 3. 其中程序.

接下来我们就一步步的 debug 吧.

使用 n 或者 s 进行单步调试,(两者的区别在于,step 遇到函数会进入函数,next 不会).

值得说明的是,执行一条命令后,直接按回车,会重复执行上一条命令.

现在应该会单步调试了吧.

使用 bt, 可以查看函数调用堆栈:

使用 p a 可以打印 a 变量的值:

p 还可以使用格式符,如 p /x a 把 a 以 hex 格式打印,对于数组,如 int arr [3]; 可以使用

1
p *arr@3

打印. @后跟的是数组长度.

但是到现在,有一个很严重的问题,那就是在 debug 的时候,没办法查看源代码。

gdb 当然想到了这个问题,我们可以通过 l 命令,展示最近的源代码.

l 命令默认展示 10 行代码,可以通过 l [start_line] [end_line] 展示 start_line – end_line 之间的代码.

可是,这还是非常难受,比如我甚至不知道当前执行到哪儿了.

ok, 接下来介绍一个必备的命令: layout

执行 h layout 可以查看 layout 的帮助:

我们关注 LAYOUT-NAME 即可.

可以看到,LAYOUT-NAME 有四个选项:

  • layout src. 展示源代码和命令窗口:

    这就搞定了我们在调试代码时,要查看同步查看源代码的需求。上面展示了我们当前执行到了哪里. B + 展示了我们的断点位置.

  • layout asm

    反汇编布局,可以查看对应的反汇编代码.

这个在 bomblab 中肯定是要用的。平常基本不适用,毕竟汇编用得确实不多.

  • layout split

这个就是同时展示,src 和 asm. 没什么好说的.

  • layout regs

展示寄存器窗口。这个在 bomblab 中也是必备的。可以分析各寄存器当前的值. 值得注意的是,有时候终端会花屏,这时执行 refresh (或 Ctrl+L) 命令即可

ok, 上面都是一些基础操作。下面按照需求,一个个讲解.

问题 1: 如何设置条件断点?·

比如在 main 函数中,我们只在 a = 10 时,才停下。则可以通过 b 16 if a == 10 命令完成.

上面代码的含义时, 在代码 16 行, 如果 a==10,则停下,否则忽略。

问题 2: 卡在一直长循环,如何跳出这个循环?·

看下面这个代码:

1
2
3
4
5
1: for(int i = 0; i< 1000; i++)
2: {
3: // do something
4: }
5: int a = 10;

假设我们在第一行打了断点 b 1, 现在通过 n或i 进入了 for 循环,此时如何快速执行完这个循环呢?可以在第 5 行打断点 b 5, 然后执行 c continue 命令,就可以快速执行到第 5 行了.

问题 3: 如何删除断点或 disable 断点 ·

其实前文已经提到了,每个断点都有一个 id, 通过 info b 查看,然后执行 d breakpointid 即可.

问题 4: 如何快速清除一个函数中的所有断点 ·

使用 clear 命令,clear FUNCTION_NAME 即可.

问题 5: 如何保存一个程序的快照 ·

有时候我们在 debug 时,在到达某个 debug 点之前,要做很多重复的工作,这时,我们可以在这个点上生成一个快照,这次 debug 失败后,下次直接从这个快照中继续运行.

此时就可以用 checkpoint 来做.

比如上文的程序,我可以当 a = 10 时,生成一个快照,然后下次直接从 a=10 启动程序.

执行 c, run 完当前进程。会看到 context 自动切换到了下一个进程.

或者手动执行 restart checkpointid, 手动切换.

问题 6: 监听某个变量,变量发生变化时,自动打印该变量 ·

使用 watch 命令.

比如监听 i 变量,只要 i 发生了变化,就自动打印它.

问题 7: 每次停顿,都要打印一些想要监听的变量 ·

使用 display 命令.

display [var] 可以在每次程序 debug 中停顿时,打印你想知道的变量值.

如,我要监听 i 可以:

1
display i

可以看到,每次停下,i 的值都打印了出来.

问题 8: 如何切换 stack frame·

有时候,我们进入到某个函数后,想要重新查看另一个 stack frame 的局部变量。比如:

当前在 findSqure stack frame 中,想要切换到 main frame 中去.

可以通过 frame frameid 切换,这里是 frame 1 切换.

如何函数调用层次过深,可以使用 frame 命令,如果只是想查看两个较为临近的 frame, 使用 up num或down num 命令更合适.up 代表向上走多少个 frame, down 则是向下.

问题 9: 更换执行程序 ·

想要在 gdb 中直接加载另一个程序,使用 file [file_path] 命令即可.

问题 10: 打印某个内存区域中的值 ·

这个问题在 c 语言中相当场景,比如要打印数组的 value, 打印某个特定内存位置的值。都可以使用.

使用 x 命令解决这个问题,x 命令的格式如下:

1
x /[num][format][width] address
  • address 没什么可说的。就是你要查看的内存开始地址.
  • num: 打印多少个单元
  • format: 以什么格式打印,通过有 十六进制 (x), 十进制 (d), 八进制 (o), 字符 ©. 具体可通过 h x 查看
  • width: 一个单元的宽度,常见单位为 byte 8bit (b), half word 16bit (h), word 32bit (w), gaint 64bit (g). 同样,可通过 h x 查看.

下面就用一些例子来说明吧.

1
char buf[10] = "hello";

现在,要以 字符形式打印 buf. 应该怎么写命令?

1
x /10cb buf
  • 10: 代表 10 个单元
  • c: 代表以字符形式打印
  • b: 一个单元 1 个字节,(从语言中的 char 的长度为 1)

又如:

1
int arr[] = {1,2,3,4};
1
x /4dw arr
  • 4: 4 个单元
  • d: 十进制打印
  • w: 一个单元 32bit

ok, 到这里基本的调试操作应该都满足了,如果遇到什么不知道的,直接百度或者查看 help 吧。

2. 反汇编 ·

gdb 也是支持反汇编的,这也是 bomblab 必备的能力.

同样以下面代码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream> 
#include <stdlib.h>
#include <string.h>
using namespace std;

int findSquare(int a)
{
return a * a;
}

int main(int n, char** args)
{
for (int i = 1; i < n; i++)
{
int a = atoi(args[i]);
cout << findSquare(a) << endl;
}
return 0;
}

演示如何反汇编.

启动 gdb,

1
2
b main
r 1

通过 disassemble 命令进行反汇编.

如果指向反汇编时,添加源代码和行号,执行

1
disassemble /s

上面的命令用来临时看看汇编还可以,但是要跟踪还是得使用 layout 命令.

1
layout asm

问题 1: 为某个特定的指令地址加断点 ·

1
2
b *address
b *(function_name + offset)

如:

ok, gdb 的简单使用就到这里了.

还有个打断点的方式是,代码走到了指令的位置,直接输入 b, 就在当前位置打了断点.

问题 2: 导出汇编代码 ·

好吧,这个我并不知道如何用 gdb 实现, 改用 objdump -d 命令即可实现。

3. 输出 log·

如何你想要导出 gdb 的输出, 那么可以采用 log 功能。

  1. set logging [on/off] 设置 log 开关, 默认导出的 log 文件名为 gdb.txt
  2. set logging file file_name. 改变 log 文件名
  3. set logging overwrite [on/off] 默认 gdb 采用 append 方式,使用 overwrite 可以每次覆盖写。
  4. show logging 显示当前 logging 设置

4. 多线程调试 ·

多线程的调试一直都是难点,好在 gdb 对多线程的调试也有良好的支持。核心命令为 thread

给个最简单的测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* @file gdb 多线程调试
*/
#include <pthread.h>
#include <unistd.h>

long long a = 0;
long long b = 0;

void* func1(void *arg)
{
while (1)
{
a++;
sleep(1);
}
}

void* func2(void *arg)
{
while (1)
{
b++;
sleep(1);
}
}

int main()
{
pthread_t t1, t2;
pthread_create(&t1, NULL, func1, NULL);
pthread_create(&t2, NULL, func2, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}

现在编译后并采用 gdb 加载后。可以使用以下一些命令:

采用 info thread 查看当前有哪些线程:

可以看到现在启动了 3 个线程,线程的 id 分别 1 2 3. 另外注意有个叫 LWP 的东西。 LWP 全称为 Light weight process , 也就是线程的别称。 Frame 下面描述的时对应线程目前所处的函数栈。线程 id 前的 * 代表的时当前正在 debug 的线程。 注意当一个线程停止下来的时候,所有线程也会停止。不过当我们采用诸如 next, finish, continue 指令调试一个线程时,其余线程也会同时运行。

采用 thread [id] 切换线程。 如 thread 2,将会切换到线程 2.

通过 thread name xxx 为当前线程设置名字。

通过 thread find xxx 找到某个线程,支持正则表达式。

通过 thread apply [ID][all] COMMAND 来为某个或者所有线程,执行某个命令。如,针对上述程序,可以采用 thread apply all bt 查看所有线程的调用栈。

或者针对某个线程 watch. thread apply 2 watch a > 10

或者 break: break xxx thread 2 来设置断点。

最后, 有时候想要停止其他线程,只对当前线程进行调试, 可以采用 set shcduler-locking on 更多的参数,可以看下面的解释。

1
set scheduler-locking mode

Set the scheduler locking mode. If it is off, then there is no locking and any thread may run at any time. If on, then only the current thread may run when the inferior is resumed. The step mode optimizes for single-stepping. It stops other threads from “seizing the prompt” by preempting the current thread while you are stepping. Other threads will only rarely (or never) get a chance to run when you step. They are more likely to run when you next' over a function call, and they are completely free to run when you use commands like continue’, until', or finish’. However, unless another thread hits a breakpoint during its timeslice, they will never steal the GDB prompt away from the thread that you are debugging.

1
show scheduler-locking

Display the current scheduler locking mode.

5. 额外推荐 ·

cgdb: gdb 的包装, 默认打开了源代码试图,而且采用了 vim 模式查看源代码,熟悉 vim 和 gdb 的可以试试。

gdbgui: 这个还不错,采用 browser 进行调试,比只使用 gdb 还是好多了.

6. 参考 ·

文章对你有帮助?打赏一下作者吧