- 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 跳转到中断处理程序的地址。
- …
ptrace
GDB 是通过 ptrace
系统调用来实现对被调试程序的控制的,利用 ptrace
系统调用,在被调试程序和 GDB 之间建立跟踪关系。然后所有发送给被调试程序的信号(除SIGKILL)都会被 GDB 截获,GDB 根据截获的信号,查看被调试程序相应的内存地址,寄存器等信息,从而实现调试功能。
ptrace 允许一个进程控制另一个进程的执行,用于调试器跟踪进程的执行,可以实现断点、单步执行、查看寄存器等功能,函数原型如下:
1 |
|
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
文件所包含的格式化字符串来控制对系统上生成的所有核心转储文件的命名和存储路径,如:
1 | ➜ sudo sysctl -w kernel.core_pattern=/corefile/core.%e.%p.%t |
格式符 | 描述 |
---|---|
%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 info
GDB 调试程序时,需要程序的调试信息,编译时加上 -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 | 存储类型信息 |
可以使用readelf -S
命令或者objdump -h
命令查看 ELF 文件的 section 信息,如下以.debug_
开头的 section 是 DWARF 调试信息:
1 | 28 .debug_aranges 000002a0 0000000000000000 0000000000000000 0000303b 2**0 |
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 的所有质数个数的程序,代码如下:
1 |
|
编译这个程序,生成可执行文件,同时加上 -g
选项,生成调试信息:
1 | ➜ gdb-demo ls |
进入 GDB
使用 GDB 打开一个二进制文件,可以直接输入 gdb
命令,然后在 GDB 中输入 file
命令加载二进制文件:
1 | ➜ gdb-demo gdb count_primes_without_debuginfo.out |
如果二进制文件没有调试信息,GDB 会提示 No debugging symbols found in xxx
,无法查看变量名、函数名以及backtrace等信息。
或者直接在命令行中指定二进制文件:
1 | ➜ gdb-demo gdb |
如果二进制文件有调试信息,GDB 会自动加载调试信息,Reading symbols from xxx
表示调试信息加载成功。
调试程序
在 GDB 中,使用 break
命令设置断点,然后使用 run
命令运行程序,程序会在断点处停下来,可以使用 next
命令单步执行,使用 step
命令进入函数内部,使用 print
命令打印变量的值,使用 backtrace
命令打印函数调用栈。
1 | >>> b count_primes.cpp:28 |
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:提供多种增强功能,如可视化堆栈、寄存器和内存信息,特别是在安全研究和逆向工程中常用。