💻【Linux】GDB 入门笔记
- 前言
 - 〇、Hello, world
 - 一、基本命令
 - 二、进阶用法
 - 三、实践案例
 - 附录
 
前言
GDB 全称 GNU Project debugger,是一个通用的 C / C++ 程序调试器,可以用来深入分析程序的运行过程,或者排查程序崩溃的原因。
GDB 主要有以下几个功能:
- 运行程序,随心所欲地查看程序内部状态 (如变量值、寄存器值)、控制程序的行为 (如逐行执行、反向执行等)
 - 使程序在特定位置中断,或者满足条件时才中断
 - 当程序崩溃时,查看完整现场,分析发生了什么
 - 改变程序状态 (如临时修改某个变量值),以测试程序在不同情况下的行为
 
在日常工作中,我经常会使用 GDB。比如线上发生 coredump,需要用 GDB 来排查;调试程序时,使用 GDB 打断点,逐行执行,效率远高于加 debug 日志。
GDB 和 Vim 一样,只需要学会几个简单的命令,就能解决大部分问题。但它们就像一把瑞士军刀,有丰富的功能和技巧,只有深入掌握,才能成为效率提升利器。
本文面向的读者是 C / C++ 程序员,主要内容包括 GDB 的基本命令、进阶用法和实践案例。目标是使读者掌握 GDB 的常见使用方法,满足日常开发所需。读者也可以将本文作为 GDB 命令的速查手册,随时查阅。
本文约定:
- 代码格式:如果没有前缀,或者前缀是 
$,表示在 shell 执行;如果前缀是(gdb),表示在 GDB 内执行;(gdb)命令后面的// xxx是注释内容,不包含在要执行的命令中。 
- 环境要求:gcc / g++,gdb。推荐使用 docker 初始化。
 
〇、Hello, world
安装 GDB
本文在 Linux (CentOS) 环境下运行 GDB,读者也可以使用网页版 GDB。
Linux 系统可以使用包管理器安装:
$ sudo apt-get update
$ sudo apt-get install gdb
Mac 系统可以使用 brew 安装:
$ brew install gdb
Mac 还需要给 GDB 签名,参考 GDB Wiki,否则会有这样的报错:
Starting program: /x/y/foo
Unable to find Mach task port for process-id 28885: (os/kern) failure (0x5).
 (please check gdb is codesigned - see taskgated(8))
使用 GDB
下面是一个使用 GDB 设置断点、逐行运行程序的示例。
- 
    
编写 C++ 程序:
// main.cpp #include <iostream> using namespace std; void print_foo(int v) { int i = v + 5; i = i + 3; cout << "i == " << i << endl; } int main() { int a = 0; a += 1; a += 2; print_foo(a); return 0; } - 
    
编译程序,添加
-g选项,保留 debug info:$ g++ -g main.cpp -o example - 
    
