前言

GDB 全称 GNU Project debugger,是一个通用的 C / C++ 程序调试器,可以用来深入分析程序的运行过程,或者排查程序崩溃的原因。

GDB 主要有以下几个功能:

  1. 运行程序,随心所欲地查看程序内部状态 (如变量值、寄存器值)、控制程序的行为 (如逐行执行、反向执行等)
  2. 使程序在特定位置中断,或者满足条件时才中断
  3. 当程序崩溃时,查看完整现场,分析发生了什么
  4. 改变程序状态 (如临时修改某个变量值),以测试程序在不同情况下的行为

在日常工作中,我经常会使用 GDB。比如线上发生 coredump,需要用 GDB 来排查;调试程序时,使用 GDB 打断点,逐行执行,效率远高于加 debug 日志。

GDB 和 Vim 一样,只需要学会几个简单的命令,就能解决大部分问题。但它们就像一把瑞士军刀,有丰富的功能和技巧,只有深入掌握,才能成为效率提升利器。

本文面向的读者是 C / C++ 程序员,主要内容包括 GDB 的基本命令、进阶用法和实践案例。目标是使读者掌握 GDB 的常见使用方法,满足日常开发所需。读者也可以将本文作为 GDB 命令的速查手册,随时查阅。

本文约定:

  • 代码格式:如果没有前缀,或者前缀是 $,表示在 shell 执行;如果前缀是 (gdb),表示在 GDB 内执行;(gdb) 命令后面的 // xxx 是注释内容,不包含在要执行的命令中。

〇、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 设置断点、逐行运行程序的示例。

  1. 编写 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;
    }
    
  2. 编译程序,添加 -g 选项,保留 debug info:

    $ g++ -g main.cpp -o example
    
  3. 进入 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...
    
  4. 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;
    
  5. 逐行执行程序,打印变量 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
    
  6. step 命令会进入函数,停在函数的第一行 (step into):

    (gdb) step    
    print_foo (v=21845) at main.cpp:5
    5       void print_foo(int v) {
    
  7. backtrack 命令可以查看当前程序的调用栈:

    (gdb) backtrace
    #0  print_foo (v=21845) at main.cpp:5
    #1  0x0000555555555245 in main () at main.cpp:15
    
  8. continue 命令会执行程序,直到遇到下一个断点。这里没有下一个断点了,整个程序正常退出:

    (gdb) continue
    Continuing.
    [Inferior 1 (process 1308) exited normally]
    

命令的简写形式

大部分 GDB 命令都有一个简写形式,一般是命令的首字母,比如:

  • backtracebt
  • breakb
  • continuec
  • nextn
  • infoi

某些命令有相同的前缀,只需要写出前几个能区分的字符,GDB 就可以识别:

(gdb) i w    // 无法判断
Ambiguous info command "w": w32, warranty, watchpoints, win.
(gdb) i wat  // 可以识别,等于 info watchpoints
No watchpoints.

此外,在 GDB 中如果什么都不输入,直接回车,会重复执行上一条命令。

命令的适用场景

当应用程序异常退出时,操作系统会生成一个 coredump 文件,记录了程序退出时的所有内存状态。GDB 可以读取这个文件,查看程序退出时的变量值或者寄存器值,但是无法执行程序。即只能使用静态命令,如 pbti

GDB 也可以直接加载一个二进制程序并执行。在这种情况下,GDB 不仅可以随时查看程序当前的变量值或其他内存状态,还可以控制程序的运行,如设置断点、单步执行、反向执行等。即不仅可以使用静态命令,还可以使用 rbc动态命令。

帮助和术语

在 GDB 内使用 apropos {keyword} 可以模糊查找某条命令:

image-20230102213313034

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

image-20230102213725055

此外,使用 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 , eh 的 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)
    

xp 的区别:

  • 传入一个数字,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 ADDRESSF / M / T 是可选的参数。

  • F:一个数字,表示输出几个内存单元,默认是 1
  • M:格式化说明符,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 functionclear filename:function
  • clear linenumclear filename:linenum
  • delete:删除所有断点,简写是 d

设置临时断点:tbreak。参数同 break,命中一次后就会自动删除。

GDB - Setting breakpoints

GDB - Deleting breakpoints

停用 / 启用断点

停用断点: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

如果想在变量被读取时中断,可以使用 rwatchawatch

  • rwatch:仅当变量被读取时终端
  • awatch:当变量被读取或写入时中断

查看所有 watchpoints:

(gdb) info watchpoints

禁用 / 删除 watchpoints 的命令同 break

GDB - Setting watchpoints

条件断点: 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")

GDB - Break conditions

断点命令列表: commands

可以通过 commands 命令给断点绑定一组自定义命令,当命中断点后会自动执行,如打印变量的值,或者设置另一个断点。

语法:先指定要绑定的断点编号,然后输入自定义命令,最后以 end 结束。例如:

(gdb) commands 1
(gdb) p foo
(gdb) end

断点编号可以通过 i bi 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

GDB - Breakpoint command lists

运行程序: 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
  • 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 文件:

gdb-dashboard 使用笔记:

  • 使用 -output 命令将某些组件在其他终端显示,比如终端 A 执行 gdb 命令,终端 B 显示断点、变量值、调用栈。在终端输入 tty 命令就可以查看当前终端的序号。
  • 介绍文章:https://zhuanlan.zhihu.com/p/435918702

加载插件: source

GDB 可以使用 Python API 来实现自定义脚本。脚本可以直接写在 ~/.gdbinit,或者写在一个单独的文件中,然后通过 source 命令加载。

网上有很多可用的插件,比如 STL views 提供了一些打印 STL 容器的命令。

三、实践案例

TODO 待补充

附录

学习资源

术语

栈帧

调用栈 (call stack) 被分成若干个栈帧 (stack frame),每个栈帧包括和一次函数调用相关的所有数据:函数的参数、函数的局部变量、以及函数的返回地址等。

程序启动时只有一个栈帧,即 main 函数,又称初始栈帧最外层栈帧。每次函数调用都会创建一个新的栈帧,每次函数返回时一个栈帧也会被弹出。当前执行的函数所对应的栈帧又称最内层栈帧

GDB 给每个栈帧分配了一个数字,最内层栈帧的编号是 0,外层栈帧依次加 1。可以通过 bt 命令展示所有栈帧,通过 f 命令加上编号进入到对应的栈帧。

GDB - Stack frames

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

GDB Coredumps

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 学习笔记