跳到主要内容

GDB 调试工具

1. GDB 概述

GDB(GNU Debugger)是一个强大的调试工具,广泛用于调试C、C++、Fortran等编程语言编写的程序。GDB可以在程序运行时执行控制,帮助开发人员诊断程序中的错误。

1.1 GDB的历史和发展

GDB 是由 Free Software Foundation 开发和维护的,最早由 Richard Stallman 和 Roland H. Pesch 在 1986 年开始设计。如今,GDB已经成为Linux及其他操作系统上最广泛使用的调试工具。

1.2 GDB应用场景

  • 软件开发:用于程序调试和性能优化
  • 嵌入式开发:调试在嵌入式系统上运行的应用程序
  • 远程调试:通过网络调试远程机器上的程序
  • 多线程调试:在多线程环境下调试并发问题

2. GDB安装

2.1 使用包管理器安装

yum install gdb

2.2 源码编译安装

(TODO:添加源码编译详细步骤)

3. GDB 基本功能

3.1 启动GDB

# 启动 GDB 调试指定程序
$ gdb <your_program>
# 启动程序执行 `run`后面的参数会像进程正常执行一样传递给进程。通常,我们会在设置好断点之后。执行此命令
(gdb) run

3.2 调试带参数的程序

# 方法1:直接传参
(gdb) run arg1 arg2 arg3
# 方法2:预设参数
(gdb) set args arg1 arg2 arg3
(gdb) run
# 方法3:修改参数
(gdb) set args new_arg1 new_arg2

3.3 调试运行中的进程

# 可以直接-p 指定pid
$ gdb -p pid
# 也可以启动gdb 后,attach pid
$ gdb -q
(gdb) <pid>

3.4 基本调试命令

命令功能描述
b/break设置断点
r/run开始程序运行
start在main函数开始前停止
c/continue继续执行直到下一个断点
n/next单步执行(不进入函数)
s/step单步执行(进入函数)
p/print打印变量的值
l/list查看源代码
bt/backtrace显示调用栈
until location执行到指定位置
fini/finish执行完成当前函数
q/quit退出gdb

3.5 断点管理

通过 break 命令在特定位置设置断点(如函数或代码行)。

(gdb) break func_name                 # 设置函数断点
(gdb) break file.c:line # 设置文件行断点
(gdb) break file:line if condition # 条件断点
(gdb) info breakpoints # 查看断点信息
(gdb) delete <num> # 删除指定断点

3.6 变量与内存操作

(gdb) print var                      # 打印变量
(gdb) info locals # 显示局部变量
(gdb) info args # 显示函数参数
(gdb) set var = value # 修改变量值
(gdb) x/10x <addr> # 查看内存内容(16进制)

3.7 寄存器操作

(gdb) info registers                 # 查看所有寄存器的值
(gdb) print $eax # 查看特定寄存器(如 x86 架构)
(gdb) p/x $eax # 十六进制显示寄存器值
(gdb) set $eax = 0x10 # 修改寄存器值
# 特殊寄存器
(gdb) info registers float # 浮点寄存器
(gdb) info registers vector # 向量寄存器
(gdb) info registers eflags # 标志寄存器
(gdb) x/i $pc # 查看栈指针指向的内存
(gdb) x/i $pc # 反汇编当前指令
(gdb) stepi # 单步执行一条汇编指令

使用 $<寄存器名> 可以访问 CPU 寄存器;$pc 表示程序计数器,$sp 表示栈指针,$fp 为帧指针。

在GDB内部,$pc 实际上是一个宏,会根据当前调试目标的架构自动映射到正确的寄存器:

  • x86-64 → 映射到 $rip
  • ARM → 映射到 $r15$pc
  • MIPS → 映射到 $pc

示例

