💻【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
→bt
break
→b
continue
→c
next
→n
info
→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:function
clear linenum
、clear filename:linenum
delete
:删除所有断点,简写是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