一直用惯了 IDE 集成的 debug 工具,忽略了 gdb 这个命令行 debug 工具。而最近在做 csapp 的 bomblab, 就不得不来学习它了。所以特此记录.
1. 基本使用 ·
考虑以下我们在 IDE 中要进行 debug 一般需要哪些功能?
- 设置断点 (包含条件断点)
- 开启 debug
- step in, step over, continue
- 观察某些值的变化,打印数组 value, 打印某个地址 value
- 函数调用 stack, 切换 stack
- 临时更改某个变量,参数的值
下面讲解如何用 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 | 跟踪查看那某个变量,每次停下来都显示其值 |
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 | // gfg.cpp |
通过 g++ 进行编译,注意编译参数需要添加 "-g":
1 | g++ -g -o gfg gfg.cpp |
执行:
1 | gdb gfg |
开启 gdb:
首先介绍的是 break (b) 命令,这是用来设置断点的命令,它的使用格式如下:
1 | b |
使用: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 | 1: for(int i = 0; i< 1000; i++) |
假设我们在第一行打了断点 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 |
|
演示如何反汇编.
启动 gdb,
1 | b main |
通过 disassemble 命令进行反汇编.
如果指向反汇编时,添加源代码和行号,执行
1 | disassemble /s |
上面的命令用来临时看看汇编还可以,但是要跟踪还是得使用 layout 命令.
1 | layout asm |
问题 1: 为某个特定的指令地址加断点 ·
1 | b *address |
如:
ok, gdb 的简单使用就到这里了.
还有个打断点的方式是,代码走到了指令的位置,直接输入 b, 就在当前位置打了断点.
问题 2: 导出汇编代码 ·
好吧,这个我并不知道如何用 gdb 实现, 改用 objdump -d 命令即可实现。
3. 输出 log·
如何你想要导出 gdb 的输出, 那么可以采用 log 功能。
- set logging [on/off] 设置 log 开关, 默认导出的 log 文件名为 gdb.txt
- set logging file file_name. 改变 log 文件名
- set logging overwrite [on/off] 默认 gdb 采用 append 方式,使用 overwrite 可以每次覆盖写。
- show logging 显示当前 logging 设置
4. 多线程调试 ·
多线程的调试一直都是难点,好在 gdb 对多线程的调试也有良好的支持。核心命令为 thread
给个最简单的测试程序:
1 | /** |
现在编译后并采用 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 modeSet the scheduler locking mode. If it is
off
, then there is no locking and any thread may run at any time. Ifon
, then only the current thread may run when the inferior is resumed. Thestep
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 younext' 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-lockingDisplay the current scheduler locking mode.
5. 额外推荐 ·
cgdb: gdb 的包装, 默认打开了源代码试图,而且采用了 vim 模式查看源代码,熟悉 vim 和 gdb 的可以试试。
gdbgui: 这个还不错,采用 browser 进行调试,比只使用 gdb 还是好多了.