$ # gdb -q  ./a.out 
Reading symbols from ./a.out...
(gdb) l
1 #include <signal.h>
2 #include <stdlib.h>
3 #include <stdio.h>
4
5 int main() {
6 int a;
7 a=10;
8 printf("Hello World\n");
9 printf("%d\n",a);
10 return 0;
(gdb) b 8
Breakpoint 1 at 0x401145: file main.c, line 8.
(gdb) r
Starting program: /home/a.out
[Thread debugging using libthread_db enabled]
Breakpoint 1, main () at main.c:8
8 printf("Hello World\n");
(gdb) p a
$1 = 10
(gdb) info locals
a = 10
(gdb) set var a=11
(gdb) p a
$2 = 11
(gdb) p &a
$3 = (int *) 0x7fffffffe23c
(gdb) x 0x7fffffffe23c
0x7fffffffe23c: 0x0000000b
(gdb) x/10x 0x7fffffffe23c
0x7fffffffe23c: 0x0000000b 0x00000001 0x00000000 0xf7c29590
0x7fffffffe24c: 0x00007fff 0x00000000 0x00000000 0x00401136
0x7fffffffe25c: 0x00000000 0x00000000
(gdb) n
Hello World
9 printf("%d\n",a);
(gdb)
11
10 return 0;
(gdb)

4. 高级调试技巧

4.1 多进程调试

# 使用 `info inferior` 命令查看当前程序中的所有进程。
(gdb) info inferior
# 使用 `inferior` 命令切换不同进程进行调试
(gdb) inferior <process_id>
# `follow-fork-mode` 参数用来设置gdb跟踪父进程还是子进程
# `set follow-fork-mode parent` 在fork之后,调试父进程,这也是gdb的默认值
(gdb) set follow-fork-mode child/parent
# gdb是否控制未调试的进程
# `set detach-on-fork on` gdb默认值,gdb不控制未调试的进
(gdb) set detach-on-fork on/off

gdb 默认只会跟踪父进程。当断点处在子进程路径时,程序可能会直接执行结束退出。

此时,可以通过设置 set follow-fork-mode child 再执行 run运行调试。

也可以 break fork 断点到fork时。

4.2 多线程调试

# 使用 `info threads` 命令查看当前程序中的所有线程。
(gdb) info threads
# 使用 `thread` 命令切换到特定线程。
(gdb) thread thread_id
(gdb) break location thread thread_id

4.3 观察点(Watchpoints)

# 标记观察断点,监控数值更改(wirte)
(gdb) watch expression or variable
# 标记观察断点,监控数值读取(read)
(gdb) rwatch expression or variable
# 标记观察断点。同时监控数据读取与更改(read and write)
(gdb) awatch expression or variable

示例

$ gdb -q a.out
Reading symbols from a.out...done.
(gdb) l
1 #include <stdio.h>
2
3 int get_sum(int n) {
4 int sum = 0, i;
5 for (i = 0; i < n; i++) {
6 sum += i;
7 }
8 return sum;
9 }
10
(gdb) b 6
Breakpoint 1 at 0x4005ad: file main.c, line 6.
(gdb) r
Starting program: /tmp/a.out
Missing separate debuginfos, use: yum debuginfo-install glibc-2.28-164.el8.x86_64

Breakpoint 1, get_sum (n=100) at main.c:6
6 sum += i;
(gdb) watch i==99
Hardware watchpoint 2: i==99
(gdb) clear 6
Deleted breakpoint 1
(gdb) c
Continuing.

Hardware watchpoint 2: i==99

Old value = 0
New value = 1
0x00000000004005b7 in get_sum (n=100) at main.c:5
5 for (i = 0; i < n; i++) {
(gdb) p i
$1 = 99
(gdb) p sum
$2 = 4851

4.4 捕获点(Catchpoints)

参考链接

# 用捕捉断点监控某一事件的发生,等同于在程序中该事件发生的位置打普通断点。
(gdb) catch event

普通断点作用于程序中的某一行,当程序运行至此行时停止执行,观察断点作用于某一变量或表达式,当该变量(表达式)的值发生改变时,程序暂停。而捕捉断点的作用是,监控程序中某一事件的发生,例如程序发生某种异常时、某一动态库被加载时等等,一旦目标时间发生,则程序停止执行。

4.5 自动化调试

# 使用 `source` 命令执行 GDB 脚本,实现自动化调试。
(gdb) source <script_file>

4.6 远程调试

# GDB支持与远程系统调试,使用 `target remote` 命令连接远程程序。
(gdb) target remote <hostname>:<port>

4.7 切换栈帧

f 切换

(gdb) bt                                                                                                      
#0 add (low=1, high=10) at test1.c:5
#1 0x00000000004005e6 in main (argc=1, argv=0x7fffffffe408) at test1.c:12
(gdb) frame 1
#1 0x00000000004005e6 in main (argc=1, argv=0x7fffffffe408) at test1.c:12
12 result[0] = add(1,10);
(gdb) f 1
#1 0x00000000004005e6 in main (argc=1, argv=0x7fffffffe408) at test1.c:12
12 result[0] = add(1,10);
(gdb) i locals
result = {-134224088, 32767, 0, 0}

5. 调试信息与符号

5.1 编译时添加调试信息

# -O0禁用优化,可以帮助调试代码
gcc -O0 -g program.c -o program

5.2 分离调试信息

# 拷贝elf中的调试信息到debug文件
objcopy --only-keep-debug program program.debug
# 瘦身二进制程序
strip --strip-debug program

5.3 加载调试信息

运行时加载

(gdb) symbol-file program.debug

提前做好关联,无需运行时加载

$ objcopy --add-gnu-debuglink=program.debug program
$ objdump -s -j .gnu_debuglink program

program: file format elf64-x86-64

Contents of section .gnu_debuglink:
0000 70726f67 72616d2e 64656275 67000000 program.debug...
0010 cb07fc34

6. 调试案例

6.1 段错误调试

(gdb) run
(gdb) bt

6.2 内存泄漏检查

(结合valgrind使用)

6.3 死锁检测

(多线程调试示例)

7. GDB 实现原理

7.1 ptrace系统调用

GDB通过ptrace系统调用实现进程控制:

long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);

7.2 断点实现原理

  • 将断点处指令替换为int 3
  • 保存原始指令
  • 触发SIGTRAP信号

8. 参考资源

  • GNU GDB官方文档
  • 《The Art of Debugging with GDB, DDD, and Eclipse》
  • GDB Cheat Sheet