GDB 全称 GNU Project debugger,是一个通用的 C / C++ 程序调试器,可以用来深入分析程序的运行过程,或者排查程序崩溃的原因。本文面向的读者是 C / C++ 程序员,主要内容包括 GDB 的基本命令、进阶用法和实践案例。目标是使读者掌握 GDB 的常见使用方法,满足日常开发所需。
GDB 主要有以下几个功能:
- 运行程序,随心所欲地查看程序内部状态 (如变量值、寄存器值)、控制程序的行为 (如逐行执行、反向执行等)
- 使程序在特定位置中断,或者满足条件时才中断
- 当程序崩溃时,查看完整现场,分析发生了什么
- 改变程序状态 (如临时修改某个变量值),以测试程序在不同情况下的行为
GDB 和 Vim 一样,只需要学会几个简单的命令,就能解决大部分问题。但它们就像一把瑞士军刀,有丰富的功能和技巧,只有深入掌握,才能成为效率提升利器。
本文面向的读者是 C / C++ 程序员,主要内容包括 GDB 的基本命令、进阶用法和实践案例。目标是使读者掌握 GDB 的常见使用方法,满足日常开发所需。读者也可以将本文作为 GDB 命令的速查手册,随时查阅。
Hello World
安装GDB
本文在 Linux (CentOS) 环境下运行 GDB,读者也可以使用网页版 GDB。
Linux 系统可以使用包管理器安装:
1 | sudo apt-get update |
使用 GDB
下面是一个使用 GDB 设置断点、逐行运行程序的示例。
- 编写 C++ 程序:
1 | // main.cpp |
编译程序,添加
-g
选项,保留 debug info:1
$ g++ -g main.cpp -o example
进入 gdb,加载二进制程序,最后一行表示符号表加载成功:
1
2
3
4
5
6
7
8$ gdb example
GNU gdb (GDB) 12.1
Copyright ...
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from example...在
main()
函数第一行设置一个断点,运行程序:1
2
3
4
5
6(gdb) b main.cpp:12
Breakpoint 1 at 0x55555555522c: file main.cpp, line 12.
(gdb) r
Starting program: /home/a.out
Breakpoint 1, main () at main.cpp:12
12 int a = 0;逐行执行程序,打印变量
a
的值:next
命令输出的是下一行要执行的代码。如果下一行是函数,next
命令会执行完整个函数,停在函数的下一行 (step over)。1
2
3
4
5
6
7
8
9
10(gdb) next
13 a += 1;
(gdb) p a
$3 = 1
(gdb) next
14 a += 2;
(gdb) next
15 print_foo(a);
(gdb) p a
$4 = 3step
命令会进入函数,停在函数的第一行 (step into):1
2
3(gdb) step
print_foo (v=21845) at main.cpp:5
5 void print_foo(int v) {backtrack
命令可以查看当前程序的调用栈:1
2
3(gdb) backtrace
#0 print_foo (v=21845) at main.cpp:5
#1 0x0000555555555245 in main () at main.cpp:15continue
命令会执行程序,直到遇到下一个断点。这里没有下一个断点了,整个程序正常退出:1
2
3(gdb) continue
Continuing.
[Inferior 1 (process 1308) exited normally]
命令的简写形式
大部分 GDB 命令都有一个简写形式,一般是命令的首字母,比如:
backtrace
→bt
break
→b
continue
→c
next
→n
info
→i
某些命令有相同的前缀,只需要写出前几个能区分的字符,GDB 就可以识别:
1 | (gdb) i w // 无法判断 |
此外,在 GDB 中如果什么都不输入,直接回车,会重复执行上一条命令。
命令的适用场景
当应用程序异常退出时,操作系统会生成一个 coredump
文件,记录了程序退出时的所有内存状态。GDB 可以读取这个文件,查看程序退出时的变量值或者寄存器值,但是无法执行程序。即只能使用静态命令,如 p
、bt
、i
。
Core Dump
当进程崩溃时,操作系统会把进程当前的所有内存和寄存器状态信息保存到 core dump 文件中。Core dump file 是一个二进制文件,需要配合 debug info 来赋予其含义。GDB 可以读取 core dump 文件,协助分析进程崩溃的瞬间发生了什么。
可能会产生 core dump 文件的场景:
- 段错误 Segmentation Fault
- Null Pointer Dereference (NPD)
- Stack Overflow / Buffer Overflow
- Use After Free (UAF)
- Double Free
- Out Of Memory (OOM)
- 其他一些会引起 core dump 的 signal
Core Dump Settings
为了使能 core dump,首先要检查最大的 core dump 限制
1 $ ulimit -c如果结果是 0,那么需要设置限制为 unlimited
1 $ ulimit -c unlimited现在,将生成一个 core dump 并将其放置在 /proc/sys/kernel/core_pattern 指定的位置 ,通过下面的命令查看 core dump 生成的位置:
1 $ cat /proc/sys/kernel/core_pattern大部分系统的默认输出是:
1 core这意味着任何核心转储都将放置在当前目录中名为 core 的文件中。
您可以使用以下方式更改此位置:
1
2 $ echo "<desired-file-path>/<desired-file-name>" > /proc/sys/kernel/core_pattern通过下面的命令可以恢复 core dump 生成时的程序运行状态:
1 $ gdb <binary-file> <core-dump-file>此时,这些命令: where, up, down, print, info locals, info args, info registers and list 通常非常有帮助
GDB 也可以直接加载一个二进制程序并执行。在这种情况下,GDB 不仅可以随时查看程序当前的变量值或其他内存状态,还可以控制程序的运行,如设置断点、单步执行、反向执行等。即不仅可以使用静态命令,还可以使用 r
、b
、c
等动态命令。
帮助和术语
在 GDB 内使用 apropos {keyword}
可以模糊查找某条命令:
使用 help {command}
可以查看某个具体命令的帮助文档:
基本命令
选择线程: t
info thread
可以查看当前进程的所有线程。示例程序是单线程的:
1 | (gdb) info threads |
thread
/ t
可以查看当前位于哪个线程:
1 | (gdb) t |
在多线程程序里,可以通过 t {id}
切换线程,每个线程有独立的调用栈。
查看堆栈: bt
backtrace
/ bt
可以查看调用栈。调用栈展示了从 main()
入口到当前断点或进程退出时刻的所有函数调用路径:
1 | (gdb) bt |
选择栈帧: f
每次函数调用,会创建一个独立的栈帧,对应上面的 #0
、#1
、#2
。默认在 #0
。
frame
/ f
可以跳转到指定栈帧:
1 | (gdb) f 2 |
up
/ down
可以向上层或下层跳转,对应编号增大或减小。
打印各种信息: i
info locals
:打印当前栈帧的所有局部变量info args
:打印所有函数参数info threads
: 打印进程的线程信息info registers
: 打印当前线程的寄存器信息info sharedlibrary
:打印当前加载的动态连接库info proc mappings
:打印地址空间中的内存 map,用来确定某个地址的类型help info
:所有 info 支持的命令
打印变量: p
基本使用
print
/ p
可以打印一个变量的值,支持数字、字符串、结构体、指针等变量类型:
1 | (gdb) p a // int a = 3; |
打印出来的值会存在名为 $1
、$2
、… 的变量里,后续可以直接复用:
1 | (gdb) p $1 // 等价于 p a |
p
有一些可选参数:
-elements
:限制字符串或者数组打印的元素数量-max-depth
:限制嵌套结构体的最大打印层数- …,
help p
查看所有参数
打印指针
指针变量
p
后面跟一个指针类型的变量,打印的是指针的值,即指针所指向的地址:
1 | (gdb) p b // int* b = &a; |
可以用解引用运算符,打印指针指向的值:
1 | (gdb) p *b |
如果是字符串指针,p
会同时输出指针指向的地址和字符串的内容:
1 | p str |
如果希望只打印地址,可以使用说明符 /a
:
1 | (gdb) p/a str |
/a
表示address
,即把变量的值以地址的形式打印。
地址字面量
p
默认会把十六进制的字面量看成是数字,输出一个十进制的整数:
1 | (gdb) p 0x7ffd3dcfa27c |
如果想把数字解释为地址、打印地址上的内容,需要先指定变量类型,然后解引用:
1 | (gdb) p *(int*)0x7ffd3dcfa27c |
更简单的语法是 {TYPE}ADDRESS:
1 | (gdb) p {int}0x7ffd3dcfa27c |
也可以用 x
命令打印地址。
转换指针类型
指针的类型可以转换,以不同方式解释其指向的内存区域:
1 | // char* c = "hello, world"; |
打印内存可以发现,1819043176
就是把 h e l l
四个字符解释成了一个整数:
1 | (gdb) x/w 0x7ffc734ff250 // 以 word 形式打印,4 个字节 |
1819043176
对应的十六进制是 0x6C6C6568
,恰好依次是 l
, l
, e
和 h
的 ASCII 码。
打印结构体的字段
如果指针 p
指向某个结构体,可以用 p ptr->field
打印字段的值。
在 GDB 里,.
和 ->
是一样的,所以无论 ptr
是否是指针,都可以用 p.field
打印字段的值。
打印数组
语法:p ELEMENT@LEN
。从 ELEMENT
的地址开始向后解释 LEN
大小的内存单元,内存单元的大小是 sizeof(T)
。
栈上数组
如果 array 是栈上数组,可以直接 p array
,会打印数组的所有元素:
1 | // int array[] = {1, 2, 3, 4}; |
也可以 p array[INDEX]@LEN
,从某个下标开始打印指定的长度:
1 | (gdb) p array[1]@[3] // array[1] 的类型是 int |
但不能 p array@LEN
,因为栈上数组 array 的类型是 int[4]
而不是 int
:
1 | (gdb) p array@3 |
堆上数组
如果 array 是堆上数组,可以 p *array@LEN
:
1 | // int* array = (int*)malloc(3 * sizeof(int)); |
或者 p array[INDEX]@LEN
,从某个下标开始打印:
1 | (gdb) p array[1]@3 // array[1] 的类型是 int |
但不能 p array
,因为堆上数组 array 的类型是 int*
指针,值是一个地址:
1 | (gdb) p array |
也不能 p array@LEN
,理由同上。array 是一个 int*
指针,保存在栈上,这里会输出栈上相邻内存的值,没有任何意义:
1 | (gdb) p array@3 |
如果只有一个地址字面量,可以把它强制转换为指针类型,然后用同样的语法打印:
1 | (gdb) p ((int*)0x55669a743eb0))[2] |
格式化输出
可以在 p
后面添加说明符 (specifier),把一个变量解释为给定的类型:
1 | (gdb) p foo // int foo = 98; |
所有说明符:
p/a
:将变量解释为指针 address,使用十六进制打印p/c
:将变量解释为字符 char,打印为字符p/o
:使用八进制打印变量p/x
:使用十六进制打印变量p/u
:将变量解释为无符号整数 unsigned,使用十进制打印p/s
:将变量解释为字符串,打印输出help x
查看全部:1
2
3o(octal), x(hex), d(decimal), u(unsigned decimal),
t(binary), f(float), a(address), i(instruction),
c(char), s(string) and z(hex, zero padded on the left)
查看历史变量
通过 p
打印出来的值会存在名为 $1
、$2
、… 的变量里 (value history),后续可以直接复用:
1 | (gdb) p a |
一些特殊的变量:
$
:最近打印的变量$$
:$
之前的变量,倒数第二个$$n
:最后一个变量往前的第 n 个变量,比如$$0
就是$
,$$1
就是$$
可以批量打印历史变量:
show values
:打印最后 10 个历史变量show values +
:打印刚才打印过的历史变量的后 10 个历史变量
打印内存: x
x
可以查看一个内存地址的值,以指定的格式打印。
1 | (gdb) x/s 0x7ffc734ff250 // 以字符串形式打印 |
x 支持的格式化说明符:
x/c
:将地址解释为字符 char,打印为字符x/o
:使用八进制打印变量x/x
:使用十六进制打印变量x/u
:将地址解释为无符号整数 unsigned,使用十进制打印x/s
:将地址解释为字符串help x
查看全部:1
2
3o(octal), x(hex), d(decimal), u(unsigned decimal),
t(binary), f(float), a(address), i(instruction),
c(char), s(string) and z(hex, zero padded on the left)
x
和 p
的区别:
传入一个数字,
p
会当作一个数字字面量,输出原始值的十进制;而x
会当作一个地址,输出对应内存区域的值。比如:1
2
3
4
5
6
7
8
9
10
11(gdb) p 0x10 // 字面量
$1 = 16 // 输出十进制值
(gdb) p/x 0x10 // 以十六进制形式输出
$2 = 0x10
(gdb) x/s 0x10 // 这个内存地址解释为字符串
0x10 "hello, world"
(gdb) x/c 0x10 // 把这个地址上的内容解释为单个字符
0x10: 'h'
(gdb) x/d 0x10 // 把这个地址上的内容解释为整数
0x10: 104传入一个指针,
p
会输出指针的值,即一个十六进制地址;而x
会输出指针指向的内存区域的值:1
2
3
4
5(gdb) p str_pointer;
$1 = 0x7ffc
(gdb) x/s 0x7ffc
0x7ffc "hello world"
x
的完整语法:x/FMT ADDRESS
,F
/ M
/ T
是可选的参数。
F
:一个数字,表示输出几个内存单元,默认是 1M
:格式化说明符,o
/x
/d
/u
/s
等T
:一个内存单元的字节数,默认是 4 个字节,可选的是 b(byte), h(halfword), w(word), g(giant, 8 bytes)ADDRESS
:一个内存地址,可以是一个字面量,也可以是一个指针类型的变量
例如,
x/3uh 0x1234
表示从内存地址 0x1234 开始,以双字节为单位,输出 3 个无符号整数。
打印类型: ptype
1 | (gdb) ptype foo |
存储变量 / 修改变量的值: set
set
可以保存一个变量 ,方便后续使用:
1 | (gdb) set $foo = *object_ptr |
查看所有存储的变量:
1 | (gdb) show convenience |
set
命令也可以用于在运行时修改某个变量的值:
1 | (gdb) set foo.bar = true |
如果没有调试符号,上述命令将无法查找到变量的地址。可以手动修改变量所在的内存位置:
1 | set (char)0x7e864a2b = 1 |
修改变量值的使用场景:
- 临时修复某个 bug,使程序可以继续运行
- 给变量设置不同的值,测试不同的 case
断点调试: b
设置 / 清除断点
设置断点:break POINT
,简写是 b
1 | (gdb) b foo.cpp:14 |
设置断点的方式有多种:
- 在当前执行位置设断点:
b
,没有任何参数 - 函数名:
b function
- 文件名 + 函数名:
b filename:function
- 行号:
b linenum
,在当前文件设置断点 - 文件名 + 行号:
b filename:linenum
,在特定文件设置断点 - 偏移量:
b +offset
/b -offset
,在当前栈帧执行位置的前后设置断点 - 给汇编命令打断点:略
删除断点:clear
1 | (gdb) clear foo.cpp:14 |
clear
的语法和 break
相同,需要指定要删除的断点的位置:
clear
:删除当前执行位置上的所有断点clear function
、clear filename:function
clear linenum
、clear filename:linenum
delete
:删除所有断点,简写是d
设置临时断点:tbreak
。参数同 break
,命中一次后就会自动删除。
停用 / 启用断点
停用断点:disable
1 | (gdb) disable // 停用所有断点 |
停用断点后,断点将暂时不被触发。可以通过 enable
命令启用断点,语法同 disable
。
继续运行: cont
命中断点后程序会停止运行,此时可以输入 continue
命令,继续运行程序。简写是 cont
。
查看所有断点:i b
1 | (gdb) i b |
这会以表格的形式展示断点编号、是否是临时断点、是否 enable、断点位置等信息。
在函数返回前中断
有时候希望在函数返回前中断,从而检查函数的返回值,或者检查函数是在哪一个 return
语句返回的。
有两种方式。一种是反向调试,先正向执行,直到函数返回,然后再反向执行,设置断点:
1 | (gdb) record |
另一种方式更通用。所有的函数无论有多少条 return
语句,在编译成汇编指令后,一定是只有一条 retq
指令。因此可以在汇编指令里找到 retq
所在位置打断点:
1 | int main() { |
监控断点: watch
GDB 可以监控一个变量,直到它被修改时才触发断点:
1 | (gdb) watch foo |
1 | (gdb) watch bar.var |
如果想在变量被读取时中断,可以使用 rwatch
或 awatch
:
rwatch
:仅当变量被读取时终端awatch
:当变量被读取或写入时中断
查看所有 watchpoints:
1 | (gdb) info watchpoints |
禁用 / 删除 watchpoints 的命令同 break
。
条件断点: b ... if
常规断点 (breakpoints) 和监控断点 (watchpoints) 都可以绑定一个条件,只在满足条件时才触发断点。
“条件”是一个布尔表达式:
1 | (gdb) b foo.cpp:123 if bar == 1 |
如果要判断两个字符串是否相等,可以使用 gdb 的内置函数 $_streq
:
1 | (gdb) b foo.cpp:123 if $_streq(some_str, "hello_world") |
断点命令列表: commands
可以通过 commands
命令给断点绑定一组自定义命令,当命中断点后会自动执行,如打印变量的值,或者设置另一个断点。
语法:先指定要绑定的断点编号,然后输入自定义命令,最后以 end
结束。例如:
1 | (gdb) commands 1 |
断点编号可以通过 i b
或 i wat
获取。如果不给 commands
传入任何编号,则默认绑定到最近触发的断点上。
commands
的应用场景之一是收集信息。比如在某行代码后面插入一行 debug 日志,打印变量或调用栈。由于每次命中断点后,必须输入 cond
命令才会继续运行程序,因此可以在 end
前面加一个 cont
命令,这样程序便可以无需干预、自动运行:
1 | (gdb) b foo.cpp:123 |
commands
的另一个应用场景是临时修复一个 bug,以便让程序正常运行。比如在某一行错误代码后面,给变量设置正确的值。同样要以 continue
命令结尾:
1 | (gdb) b foo.cpp:123 |
运行程序: n
/ s
/ c
/ fin
/ u
run
/r
:运行程序,直到遇到第一个断点或者运行结束start
:启动程序,临时停在 main() 的第一行next
/n
:逐行执行,如果某一行是函数,不会进入到函数里,而是会执行完整个函数 (step over)step
/s
:逐行执行,如果某一行是函数,会进入到函数的第一行 (step into)continue
/c
:从断点位置继续执行,直到遇到下一个断点或者运行结束finish
/fin
:执行到函数结束,停在 return 后的下一条语句until
/u
:- 不加任何参数:执行直到当前语句结束,比如在 for loop 里
until
会跳到 for 循环体的下一行 - 加参数:执行直到特定位置,参数的语法同
break
,等价于tbreak
+continue
- 不加任何参数:执行直到当前语句结束,比如在 for loop 里
quit
/q
:退出 GDB
直接回车会重复上一次执行的命令,所以在单步跟踪的时候,无论是 s
还是 n 都可以连续敲回车继续执行。
输出日志: set logging
可以把 GDB 的所有输出打印到日志里,作进一步分析。
需要执行这两个命令:
1 | (gdb) set logging file gdb.txt |
这样任何命令的输出便会写到 gdb.txt
,前提是 shell 拥有该文件的写入权限。
配合以下命令,确保输出完整内容:
1 | set print repeats 0 // 否则相同的连续字符会被合并 |
总结
掌握上面这些命令,就能够满足日常的使用了,在代码调试的过程一点要习惯使用 GDB 而不是打印输出,因为 GDB 能够极大的提高效率,更容易发现隐藏的 bug。