进入 gdb,加载二进制程序,最后一行表示符号表加载成功:
$ 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()函数第一行设置一个断点,运行程序:(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)。(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 = 3 - 
    
step命令会进入函数,停在函数的第一行 (step into):(gdb) step print_foo (v=21845) at main.cpp:5 5 void print_foo(int v) { - 
    
backtrack命令可以查看当前程序的调用栈:(gdb) backtrace #0 print_foo (v=21845) at main.cpp:5 #1 0x0000555555555245 in main () at main.cpp:15 - 
    
continue命令会执行程序,直到遇到下一个断点。这里没有下一个断点了,整个程序正常退出:(gdb) continue Continuing. [Inferior 1 (process 1308) exited normally] 
命令的简写形式
大部分 GDB 命令都有一个简写形式,一般是命令的首字母,比如:
backtrace→btbreak→bcontinue→cnext→ninfo→i
某些命令有相同的前缀,只需要写出前几个能区分的字符,GDB 就可以识别:
(gdb) i w    // 无法判断
Ambiguous info command "w": w32, warranty, watchpoints, win.
(gdb) i wat  // 可以识别,等于 info watchpoints
No watchpoints.
此外,在 GDB 中如果什么都不输入,直接回车,会重复执行上一条命令。
命令的适用场景
当应用程序异常退出时,操作系统会生成一个 coredump 文件,记录了程序退出时的所有内存状态。GDB 可以读取这个文件,查看程序退出时的变量值或者寄存器值,但是无法执行程序。即只能使用静态命令,如 p、bt、i。
GDB 也可以直接加载一个二进制程序并执行。在这种情况下,GDB 不仅可以随时查看程序当前的变量值或其他内存状态,还可以控制程序的运行,如设置断点、单步执行、反向执行等。即不仅可以使用静态命令,还可以使用 r、b、c 等动态命令。
帮助和术语
在 GDB 内使用 apropos {keyword} 可以模糊查找某条命令:

使用 help {command} 可以查看某个具体命令的帮助文档:

此外,使用 GDB 最好了解一些计算机的基础知识:
- 操作系统:coredump、栈帧、线程等。
 - 组成原理:寄存器、汇编、ABI 等。
 
部分术语的说明详见附录。
一、基本命令
选择线程: t
info thread 可以查看当前进程的所有线程。示例程序是单线程的:
(gdb) info threads
  Id   Target Id            Frame 
* 1    process 1537 "example" main () at main.cpp:15
thread / t 可以查看当前位于哪个线程:
(gdb) t
[Current thread is 1 (process 3496)]
在多线程程序里,可以通过 t {id} 切换线程,每个线程有独立的调用栈。
查看堆栈: bt
backtrace / bt 可以查看调用栈。调用栈展示了从 main() 入口到当前断点或进程退出时刻的所有函数调用路径:
(gdb) bt
#0  0x0 in (unknown) at :0
#1  0x1a796e7c in foo() at main.cpp:13
#2  0x6259058 in bar() at main.cpp:17
#3  0x6bb7580 in main() at main.cpp:83
选择栈帧: f
每次函数调用,会创建一个独立的栈帧,对应上面的 #0、#1、#2。默认在 #0。
frame / f  可以跳转到指定栈帧:
(gdb) f 2
#2  bar() at main.cpp:17
17        int a = foo();
up / down 可以向上层或下层跳转,对应编号增大或减小。
打印变量: p
基本使用
print / p 可以打印一个变量的值,支持数字、字符串、结构体、指针等变量类型:
(gdb) p a // int a = 3;
$1 = 3
打印出来的值会存在名为 $1、$2、… 的变量里,后续可以直接复用:
(gdb) p $1 // 等价于 p a
$2 = 3
p 有一些可选参数:
-elements:限制字符串或者数组打印的元素数量-max-depth:限制嵌套结构体的最大打印层数- …,
help p查看所有参数 
💡  p 可以打印当前栈帧和全局作用域内的变量。如果打印变量时提示变量已经 optimized,可以尝试用 f 切换到更上层的栈帧。
打印指针
指针变量
p 后面跟一个指针类型的变量,打印的是指针的值,即指针所指向的地址:
(gdb) p b // int* b = &a;
$1 = (int *) 0x7ffd3dcfa27c
可以用解引用运算符,打印指针指向的值:
(gdb) p *b
$2 = 1
如果是字符串指针,p 会同时输出指针指向的地址和字符串的内容:
p str
$3 = (char*) 0x7ffc734ff250 "hello,world"
如果希望只打印地址,可以使用说明符 /a:
(gdb) p/a str
$4 = 0x7ffc734ff250
/a表示address,即把变量的值以地址的形式打印。
地址字面量
p 默认会把十六进制的字面量看成是数字,输出一个十进制的整数:
(gdb) p 0x7ffd3dcfa27c
$1 = 140725640471164
(gdb) p 140725640471164 == 0x7ffd3dcfa27c
$2 = true
如果想把数字解释为地址、打印地址上的内容,需要先指定变量类型,然后解引用:
(gdb) p *(int*)0x7ffd3dcfa27c
$3 = 1
更简单的语法是 {TYPE}ADDRESS:
(gdb) p {int}0x7ffd3dcfa27c
$4 = 1
也可以用 x 命令打印地址。
转换指针类型
指针的类型可以转换,以不同方式解释其指向的内存区域:
// char* c = "hello, world";
(gdb) p c
$1 = (char *) 0x7ffc734ff250 "hello, world";
(gdb) p *(int*)c
$2 = 1819043176
(gdb) p {int}c
$3 = 1819043176
打印内存可以发现,1819043176 就是把 h e l l 四个字符解释成了一个整数:
(gdb) x/w 0x7ffc734ff250    // 以 word 形式打印,4 个字节
0x7ffc734ff250:	1819043176  // 上述 4 个字符的 ASCII 码转成整数
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,会打印数组的所有元素:
// int array[] = {1, 2, 3, 4};
(gdb) p array
$1 = {1, 2, 3, 4}
也可以 p array[INDEX]@LEN,从某个下标开始打印指定的长度:
(gdb) p array[1]@[3] // array[1] 的类型是 int
$2 = {1, 2, 3}
但不能 p array@LEN,因为栈上数组 array 的类型是 int[4] 而不是 int:
(gdb) p array@3
$3 = {{1, 2, 3, 4}, {-693741568, 32764, 1033857024, -1536906435}, {0, 0, -793505661, 32580}}
堆上数组
如果 array 是堆上数组,可以 p *array@LEN:
// int* array = (int*)malloc(3 * sizeof(int));
(gdb) p *array@3 // *array 是数组的第一个元素,类型是 int
$1 = {1, 2, 3}
或者 p array[INDEX]@LEN,从某个下标开始打印:
(gdb) p array[1]@3 // array[1] 的类型是 int
$2 = {2, 3, 4}
但不能 p array ,因为堆上数组 array 的类型是 int* 指针,值是一个地址:
(gdb) p array
$3 = 0x55669a743eb0
也不能 p array@LEN,理由同上。array 是一个 int* 指针,保存在栈上,这里会输出栈上相邻内存的值,没有任何意义:
(gdb) p array@3
$4 = {0x55669a743eb0, 0x55669a255330, 0x200000001}
如果只有一个地址字面量,可以把它强制转换为指针类型,然后用同样的语法打印:
(gdb) p ((int*)0x55669a743eb0))[2]
$5 = 3
格式化输出
可以在 p 后面添加说明符 (specifier),把一个变量解释为给定的类型:
(gdb) p foo // int foo = 98;
$1 = 98
(gdb) p/c foo // 将 98 解释为字符
$2 = 98 'b'
所有说明符:
- 
    
p/a:将变量解释为指针 address,使用十六进制打印 - 
    
p/c:将变量解释为字符 char,打印为字符 - 
    
p/o:使用八进制打印变量 - 
    
p/x:使用十六进制打印变量 - 
    
p/u:将变量解释为无符号整数 unsigned,使用十进制打印 - 
    
p/s:将变量解释为字符串,打印输出 - 
    
help x查看全部:o(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) 
STL 容器
std::shared_ptr
直接打印:
// std::shared_ptr<int> ptr = std::make_shared<int>(1);
(gdb) p ptr
$1 = std::shared_ptr<int> (use count 1, weak count 0) = {
  get() = 0x5596169122f0}
(gdb) p *ptr
$2 = 1
或者根据上面 get() 方法给出的地址打印:
(gdb) p {int}0x5596169122f0
$3 = 1
或者根据 shard_ptr 内部的私有变量 _M_ptr 打印:
(gdb) p ptr._M_ptr
$4 = 0x5596169122f0
(gdb) p *(ptr._M_ptr)
$5 = 1
std::vector
直接打印:
// std::vector<int> vec = {1, 2, 3, 4};
(gdb) p vec
$1 = std::vector of length 4, capacity 4 = {1, 2, 3, 4}
vector 也有私有变量保存了数据的实际存储位置:
_M_impl._M_start:数组起始地址_M_impl._M_finish:数组结束地址 (数组最后一个元素的下一个)
可以根据这个指针打印:
(gdb) p {int}vec._M_impl._M_start
$2 = 1
(gdb) p {int}vec._M_impl._M_start@3
$3 = {1, 2, 3}
(gdb) p ({int}vec._M_impl._M_start)[2]
$4 = 3
std::string
直接打印:
(gdb) p str
$1 = "hello,world"
或者根据私有变量 _M_dataplus._M_p 打印,其类型是 char*:
(gdb) p str._M_dataplus._M_p
$2 = (std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::pointer) 0x7ffc734ff250 "hello,world"
使用插件 STL-Views
使用 GDB 直接打印 set、stack、map 等 STL 类型是十分困难的。GDB 支持使用 python 编写 printer。GDB 官网提供了现成的 STL 容器的 printer,安装十分容易,开箱即用。
先下载源代码到 home 目录,如果终端不支持科学上网,可以网页里打开后复制内容,然后在 vim 里粘贴源代码:
$ wget https://sourceware.org/gdb/wiki/STLSupport?action=AttachFile&do=get&target=stl-views-1.0.3.gdb -O ~/stl-views-1.0.3.gdb
进入 gdb,加载插件,查看帮助:
(gdb) source ~/stl-views-1.0.3.gdb
(gdb) help pset
(gdb) help pmap
使用:
(gdb) pset s
(gdb) pset s int
(gdb) pset s int 20
如果打印内容被省略
打印字符串的时候,如果有重复的字符,可能会被合并成一个:
(gdb) p "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
$1 = 'a' <repeats 30 times>
可以通过命令 set print repeats 0 设置为不合并:
(gdb) set print repeats 0
(gdb) p "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
$2 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
打印数组的时候,如果元素过多,中间的元素会被省略。可以通过以下设置为不省略:
set print elements 0
查看历史变量
通过 p 打印出来的值会存在名为 $1、$2、… 的变量里 (value history),后续可以直接复用:
(gdb) p a
$1 = 123
(gdb) p $1 // 等价于 p a
$2 = 123
一些特殊的变量:
$:最近打印的变量$$:$之前的变量,倒数第二个$$n:最后一个变量往前的第 n 个变量,比如$$0就是$,$$1就是$$
可以批量打印历史变量:
show values:打印最后 10 个历史变量show values +:打印刚才打印过的历史变量的后 10 个历史变量
打印内存: x
x 可以查看一个内存地址的值,以指定的格式打印。
(gdb) x/s 0x7ffc734ff250  // 以字符串形式打印
0x7ffc734ff250:	"hello,world"
x 支持的格式化说明符:
- 
    
x/c:将地址解释为字符 char,打印为字符 - 
    
x/o:使用八进制打印变量 - 
    
x/x:使用十六进制打印变量 - 
    
x/u:将地址解释为无符号整数 unsigned,使用十进制打印 - 
    
x/s:将地址解释为字符串 - 
    
help x查看全部:o(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会当作一个地址,输出对应内存区域的值。比如:(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会输出指针指向的内存区域的值:(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
(gdb) ptype foo
type = int
打印各种信息: i
info locals:打印当前栈帧的所有局部变量info args:打印所有函数参数info threads: 打印进程的线程信息info registers: 打印当前线程的寄存器信息info sharedlibrary:打印当前加载的动态连接库info proc mappings:打印地址空间中的内存 map,用来确定某个地址的类型help info:所有 info 支持的命令
存储变量 / 修改变量的值: set
set 可以保存一个变量 (convenience variables),方便后续使用:
(gdb) set $foo = *object_ptr
查看所有存储的变量:
(gdb) show convenience
(gdb) show conv  // 简写形式
set 命令也可以用于在运行时修改某个变量的值:
(gdb) set foo.bar = true
如果没有调试符号,上述命令将无法查找到变量的地址。可以手动修改变量所在的内存位置:
set (char)0x7e864a2b = 1
修改变量值的使用场景:
- 临时修复某个 bug,使程序可以继续运行
 - 给变量设置不同的值,测试不同的 case
 
断点调试: b
设置 / 清除断点
设置断点:break POINT,简写是 b
(gdb) b foo.cpp:14
设置断点的方式有多种:
- 在当前执行位置设断点:
b,没有任何参数 - 函数名:
b function - 文件名 + 函数名:
b filename:function - 行号:
b linenum,在当前文件设置断点 - 文件名 + 行号:
b filename:linenum,在特定文件设置断点 - 偏移量:
b +offset/b -offset,在当前栈帧执行位置的前后设置断点 - 给汇编命令打断点:略
 
删除断点:clear
(gdb) clear foo.cpp:14
clear 的语法和 break 相同,需要指定要删除的断点的位置:
clear:删除当前执行位置上的所有断点clear function、clear filename:functionclear linenum、clear filename:linenumdelete:删除所有断点,简写是d
设置临时断点:tbreak。参数同 break,命中一次后就会自动删除。
停用 / 启用断点
停用断点:disable
(gdb) disable      // 停用所有断点
(gdb) disable NUM  // 停用编号为 n 的断点
停用断点后,断点将暂时不被触发。可以通过 enable 命令启用断点,语法同 disable。
继续运行: cont
命中断点后程序会停止运行,此时可以输入 continue 命令,继续运行程序。简写是 cont。
查看所有断点:i b
(gdb) i b
(gdb) info breakpoints
这会以表格的形式展示断点编号、是否是临时断点、是否 enable、断点位置等信息。
在函数返回前中断
有时候希望在函数返回前中断,从而检查函数的返回值,或者检查函数是在哪一个 return 语句返回的。
有两种方式。一种是反向调试,先正向执行,直到函数返回,然后再反向执行,设置断点:
(gdb) record
(gdb) fin
(gdb) reverse-step
另一种方式更通用。所有的函数无论有多少条 return 语句,在编译成汇编指令后,一定是只有一条 retq 指令。因此可以在汇编指令里找到 retq 所在位置打断点:
int main() {
  return foo(0);
}
(gdb) disas foo  // 查看汇编
Dump of assembler code for function foo:
   0x0000000000400448 <+0>: push   %rbp
   0x0000000000400449 <+1>: mov    %rsp,%rbp
   ...
   0x0000000000400473 <+43>:    jmp    0x40047c <foo+52>
   0x0000000000400480 <+56>:    retq   // 这里就是函数的返回指令
End of assembler dump.
(gdb) b *0x0000000000400480  // 在 retq 指令打断点
Breakpoint 1 at 0x400480
(gdb) r  // 运行程序,直到命中断点
Breakpoint 1, 0x0000000000400480 in foo ()
(gdb) p var
$1 = 42
监控断点: watch
GDB 可以监控一个变量,直到它被修改时才触发断点:
(gdb) watch foo
(gdb) watch bar.var
如果想在变量被读取时中断,可以使用 rwatch 或 awatch:
rwatch:仅当变量被读取时终端awatch:当变量被读取或写入时中断
查看所有 watchpoints:
(gdb) info watchpoints
禁用 / 删除 watchpoints 的命令同 break。
条件断点: b ... if
常规断点 (breakpoints) 和监控断点 (watchpoints) 都可以绑定一个条件,只在满足条件时才触发断点。
“条件”是一个布尔表达式:
(gdb) b foo.cpp:123 if bar == 1
(gdb) b foo.cpp:123 if bar == 1 && foo < 2
如果要判断两个字符串是否相等,可以使用 gdb 的内置函数 $_streq:
(gdb) b foo.cpp:123 if $_streq(some_str, "hello_world")
断点命令列表: commands
可以通过 commands 命令给断点绑定一组自定义命令,当命中断点后会自动执行,如打印变量的值,或者设置另一个断点。
语法:先指定要绑定的断点编号,然后输入自定义命令,最后以 end 结束。例如:
(gdb) commands 1
(gdb) p foo
(gdb) end
断点编号可以通过 i b 或 i wat 获取。如果不给 commands 传入任何编号,则默认绑定到最近触发的断点上。
commands 的应用场景之一是收集信息。比如在某行代码后面插入一行 debug 日志,打印变量或调用栈。由于每次命中断点后,必须输入 cond 命令才会继续运行程序,因此可以在 end 前面加一个 cont 命令,这样程序便可以无需干预、自动运行:
(gdb) b foo.cpp:123
(gdb) commands
(gdb) p bar
(gdb) cont
(gdb) end
commands 的另一个应用场景是临时修复一个 bug,以便让程序正常运行。比如在某一行错误代码后面,给变量设置正确的值。同样要以 continue 命令结尾:
(gdb) b foo.cpp:123
(gdb) commands
(gdb) silent  // 这个命令后面的命令不会有任何输出
(gdb) set x = y + 4
(gdb) cont
(gdb) end
运行程序: 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 的所有输出打印到日志里,作进一步分析。
需要执行这两个命令:
(gdb) set logging file gdb.txt
(gdb) set logging on
copying output to gdb.txt
这样任何命令的输出便会写到 gdb.txt,前提是 shell 拥有该文件的写入权限。
配合以下命令,确保输出完整内容:
set print repeats 0       // 否则相同的连续字符会被合并
set print elements 0      // 否则过长的数组会被省略
set height 0              // 否则如果一页显示不完,会停下来要求 continue
set width 0  
二、进阶用法
配置文件: ~/.gdbinit
像 ~/.vimrc、~/.zshrc 一样,GDB 也有默认的配置文件 ~/.gdbinit。可以把一些常用的配置、插件、自定义命令放在 ~/.gdbinit。
Github 上有一些开箱即用的 ~/.gdbinit 文件:
- https://github.com/gdbinit/Gdbinit/blob/master/gdbinit
 - gdb-dashboard:可视化界面、丰富的功能
 - gef:可视化界面、丰富的功能
 - pwndbg
 
gdb-dashboard 使用笔记:
- 使用 
-output命令将某些组件在其他终端显示,比如终端 A 执行 gdb 命令,终端 B 显示断点、变量值、调用栈。在终端输入tty命令就可以查看当前终端的序号。 - 介绍文章:https://zhuanlan.zhihu.com/p/435918702
 
加载插件: source
GDB 可以使用 Python API 来实现自定义脚本。脚本可以直接写在 ~/.gdbinit,或者写在一个单独的文件中,然后通过 source 命令加载。
网上有很多可用的插件,比如 STL views 提供了一些打印 STL 容器的命令。
三、实践案例
TODO 待补充
附录
学习资源
- GDB 官网:https://sourceware.org/gdb/
 - Debugging with GDB
 - gdb debug full examples
 - 100个 GDB 小技巧
 - https://pernos.co:在线 GDB 平台
 
术语
栈帧
调用栈 (call stack) 被分成若干个栈帧 (stack frame),每个栈帧包括和一次函数调用相关的所有数据:函数的参数、函数的局部变量、以及函数的返回地址等。
程序启动时只有一个栈帧,即 main 函数,又称初始栈帧或最外层栈帧。每次函数调用都会创建一个新的栈帧,每次函数返回时一个栈帧也会被弹出。当前执行的函数所对应的栈帧又称最内层栈帧。
GDB 给每个栈帧分配了一个数字,最内层栈帧的编号是 0,外层栈帧依次加 1。可以通过 bt 命令展示所有栈帧,通过 f 命令加上编号进入到对应的栈帧。
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
 
Debug Info
Debug 是编译器生成的调试用的符号表,保留了源代码的信息,如标识符名称、可执行文件中第几条机器指令对应源代码的第几行等,但并不是把整个源文件嵌入到可执行文件中。
gcc 或 g++ 在编译时,可以通过 -g 选项生成 debug info。如果没有 debug info,GDB 就无法按源码行打断点、输出变量的值、或者展示 coredump 文件中的调用栈信息。
DWARF 是现在操作系统 debug info 的主要标准。Debug info 保存在程序 ELF 文件的 .debug_info 段中。
The GDB developer’s GNU Debugger tutorial, Part 2: All about debuginfo
📒 相关文章:💻【Linux】Vim 学习笔记
- 版权声明:本文采用知识共享 3.0 许可证 (保持署名-自由转载-非商用-非衍生)
 - 发表于 2023-01-06