GDB 是类 Unix 系统下的调试工具,可以用来深入分析程序的运行过程,或者排查程序崩溃的原因。
相比于在 IDE 中封装好图形化界面的调试工具,GDB 一般直接通过命令行操作,只需要一些简单的命令就可以完成大部分调试任务。
本文面向 Linux 下的 C/C++ 程序,介绍 GDB 的基本命令、进阶用法、工程实践及调试原理,作为速查手册以供工程实践过程中随时查阅。
功能介绍GDB主要有以下功能:
运行程序,可以按照自定义的要求随心所欲的运行程序,比如设定断点,单步执行,逐过程执行等等。
中断程序,可以在指定的地方中断程序的运行,然后检查此时程序中的变量的值。
改变程序,可以改变程序的执行状态,比如改变临时变量的值,测试程序在不同的条件下的执行情况。
崩溃分析,可以在程序崩溃时查看完整的调用栈,分析程序崩溃的原因。
在日常研发过程中的使用场景:
程序发生 coredump,需要用 GDB 来排查,找出程序崩溃的原因。
开发过程中调试程序,使用 GDB 来打断点,逐行执行,查看变量值,效率高于日志打印输出。
调试原理register寄存器 是 CPU 内部的一组存储单元。
在程序运行时,寄存器中会存储多种信息,如指令、数据、地址等,这些信息对于程序的执行和状态管理至关重要,GDB 可以查看和修改寄存器的值,进而控制程序的运行。
常见的寄存器包括:
程序计数器(PC):存储下一条要执行的指令的地址。指示 CPU 正在执行的指令,控制程序的执行流程。
基址寄存器(BP):存储栈帧的基地址。用于访问函数参数、局部变量、返回地址等,帮助管理堆栈帧。
堆栈指针(SP):存储栈顶的地址。用于管理函数调用和局部变量的存储。
通用寄存器(GP):存储临时数据。执行算术运算、逻辑运算、存储中间结果和临时变量等。
状态寄存器(SR):存储 CPU 的状态信息。包括条件码、中断使能、特权级别等,控制 CPU 的运行状态。
中断寄存器(IR):存储中断处理程序的地址。在发生中断时,指示 CPU 跳转到中断处理程序的地址。
…
ptraceGDB 是通过 ptrace 系统调用来实现对被调试程序的控制的,利用 ptrace 系统调用,在被调试程序和 GDB 之间建立跟踪关系。然后所有发送给被调试程序的信号(除SIGKILL)都会被 GDB 截获,GDB 根据截获的信号,查看被调试程序相应的内存地址,寄存器等信息,从而实现调试功能。
ptrace 允许一个进程控制另一个进程的执行,用于调试器跟踪进程的执行,可以实现断点、单步执行、查看寄存器等功能,函数原型如下:
123456789#include
request
description
PTRACE_TRACEME、PTRACE_ATTACH
建立进程间的追踪关系
PTRACE_PEEKTEXT、PTRACE_PEEKDATA、PTRACE_PEEKUSER
读取子进程内存/寄存器中保留的数据
PTRACE_POKETEXT、PTRACE_POKEDATA、PTRACE_POKEUSER
修改子进程内存/寄存器中保留的数据
PTRACE_CONT、PTRACE_SYSCALL、PTRACE_SINGLESTEP
控制被跟踪进程以不同方式继续执行
PTRACE_DETACH、PTRACE_KILL
终止进程间的跟踪关系
core dump当进程崩溃时,会向操作系统发送一个信号操作系统会将进程的内存映像和寄存器状态信息保存到核心转储文件中,叫做 core dump 文件,默认名称为 core(名字来源与一种很古老的磁芯存储器,用于存储程序的状态信息)。
默认情况下,Linux 系统不会生成 core dump 文件,可以通过 ulimit -c 命令查看 core dump 文件的限制,如果为 0 表示系统不会生成 core dump 文件,可以通过 ulimit -c unlimited 命令设置为无限制。
默认情况下,core dump 文件会生成在可执行文件运行命令的同一路径下,且所有核心转储文件的名称都为 core,新的 core 文件生成将覆盖原来的 core 文件。
可以通过修改 Linux 特有的 /proc/sys/kernel/core_pattern 文件所包含的格式化字符串来控制对系统上生成的所有核心转储文件的命名和存储路径,如:
12➜ sudo sysctl -w kernel.core_pattern=/corefile/core.%e.%p.%t➜ sudo sysctl -p
格式符
描述
%c
对核心文件大小的资源软限制(字节数)
%e
可执行文件名(不含路径前缀)
%g
遭转储进程的实际组 ID
%h
主机系统的名称
%p
遭转储进程的进程 ID
%s
导致进程终止的信号编号
%t
转储时间,始于 Epoch,以秒为单位
%u
遭转储进程的实际用户 ID
%%
单个%字符
核心转储文件是一个ELF格式的二进制文件,可能产生 core dump 的代码问题如下,使用 GDB 可以快速的定位以下问题:
Segmentation fault, 段错误
Null Pointer Dereference, 空指针解引用
Stack Overflow / Buffer Overflow, 栈溢出/缓冲区溢出
Use After Free, 释放后使用
Double Free, 重复释放
Out of Memory, 内存溢出
特定信号也会引发进程创建一个核心转储文件并终止运行,如下表所示。
名称
信号值
描述
名称
信号值
描述
SIGABRT
6
中止进程
SIGFPE
8
算术异常
SIGALRM
14
实时定时器过期
SIGHUP
1
挂起
SIGBUS
7
内存访问错误
SIGILL
4
非法指令
SIGCHLD
17
终止或者停止子进程
SIGINT
2
终端中断
SIGCONT
18
若停止则继续
SIGIO
29
I/O 时可能产生
SIGEMT
undef
硬件错误
SIGKILL
9
必杀(确保杀死)
SIGPIPE
13
管道断开
SIGPROF
27
性能分析定时器过期
SIGPWR
30
电量行将耗尽
SIGQUIT
3
终端退出
SIGSEGV
11
无效的内存引用
SIGSTKFLT
16
协处理器栈错误
SIGSTOP
19
确保停止
SIGSYS
31
无效的系统调用
SIGTERM
15
终止进程
SIGTRAP
5
跟踪/断点陷阱
SIGTSTP
20
终端停止
SIGTTIN
21
从终端读取
SIGTTOU
22
向终端写
SIGURG
23
套接字上的紧急数据
SIGUSR1
10
用户自定义信号 1
SIGUSR2
12
用户自定义信号 2
SIGVTALRM
26
虚拟定时器过期
SIGWINCH
28
终端窗口尺寸发生变化
SIGXCPU
24
突破对 CPU 时间的限制
SIGXFSZ
25
突破对文件大小的限制
debug infoGDB 调试程序时,需要程序的调试信息,编译时加上 -g 选项,这样生成的 ELF 格式的可执行文件中就会包含源代码的调试信息。
ELF 用于存储可执行文件、目标文件、共享库和核心转储文件,是 Linux 平台上通用的二进制文件格式,它包括可执行文件、可重定位的目标文件(包括.o和.a文件)、core文件和共享对象(.so文件)等。
调试信息以 DWARF 格式被保存在 ELF 格式文件的几个 section 中,如果没有调试信息,GDB 会提示 No symbol table is loaded,无法查看变量名、函数名等信息,也无法打断点或打印函数调用栈。
调试信息格式主要面向开发者用以指导如何生成调试信息以及如何使用调试信息,DWARF是类Unix操作系统下的调试信息格式。PDB是Windows平台调试信息的主要格式。
ELF 文件由多个 section 组成,每个 section 用于存储不同的信息,DWARF 调试信息根据描述对象的不同,存储到不同的 section,名称均以前缀.debug_开头,下面是一些常见的调试信息:
Section
Description
debug_info
存储核心DWARF数据,包含了描述变量、代码等的DIEs
.debug_abbrev
存储.debug_info中使用的缩写信息
.debug_arranges
存储一个加速访问的查询表,通过内存地址查询对应编译单元信息
.debug_frame
存储调用堆栈信息,包括函数调用、返回地址等
.debug_line
存储源代码行号信息
.debug_loc
存储变量的位置信息
.debug_pubnames
存储一个加速访问的查询表,通过名称查询全局对象和函数
.debug_pubtypes
存储一个加速访问的查询表,通过名称查询全局类型信息
.debug_ranges
存储一个加速访问的查询表,通过内存地址查询对应编译单元信息
.debug_str
存储字符串信息
.debug_types
存储类型信息
Ref:DWARF Debugging Information Format 1 2 3
可以使用readelf -S命令或者objdump -h命令查看 ELF 文件的 section 信息,如下以.debug_开头的 section 是 DWARF 调试信息:
12345678910111228 .debug_aranges 000002a0 0000000000000000 0000000000000000 0000303b 2**0 CONTENTS, READONLY, DEBUGGING, OCTETS29 .debug_info 000051d2 0000000000000000 0000000000000000 000032db 2**0 CONTENTS, READONLY, DEBUGGING, OCTETS30 .debug_abbrev 000009a8 0000000000000000 0000000000000000 000084ad 2**0 CONTENTS, READONLY, DEBUGGING, OCTETS31 .debug_line 00000a77 0000000000000000 0000000000000000 00008e55 2**0 CONTENTS, READONLY, DEBUGGING, OCTETS32 .debug_str 00003fea 0000000000000000 0000000000000000 000098cc 2**0 CONTENTS, READONLY, DEBUGGING, OCTETS33 .debug_ranges 00000290 0000000000000000 0000000000000000 0000d8b6 2**0 CONTENTS, READONLY, DEBUGGING, OCTETS
GDB就是根据这些调试信息从可执行程序中获取源代码的信息,然后进行调试的,关于调试器具体如何解析需要的信息,可参考这篇文章。
stack frame在程序执行过程中,现代计算机系统使用栈来管理函数调用的过程,调用栈(call stack)是函数调用时分配的栈空间,它被分成若干个栈帧(stack frame),每个栈帧对应一个函数调用和相关的所有数据,包括:
函数实参和局部变量:这些变量都是在调用函数时自动创建的,函数返回时将自动销毁这些变量(因为栈帧会被释放)。
函数调用的链接信息:每个函数都会用到一些 CPU 寄存器,比如程序计数器,其指向下一条将要执行的机器语言指令。每当一个函数调用另一函数时,会在被调用函数的栈帧中保存这些寄存器的副本,以便函数返回时能为函数调用者将寄存器恢复原状。
程序启动时只有一个栈帧,即 main 函数,又称初始栈帧或最外层栈帧。每次调用函数时,会在栈上新分配一帧,每当函数返回时,再从栈上将此帧移去。当前执行的函数所对应的栈帧又称最内层栈帧。
GDB 给每个栈帧分配了一个数字,最内层栈帧的编号是 0,外层栈帧依次加 1。可以通过 bt 命令展示所有栈帧,只有在当前栈帧中的变量才能被访问,如果要访问其他栈帧的变量,需要先切换到对应的栈帧,可以通过 f 命令加上编号进入到对应的栈帧。
application binary interface应用程序二进制接口(ABI)是一套规则,规定了二进制可执行文件在运行时应如何与某些服务(诸如内核或函数库所提供的服务)交换信息。ABI 特别规定了使用哪些寄存器和栈地址来交换信息以及所交换值的含义,一旦针对某个特定 ABI 进行了编译,其二进制可执行文件应能在 ABI 相同的任何系统上运行。
简单入门当应用程序异常退出时,操作系统会生成 coredump 文件,记录程序退出时的所有内存状态。GDB 可以读取这个文件,查看程序退出时的变量值或者寄存器值,但是无法执行程序。即只能使用静态命令,如 p、bt、i 打印发生异常时的信息。
GDB 也可以直接加载一个二进制程序并执行。在这种情况下,GDB 不仅可以随时查看程序当前的变量值或其他内存状态,还可以控制程序的运行,如设置断点、单步执行、反向执行等。即不仅可以使用静态命令,还可以使用 r、b、c 等动态命令。
下面是一个简单的示例,展示如何使用 GDB 调试一个程序。
编译程序现在有这么一个采用质数筛法求解小于 n 的所有质数个数的程序,代码如下:
12345678910111213141516171819202122232425262728293031#include
编译这个程序,生成可执行文件,同时加上 -g 选项,生成调试信息:
123456➜ gdb-demo lscount_primes.cpp➜ gdb-demo g++ count_primes.cpp -o count_primes_without_debuginfo.out➜ gdb-demo g++ -g count_primes.cpp -o count_primes_with_debuginfo.out ➜ gdb-demo lscount_primes.cpp count_primes_with_debuginfo.out count_primes_without_debuginfo.out
进入 GDB使用 GDB 打开一个二进制文件,可以直接输入 gdb 命令,然后在 GDB 中输入 file 命令加载二进制文件:
12345678910111213141516171819202122232425➜ gdb-demo gdb count_primes_without_debuginfo.out GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-gitCopyright (C) 2024 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later
如果二进制文件没有调试信息,GDB 会提示 No debugging symbols found in xxx,无法查看变量名、函数名以及backtrace等信息。
或者直接在命令行中指定二进制文件:
12345678910111213141516171819➜ gdb-demo gdb GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-gitCopyright (C) 2024 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later
如果二进制文件有调试信息,GDB 会自动加载调试信息,Reading symbols from xxx 表示调试信息加载成功。
调试程序在 GDB 中,使用 break 命令设置断点,然后使用 run 命令运行程序,程序会在断点处停下来,可以使用 next 命令单步执行,使用 step 命令进入函数内部,使用 print 命令打印变量的值,使用 backtrace 命令打印函数调用栈。
1234567891011121314151617181920>>> b count_primes.cpp:28Breakpoint 1 at 0x128b: file count_primes.cpp, line 28.>>> rStarting program: /home/xiejunjie/Work/PersonalCodes/gdb-demo/count_primes_with_debuginfo.out Breakpoint 1, main () at count_primes.cpp:2828 int res = s.countPrimes(n);>>> sSolution::countPrimes (this=0x7fffffffd62f, n=10) at count_primes.cpp:77 int countPrimes(int n) {>>> n 310 for (int i = 2; i < n; ++i) {>>> p isPrime$1 = std::vector of length 10, capacity 10 = {[0] = 1, [1] = 1, [2] = 1, [3] = 1, [4] = 1, [5] = 1, [6] = 1, [7] = 1, [8] = 1, [9] = 1}>>> bt#0 Solution::countPrimes (this=0x7fffffffd62f, n=10) at count_primes.cpp:10#1 0x000055555555529c in main () at count_primes.cpp:28>>> c4[Inferior 1 (process 75796) exited normally]>>> q
continue 命令继续执行程序,直到遇到下一个断点,这里没有下一个断点了,所以程序执行完毕,退出,quit 命令退出 GDB。
命令缩写
命令
简写
说明
run
r
运行程序
break
b
设置断点
next
n
单步执行,执行完当前函数,停在函数后下一行(step over)
step
s
单步执行,进入函数内部,停在函数内第一行(step into)
continue
c
继续执行程序,直到遇到下一个断点
p
打印变量的值,支持数字、字符串、结构体、指针、表达式等
backtrace
bt
打印函数调用栈,显示从程序开始执行到当前位置的函数调用关系
frame
f
切换栈帧,进入到指定的栈帧
list
l
显示源代码,可以指定行号或函数名
info
i
显示程序信息,如断点、栈帧、寄存器等
set
设置变量的值
watch
设置监视点,当变量的值发生变化时,停止程序执行
until
u
执行程序直到达到指定行
命令详解启动程序
命令
说明
gdb object
正常启动,加载可执行
gdb object core
对可执行 + core 文件进行调试
gdb object pid
对正在执行的进程进行调试
gdb
正常启动,启动后需要 file 命令手动加载
gdb -tui
启用 gdb 的文本界面(或 ctrl-x ctrl-a 更换 CLI/TUI)
帮助信息
命令
说明
help
列出命令分类
help running
查看某个类别的帮助信息
help run
查看命令 run 的帮助
help info
列出查看程序运行状态相关的命令
help info line
列出具体的一个运行状态命令的帮助
help show
列出 GDB 状态相关的命令
help show commands
列出 show 命令的帮助
断点设置
命令
说明
break main
对函数 main 设置一个断点,可简写为 b main
break 101
对源代码的行号设置断点,可简写为 b 101
break basic.c:101
对源代码和行号设置断点
break basic.c:foo
对源代码和函数名设置断点
break *0x00400448
对内存地址 0x00400448 设置断点
info breakpoints
列出当前的所有断点信息,可简写为 info break
delete 1
按编号删除一个断点
delete
删除所有断点
clear
删除在当前行的断点
clear function
删除函数断点
clear line
删除行号断点
clear basic.c:101
删除文件名和行号的断点
clear basic.c:main
删除文件名和函数名的断点
clear *0x00400448
删除内存地址的断点
disable 2
禁用某断点,但是不删除
enable 2
允许某个之前被禁用的断点,让它生效
rbreak {regexpr}
匹配正则的函数前断点,如 ex_* 将断点 ex_ 开头的函数
tbreak function
line
hbreak function
line
ignore {id} {count}
忽略某断点 N-1 次
condition {id} {expr}
条件断点,只有在条件生效时才发生
condition 2 i == 20
2号断点只有在 i == 20 条件为真时才生效
watch {expr}
对变量设置监视点
info watchpoints
显示所有观察点
catch exec
断点在exec事件,即子进程的入口地址
运行程序
命令
说明
run
运行程序
run {args}
以某参数运行程序
run < file
以某文件为标准输入运行程序
run < <(cmd)
以某命令的输出作为标准输入运行程序
run <<< $(cmd)
以某命令的输出作为标准输入运行程序
set args {args} …
设置运行的参数
show args
显示当前的运行参数
continue
继续运行,可简写为 c 或 cont
step
单步进入,碰到函数会进去(Step in)
step {count}
单步多少次
next
单步跳过,碰到函数不会进入(Step Over)
next {count}
单步多少次
finish
运行到当前函数结束(Step Out)
until
持续执行直到代码行号大于当前行号(跳出循环)
until {line}
持续执行直到执行到某行
CTRL+C
发送 SIGINT 信号,中断程序执行
attach {process-id}
链接上当前正在运行的进程,开始调试
detach
断开进程链接
kill
杀死当前运行的函数
栈帧操作
命令
说明
bt
打印 backtrace (命令 where 是 bt 的别名)
frame
显示当前运行的栈帧
up
向上移动栈帧(向着 main 函数)
down
向下移动栈帧(远离 main 函数)
info locals
打印帧内的相关变量
info args
打印函数的参数
代码查看
命令
说明
list 101
显示第 101 行周围 10 行代码
list 1,10
显示 1 到 10 行代码
list main
显示函数周围代码
list basic.c:main
显示另外一个源代码文件的函数周围代码
list -
重复之前 10 行代码
list *0x22e4
显示特定地址的代码
cd dir
切换当前目录
pwd
显示当前目录
search {regexpr}
向前进行正则搜索
reverse-search {regexp}
向后进行正则搜索
dir {dirname}
增加源代码搜索路径
dir
复位源代码搜索路径(清空)
show directories
显示源代码路径
浏览数据
命令
说明
print {expression}
打印表达式,并且增加到打印历史
print /x {expression}
十六进制输出,print 可以简写为 p
print array[i]@count
打印数组范围
print $
打印之前的变量
print *$->next
打印 list
print $1
输出打印历史里第一条
print ::gx
将变量可视范围(scope)设置为全局
print ‘basic.c’::gx
打印某源代码里的全局变量,(gdb 4.6)
print /x &main
打印函数地址
x *0x11223344
显示给定地址的内存数据
x /nfu {address}
打印内存数据,n 是多少个,f 是格式,u 是单位大小
x /10xb *0x11223344
按十六进制打印内存地址 0x11223344 处的十个字节
x/x &gx
按十六进制打印变量 gx,x 和斜杆后参数可以连写
x/4wx &main
按十六进制打印位于 main 函数开头的四个 long
x/gf &gd1
打印 double 类型
help x
查看关于 x 命令的帮助
info locals
打印本地局部变量
info functions {regexp}
打印函数名称
info variables {regexp}
打印全局变量名称
ptype name
查看类型定义,比如 ptype FILE,查看 FILE 结构体定义
whatis {expression}
查看表达式的类型
set var = {expression}
变量赋值
display {expression}
在单步指令后查看某表达式的值
undisplay
删除单步后对某些值的监控
info display
显示监视的表达式
show values
查看记录到打印历史中的变量的值 (gdb 4.0)
info history
查看打印历史的帮助 (gdb 3.5)
文件操作
命令
说明
file {object}
加载新的可执行文件供调试
file
放弃可执行和符号表信息
symbol-file {object}
仅加载符号表
exec-file {object}
指定用于调试的可执行文件(非符号表)
core-file {core}
加载 core 用于分析
信号控制
命令
说明
info signals
打印信号设置
handle {signo} {actions}
设置信号的调试行为
handle INT print
信号发生时打印信息
handle INT noprint
信号发生时不打印信息
handle INT stop
信号发生时中止被调试程序
handle INT nostop
信号发生时不中止被调试程序
handle INT pass
调试器接获信号,不让程序知道
handle INT nopass
调试器不接获信号
signal signo
继续并将信号转移给程序
signal 0
继续但不把信号给程序
线程调试
命令
说明
info threads
查看当前线程和 id
thread {id}
切换当前调试线程为指定 id 的线程
break {line} thread all
所有线程在指定行号处设置断点
thread apply {id..} cmd
指定多个线程共同执行 gdb 命令
thread apply all cmd
所有线程共同执行 gdb 命令
set schedule-locking ?
调试一个线程时,其他线程是否执行,off
set non-stop on/off
调试一个线程时,其他线程是否运行
set pagination on/off
调试一个线程时,分页是否停止
set target-async on/off
同步或者异步调试,是否等待线程中止的信息
进程调试
命令
说明
info inferiors
查看当前进程和 id
inferior {id}
切换某个进程
kill inferior {id…}
杀死某个进程
set detach-on-fork on/off
设置当进程调用 fork 时 gdb 是否同时调试父子进程
set follow-fork-mode parent/child
设置当进程调用 fork 时是否进入子进程
汇编调试
命令
说明
info registers
打印普通寄存器
info all-registers
打印所有寄存器
print/x $pc
打印单个寄存器
stepi
指令级别单步进入,可以简写为 si
nexti
指令级别单步跳过,可以简写为 ni
display/i $pc
监控寄存器(每条单步完以后会自动打印值)
x/x &gx
十六进制打印变量
info line 22
打印行号为 22 的内存地址信息
info line *0x2c4e
打印给定内存地址对应的源代码和行号信息
disassemble {addr}
对地址进行反汇编,比如 disassemble 0x2c4e
历史信息
命令
说明
show commands
显示历史命令 (gdb 4.0)
info editing
显示历史命令 (gdb 3.5)
ESC-CTRL-J
切换到 Vi 命令行编辑模式
set history expansion on
允许类 c-shell 的历史
break class::member
在类成员处设置断点
list class:member
显示类成员代码
ptype class
查看类包含的成员
print *this
查看 this 指针
其他命令
命令
说明
define command … end
定义用户命令
{return}
直接按回车执行上一条指令
shell {command} [args]
执行 shell 命令
source {file}
从文件加载 gdb 命令
quit
退出 gdb
进阶用法GitHub 上有一些很好的插件和工具,可以帮助我们更好地使用 GDB,提高调试效率。
gdb-dashboard:可视化的调试信息仪表板拓展,帮助用户更直观地查看寄存器、堆栈、内存等信息。
GdbInit: 允许用户通过 .gdbinit 文件自动加载常用命令和配置,简化 GDB 的使用。
pwndbg:用于 CTF 和二进制安全研究的 GDB 插件,提供诸如反汇编、堆栈分析、ROP 链生成等功能。
gef:提供多种增强功能,如可视化堆栈、寄存器和内存信息,特别是在安全研究和逆向工程中常用。
参考资料
GDB Documentation
Debugging with GDB
GDB Debugging Full Example
100 GDB Tips
Online GDB
GDB UIs
imageslr’s GDB Tutorial
GDB Cheatsheet
Build your own debugger