文章目录
- [Linux 环境下GDB调试技巧与指令完全指南](#Linux 环境下GDB调试技巧与指令完全指南)
- [1.GDB 概述与环境准备](#1.GDB 概述与环境准备)
-
- [1.1 什么是GDB](#1.1 什么是GDB)
- [1.2 安装GDB](#1.2 安装GDB)
- [1.3 GCC、G++、GDB的区别与协作](#1.3 GCC、G++、GDB的区别与协作)
- [1.4 编译调试版程序](#1.4 编译调试版程序)
- [1.5 验证调试信息](#1.5 验证调试信息)
- [1.6 GDB 的四种启动模式](#1.6 GDB 的四种启动模式)
- [2.GDB 启动与基础操作](#2.GDB 启动与基础操作)
-
- [2.1 启动与退出](#2.1 启动与退出)
- [2.2 运行控制](#2.2 运行控制)
- [2.3 运行参数与环境](#2.3 运行参数与环境)
- [2.4 查看源码](#2.4 查看源码)
- [2.5 搜索源码](#2.5 搜索源码)
- [2.6 帮助系统](#2.6 帮助系统)
- 3.断点体系------精准控制程序暂停
-
- [3.1 普通断点(breakpoint)](#3.1 普通断点(breakpoint))
- [3.2 条件断点](#3.2 条件断点)
- [3.3 观察断点(watchpoint)](#3.3 观察断点(watchpoint))
- [3.4 捕捉断点(catchpoint)](#3.4 捕捉断点(catchpoint))
- [3.5 断点管理](#3.5 断点管理)
- [3.6 断点命令列表(commands)](#3.6 断点命令列表(commands))
- [3.7 保存与恢复断点](#3.7 保存与恢复断点)
- 4.数据观察------变量、内存与表达式
-
- [4.1 print 命令详解](#4.1 print 命令详解)
- [4.2 打印复杂类型](#4.2 打印复杂类型)
- [4.3 类型查询](#4.3 类型查询)
- [4.4 display------持续显示变量](#4.4 display——持续显示变量)
- [4.5 内存检查------x 命令](#4.5 内存检查——x 命令)
- [4.6 修改变量与内存](#4.6 修改变量与内存)
- [4.7 寄存器查看](#4.7 寄存器查看)
- 5.栈帧与调用链分析
-
- [5.1 栈帧基础](#5.1 栈帧基础)
- [5.2 栈帧操作命令](#5.2 栈帧操作命令)
- [5.3 backtrace 实战解读](#5.3 backtrace 实战解读)
- [5.4 函数调用](#5.4 函数调用)
- 6.多线程与多进程调试
-
- [6.1 多线程调试基础](#6.1 多线程调试基础)
- [6.2 线程断点](#6.2 线程断点)
- [6.3 调度器锁定](#6.3 调度器锁定)
- [6.4 线程创建与退出事件](#6.4 线程创建与退出事件)
- [6.5 多进程调试](#6.5 多进程调试)
- [6.6 多线程死锁排查实战](#6.6 多线程死锁排查实战)
- [7.Core Dump 文件分析](#7.Core Dump 文件分析)
-
- [7.1 什么是 Core Dump](#7.1 什么是 Core Dump)
- [7.2 启用Core Dump](#7.2 启用Core Dump)
- [7.3 分析Core Dump](#7.3 分析Core Dump)
- [7.4 Core Dump分析实战](#7.4 Core Dump分析实战)
- [7.5 Core Dump常见问题](#7.5 Core Dump常见问题)
- [7.6 线上环境调试建议](#7.6 线上环境调试建议)
- 8.内存检查与段错误排查
-
- [8.1 段错误定位](#8.1 段错误定位)
- [8.2 内存越界检查------AddressSanitizer](#8.2 内存越界检查——AddressSanitizer)
- [8.3 内存泄漏检测](#8.3 内存泄漏检测)
- [8.4 使用Valgrind检测内存问题](#8.4 使用Valgrind检测内存问题)
- [8.5 GDB中的内存映射查看](#8.5 GDB中的内存映射查看)
- 9.反向调试------时光倒流的魔法
-
- [9.1 启用记录与回放](#9.1 启用记录与回放)
- [9.2 设置执行方向](#9.2 设置执行方向)
- [9.3 反向调试实战场景](#9.3 反向调试实战场景)
- [9.4 反向调试的限制](#9.4 反向调试的限制)
- [9.5 替代方案:rr(Record and Replay)](#9.5 替代方案:rr(Record and Replay))
- 10.远程调试与gdbserver
-
- [10.1 为什么需要远程调试](#10.1 为什么需要远程调试)
- [10.2 gdbserver 基本用法](#10.2 gdbserver 基本用法)
- [10.3 远程调试完整流程](#10.3 远程调试完整流程)
- [10.4 gdbserver 高级选项](#10.4 gdbserver 高级选项)
- [11.GDB TUI图形化界面](#11.GDB TUI图形化界面)
-
- [11.1 启用 TUI模式](#11.1 启用 TUI模式)
- [11.2 TUI窗口管理](#11.2 TUI窗口管理)
- [11.3 TUI 快捷键](#11.3 TUI 快捷键)
- [11.4 CGDB------增强版TUI](#11.4 CGDB——增强版TUI)
- [12.VS Code 图形化调试](#12.VS Code 图形化调试)
-
- [12.1 为什么用 VS Code 调试](#12.1 为什么用 VS Code 调试)
- [12.2 环境准备](#12.2 环境准备)
- [12.3 配置文件说明](#12.3 配置文件说明)
- [12.4 tasks.json 配置](#12.4 tasks.json 配置)
- [12.5 launch.json 配置](#12.5 launch.json 配置)
- [12.6 调试工具栏](#12.6 调试工具栏)
- [12.7 调试面板信息](#12.7 调试面板信息)
- [12.8 调试控制台](#12.8 调试控制台)
- [12.9 VS Code 调试常见问题](#12.9 VS Code 调试常见问题)
- 13.信号处理与异常捕获
-
- [13.1 信号基础](#13.1 信号基础)
- [13.2 自定义信号处理](#13.2 自定义信号处理)
- [13.3 主动发送信号](#13.3 主动发送信号)
- [13.4 C++ 异常调试](#13.4 C++ 异常调试)
- [14.自定义命令与 .gdbinit 配置](#14.自定义命令与 .gdbinit 配置)
-
- [14.1 gdbinit 文件](#14.1 gdbinit 文件)
- [14.2 自定义命令(define)](#14.2 自定义命令(define))
- [14.3 Hook 命令](#14.3 Hook 命令)
- [14.4 常用gdbinit实用配置集合](#14.4 常用gdbinit实用配置集合)
- [15.GDB Python脚本编程](#15.GDB Python脚本编程)
-
- [15.1 Python脚本基础](#15.1 Python脚本基础)
- [15.2 使用GDB Python API](#15.2 使用GDB Python API)
- [15.3 Pretty Printer------美化打印](#15.3 Pretty Printer——美化打印)
- [15.4 自定义断点(Python)](#15.4 自定义断点(Python))
- [15.5 实用Python脚本示例](#15.5 实用Python脚本示例)
- 16.常用调试实战案例
-
- [16.1 案例一:定位段错误](#16.1 案例一:定位段错误)
- [16.2 案例二:调试多线程死锁](#16.2 案例二:调试多线程死锁)
- [16.3 案例三:查找内存泄漏](#16.3 案例三:查找内存泄漏)
- [16.4 案例四:条件断点优化循环调试](#16.4 案例四:条件断点优化循环调试)
- [16.5 案例五:调试共享库问题](#16.5 案例五:调试共享库问题)
- [17.附录 A GDB命令速查表](#17.附录 A GDB命令速查表)
-
- [17.1 启动与退出](#17.1 启动与退出)
- [17.2 运行控制](#17.2 运行控制)
- [17.3 断点](#17.3 断点)
- [17.4 数据查看](#17.4 数据查看)
- [17.5 栈帧](#17.5 栈帧)
- [17.6 多线程](#17.6 多线程)
- [17.7 反向调试](#17.7 反向调试)
- [17.8 源码与汇编](#17.8 源码与汇编)
- 18.GCC编译选项速查
-
- [18.1 常用编译选项](#18.1 常用编译选项)
- [18.2 库文件编译](#18.2 库文件编译)
- 19.总结
Linux 环境下GDB调试技巧与指令完全指南
1.GDB(GNU Debugger)是 Linux下最强大的程序调试工具,支持C、C++、Go、等多种语言。
2.本文从基础操作到高级技巧,系统性地讲解GDB的核心功能,涵盖断点管理、内存检查、多线程调试、反向调试、远程调试、脚本自动化等内容,并结合实战案例帮助你真正掌握这一调试利器。
1.GDB 概述与环境准备
1.1 什么是GDB
GDB是GNU项目开发的调试器,诞生于 1986 年,由Richard Stallman亲自编写。它能够在程序运行时检查其内部状态,也可以在程序崩溃后分析其死亡现场。GDB的核心能力包括:
- 启动程序,指定任意运行参数和环境
- 设置断点,让程序在指定条件下暂停
- 检查状态,查看变量、内存、寄存器、调用栈
- 动态修改,在运行时改变程序的内部状态
- 反向执行,让程序"时光倒流"回退到之前的执行点
1.2 安装GDB
大多数Linux发行版通过包管理器即可安装:
bash
# Debian/Ubuntu
sudo apt update
sudo apt install build-essential gdb
# Arch Linux
sudo pacman -S gdb
安装完成后验证版本:
bash
$ gdb --version
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
建议:使用GDB 9.0以上版本,以获得完整的多线程调试和Python脚本支持。
1.3 GCC、G++、GDB的区别与协作
| 工具 | 用途 | 特点 |
|---|---|---|
| GCC | 编译 C 程序 | 不自动链接 C++ 标准库 |
| G++ | 编译 C++ 程序 | 自动链接 C++ 标准库 |
| GDB | 程序调试 | 需加 -g 参数保留符号表 |
核心区别:
.c文件:gcc当作 C 程序,g++当作 C++.cpp文件:都会当成 C++ 程序g++会调用gcc,但链接阶段由g++完成(因为gcc不能自动和 C++ 程序使用的库连接)
1.4 编译调试版程序
GDB依赖编译器生成的调试信息来映射机器码与源代码的对应关系。使用GCC/G++ 编译时,务必加上 -g 选项:
bash
# 基础编译
g++ -g -O0 -o myapp myapp.cpp
# 多文件编译
g++ -g -O0 main.cpp utils.cpp -o app -lm
# 编译带更详细调试信息(包含宏定义)
g++ -g3 -O0 -o myapp myapp.cpp
# CMake 项目
cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS="-g -O0" ..
| 选项 | 作用 |
|---|---|
-g |
在可执行文件中嵌入调试信息(DWARF 格式) |
-g3 |
包含宏定义等更详细的调试信息 |
-O0 |
关闭优化,保证变量和执行顺序与源码一致 |
-gdwarf-4 |
指定 DWARF 版本(默认通常为 DWARF 4) |
-fno-omit-frame-pointer |
保留帧指针,确保栈回溯正确 |
重要提示 :调试阶段务必使用
-O0关闭优化。-O2或-O3优化会内联函数、消除变量、重排指令,导致断点偏移、变量值显示为<optimized out>等问题。
1.5 验证调试信息
编译后检查二进制文件是否包含调试信息:
bash
# 检查调试段
readelf -S ./myapp | grep debug
# 快速查看文件类型
file ./myapp
# 输出示例(有调试信息)
# .debug_info, .debug_abbrev, .debug_line 等调试段存在
1.6 GDB 的四种启动模式
bash
# 模式一:直接调试可执行文件
gdb ./myapp
# 模式二:调试正在运行的进程
gdb -p <PID>
# 或在 GDB 内部
(gdb) attach <PID>
# 模式三:调试 core dump 文件
gdb ./myapp core.<PID>
# 模式四:带参数启动
gdb --args ./myapp arg1 arg2 arg3
2.GDB 启动与基础操作
2.1 启动与退出
bash
# 启动 GDB 并加载可执行文件
gdb ./myapp
# 启动时不显示版权信息(静默模式)
gdb -q ./myapp
# 带命令行参数启动
gdb --args ./myapp arg1 arg2 arg3
# 启动后设置参数
(gdb) set args arg1 arg2 arg3
(gdb) show args
退出GDB:
bash
(gdb) quit # 或简写 q
(gdb) Ctrl+d # 快捷键退出
2.2 运行控制
| 命令 | 缩写 | 说明 |
|---|---|---|
run |
r |
从头开始运行程序 |
start |
--- | 运行程序并在 main 函数入口停下 |
continue |
c |
继续运行到下一个断点 |
step |
s |
单步执行,进入函数内部 |
next |
n |
单步执行,跳过函数调用 |
stepi |
si |
机器指令级单步,进入函数 |
nexti |
ni |
机器指令级单步,跳过函数 |
finish |
--- | 运行到当前函数返回 |
until |
u |
运行到当前循环结束(跳出循环) |
until <行号> |
--- | 运行至指定行 |
return |
--- | 直接从当前函数返回(可指定返回值) |
kill |
k |
终止正在运行的程序 |
skip |
--- | 跳过指定函数,不进入调试 |
step与next的关键区别:
c
// 当执行到第 3 行时:
1 int result = 0;
2 result = compute(a, b); ← step 会进入 compute() 函数
3 printf("%d\n", result); ← next 会把 compute() 当一步执行完
2.3 运行参数与环境
bash
# 设置运行参数
(gdb) set args --config /etc/app.conf --verbose
# 设置环境变量
(gdb) set env LD_LIBRARY_PATH=/usr/local/lib
(gdb) set env DEBUG=1
# 清除环境变量
(gdb) unset env DEBUG
# 改变工作目录
(gdb) cd /path/to/project
# 执行 shell 命令
(gdb) shell ls -la
(gdb) !ls -la # 简写形式
2.4 查看源码
bash
(gdb) list # 显示当前行附近的源码(默认10行)
(gdb) list 30 # 以第 30 行为中心显示
(gdb) list main # 显示 main 函数源码
(gdb) list file.c:50 # 显示 file.c 第 50 行
(gdb) list file.c:func # 显示 file.c 的 func 函数
(gdb) set listsize 20 # 修改每次显示的行数
(gdb) show listsize # 查看当前显示行数
2.5 搜索源码
bash
(gdb) forward-search pattern # 从当前位置向前搜索
(gdb) reverse-search pattern # 从当前位置向后搜索
2.6 帮助系统
GDB内置了完善的帮助文档:
bash
(gdb) help # 顶层帮助
(gdb) help breakpoints # 断点相关命令帮助
(gdb) help running # 运行控制命令帮助
(gdb) help data # 数据查看命令帮助
(gdb) help stack # 栈相关命令帮助
(gdb) apropos thread # 搜索与 thread 相关的所有命令
3.断点体系------精准控制程序暂停
断点是GDB调试的核心机制。掌握不同类型的断点及其高级用法,是从入门走向精通的关键一步。
3.1 普通断点(breakpoint)
bash
# 在函数入口设断点
(gdb) break main
(gdb) break MyClass::myMethod # C++ 类方法
# 在指定行设断点
(gdb) break test.c:42
# 在指定文件指定函数设断点
(gdb) break file.c:process_data
# 在当前行设断点
(gdb) break
# 用正则表达式批量设断点
(gdb) rbreak ^handle_.* # 所有以 handle_ 开头的函数
(gdb) rbreak file.c:.* # file.c 中所有函数
# 临时断点(命中一次后自动删除)
(gdb) tbreak main
(gdb) tbreak file.c:100
3.2 条件断点
当只需要在特定条件下才暂停程序时,条件断点极为有用:
bash
# 直接设置条件断点
(gdb) break file.c:50 if i == 100
(gdb) break process_data if size > 1024
(gdb) break loop_func if strcmp(name, "error") == 0
# 为已有断点添加条件
(gdb) break file.c:50
(gdb) condition 1 i == 100 # 给 1 号断点加条件
# 移除条件(保留断点)
(gdb) condition 1 # 清除 1 号断点的条件
条件断点实战技巧:在循环中调试时,如果只想在特定迭代时暂停,条件断点可以避免成千上万次的手动 continue:
c
for (int i = 0; i < 10000; i++) {
process(data[i]); // 只在 i == 5000 时暂停
}
bash
(gdb) break process if i == 5000
实战案例:排查排序算法中第 9999 次交换操作出问题:
bash
(gdb) break swap if swap_count == 9999
3.3 观察断点(watchpoint)
观察断点不关注执行到了哪一行,而是关注某个变量或内存地址的值何时发生变化:
bash
# 监视变量写入
(gdb) watch global_counter # 变量值改变时暂停
# 监视变量读取
(gdb) rwatch global_counter # 变量被读取时暂停
# 监视变量读写
(gdb) awatch global_counter # 变量被读或写时暂停
# 监视内存地址
(gdb) watch *(int*)0x7ffff7a0 # 监视指定地址的 int 值
观察断点触发时会显示旧值和新值:
Old value = 42
New value = 100
注意:watchpoint 依赖硬件调试寄存器,数量有限(通常x86架构仅有4个)。超出限制时 GDB 会使用软件模拟方式,性能会大幅下降。
3.4 捕捉断点(catchpoint)
捕捉断点用于捕获特定事件而非代码位置:
bash
(gdb) catch throw # C++ 异常抛出时暂停
(gdb) catch catch # C++ 异常捕获时暂停
(gdb) catch syscall open # 系统调用 open 时暂停
(gdb) catch syscall # 任意系统调用时暂停
(gdb) catch fork # fork 调用时暂停
(gdb) catch vfork # vfork 调用时暂停
(gdb) catch exec # exec 调用时暂停
(gdb) catch load # 动态库加载时暂停
(gdb) catch signal SIGSEGV # 捕获特定信号
catch throw 的实用场景 :当 C++ 程序因未捕获异常崩溃时,用 catch throw 可以在异常刚抛出时就暂停,而不是等到程序崩溃,这样可以获得完整的异常抛出上下文。
3.5 断点管理
bash
# 查看所有断点
(gdb) info breakpoints # 或 info break,简写 i b
(gdb) info watchpoints # 只查看观察断点
# 禁用/启用断点
(gdb) disable 1 # 禁用 1 号断点
(gdb) disable 2-5 # 禁用 2 到 5 号断点
(gdb) enable 1 # 启用 1 号断点
(gdb) enable once 1 # 启用一次,命中后自动禁用
(gdb) enable delete 1 # 启用一次,命中后自动删除
(gdb) enable count 3 1 # 启用 1 号断点,3 次后自动禁用
# 删除断点
(gdb) delete 1 # 删除 1 号断点
(gdb) delete # 删除所有断点
(gdb) clear # 删除当前位置的断点
(gdb) clear file.c:50 # 删除 file.c:50 处的断点
# 忽略断点
(gdb) ignore 1 100 # 忽略 1 号断点接下来 100 次命中
3.6 断点命令列表(commands)
commands 可以为断点绑定一组自动执行的命令,非常适合自动化调试:
bash
(gdb) break process_data if size > 1024
(gdb) commands
Type commands for breakpoint(s), 1, one per line.
End with a line saying just "end".
>printf "size = %d, data = %p\n", size, data
>backtrace 3
>continue
>end
执行效果:每次断点命中时,自动打印变量值、显示调用栈,然后继续运行。无需人工干预。
实用场景------用 commands 替代 printf 调试:
bash
(gdb) break main_loop
(gdb) commands
>silent
>printf "iteration %d: value=%f\n", iter, value
>continue
>end
silent会抑制断点命中的默认输出,只打印你指定的内容,相当于零侵入的日志打印。
3.7 保存与恢复断点
bash
# 保存所有断点到文件
(gdb) save breakpoints breakpoints.gdb
# 加载断点
(gdb) source breakpoints.gdb
4.数据观察------变量、内存与表达式
4.1 print 命令详解
print 是查看数据最基本的命令:
bash
# 基本用法
(gdb) print var # 打印变量值
(gdb) print/x var # 十六进制输出
(gdb) print/d var # 十进制输出
(gdb) print/o var # 八进制输出
(gdb) print/t var # 二进制输出
(gdb) print/c var # 字符输出
(gdb) print/f var # 浮点数输出
(gdb) print/a var # 地址输出
(gdb) print/s (char*)ptr # 字符串输出
输出格式速查:
| 格式符 | 含义 | 示例 |
|---|---|---|
/x |
十六进制 | 0xff |
/d |
有符号十进制 | 255 |
/u |
无符号十进制 | 255 |
/o |
八进制 | 0377 |
/t |
二进制 | 11111111 |
/c |
字符 | 'A' |
/f |
浮点 | 3.14 |
/a |
地址 | 0x7fffffffe000 |
/s |
字符串 | "hello" |
4.2 打印复杂类型
bash
# 打印结构体
(gdb) print my_struct
(gdb) print my_struct.field
# 打印指针指向的内容
(gdb) print ptr # 打印指针地址
(gdb) print *ptr # 解引用,打印指向的值
(gdb) print *ptr@10 # 打印连续 10 个元素(数组)
# 打印数组
(gdb) print array # 打印整个数组
(gdb) print array[0]@5 # 从 array[0] 开始打印 5 个元素
(gdb) print *array@10 # 另一种打印 10 个元素的方式
# C++ STL 容器(需 pretty printer 支持)
(gdb) print my_vector
(gdb) print my_vector.size()
(gdb) print my_map
# 打印字符串
(gdb) print str # 打印 char* 指针
(gdb) print (char*)ptr # 强制类型转换
4.3 类型查询
bash
# 简要类型
(gdb) whatis var # 输出: type = int
(gdb) whatis my_struct # 输出: type = struct MyStruct
# 详细类型定义
(gdb) ptype my_struct # 输出完整的结构体定义
# struct MyStruct {
# int id;
# char name[64];
# double value;
# }
# 查看类继承关系
(gdb) ptype MyClass
# class MyClass : public BaseClass {
# public:
# void process(void);
# private:
# int data_;
# }
4.4 display------持续显示变量
display 在每次程序暂停时自动打印指定变量,非常适合监控状态变化:
bash
(gdb) display i # 每次停下自动打印 i 的值
(gdb) display/x i # 十六进制自动打印
(gdb) display *ptr@5 # 自动打印数组前 5 个元素
# 管理 display
(gdb) info display # 查看所有 display 设置
(gdb) undisplay 1 # 取消 1 号 display
(gdb) delete display 1 # 同 undisplay
(gdb) disable display 1 # 禁用但保留
(gdb) enable display 1 # 重新启用
4.5 内存检查------x 命令
x(examine)是直接检查原始内存的最强大工具:
bash
# 语法: x/NFU address
# N: 显示单元数量
# F: 显示格式 (x/d/u/o/t/c/f/s/a/i)
# U: 单元大小 (b=1字节, h=2字节, w=4字节, g=8字节)
| 参数 | 含义 |
|---|---|
N |
显示的单元数量 |
F |
格式:x(hex) d(dec) u(unsigned) o(oct) t(bin) c(char) f(float) s(string) i(instruction) |
U |
单元大小:b(byte) h(halfword=2B) w(word=4B) g(giant=8B) |
典型用法:
bash
# 查看内存区域的十六进制
(gdb) x/16xb 0x7fffffffe000 # 16 个字节,十六进制
(gdb) x/8xw 0x7fffffffe000 # 8 个 word(4字节),十六进制
(gdb) x/4xg 0x7fffffffe000 # 4 个 giant(8字节),十六进制
# 查看字符串
(gdb) x/s str_ptr # 从地址读取以 null 结尾的字符串
# 查看内存中的指令
(gdb) x/10i $pc # 查看当前 PC 处的 10 条汇编指令
(gdb) x/10i main # 查看 main 函数的汇编指令
# 查看栈上的数据
(gdb) x/32xb $rsp # 查看栈顶 32 字节
(gdb) x/8xg $rbp # 查看帧指针附近的 8 个 8 字节值
# 查看数组
(gdb) x/20dw array_ptr # 20 个十进制整数
# 查看64字节十六进制内存
(gdb) x/64x &buffer
# 反汇编指定地址
(gdb) x/10i &function
4.6 修改变量与内存
bash
# 修改变量值
(gdb) set variable i = 100
(gdb) set var ptr = (int*)malloc(sizeof(int) * 10) # 动态分配内存
# 修改内存
(gdb) set *(int*)0x7fffffffe000 = 42
# 修改结构体字段
(gdb) set my_struct.field = 3.14
# 修改字符串
(gdb) set str = "new value"
警告:运行时修改变量可能导致程序进入未预期的状态,请确保理解修改的影响。
4.7 寄存器查看
bash
(gdb) info registers # 查看所有通用寄存器
(gdb) info all-registers # 查看所有寄存器(含浮点、向量)
(gdb) print $rax # 查看特定寄存器
(gdb) print $pc # 查看程序计数器
(gdb) print $rsp # 查看栈指针
(gdb) print $rbp # 查看帧指针
(gdb) set $rax = 0 # 修改寄存器值
5.栈帧与调用链分析
5.1 栈帧基础
每当函数被调用,一个栈帧(stack frame)被压入调用栈,包含局部变量、参数、返回地址等信息。理解栈帧是排查复杂bug的关键。
bash
+-------------------+
| main() frame | ← frame 3 (最外层)
+-------------------+
| parse() frame | ← frame 2
+-------------------+
| process() frame | ← frame 1
+-------------------+
| compute() frame | ← frame 0 (当前函数)
+-------------------+ ← 栈顶 (低地址方向)
5.2 栈帧操作命令
bash
# 查看完整调用栈
(gdb) backtrace # 或 bt
(gdb) backtrace full # 同时打印每个帧的局部变量
(gdb) backtrace 5 # 只显示最近 5 个帧
(gdb) bt # 简写
# 切换栈帧
(gdb) frame 2 # 切换到第 2 号帧
(gdb) frame # 显示当前帧信息
(gdb) up # 切换到上一级调用帧
(gdb) up 2 # 上移两级
(gdb) down # 切换到下一级调用帧
(gdb) down 1 # 下移一级
# 查看帧信息
(gdb) info frame # 当前帧的详细信息(地址、参数等)
(gdb) info args # 当前帧的函数参数
(gdb) info locals # 当前帧的局部变量
(gdb) info frame 2 # 指定帧的信息
5.3 backtrace 实战解读
(gdb) bt
#0 compute (a=0x0, b=42) at math.c:25
#1 0x0000555555555189 in process (data=0x5555555592a0) at process.c:67
#2 0x00005555555551f4 in parse (input=0x7fffffffe2d0) at parse.c:103
#3 0x0000555555555268 in main (argc=2, argv=0x7fffffffe3d8) at main.c:34
阅读顺序(从下往上):
- #0 :确认"死因"------
compute函数中a为 NULL 指针,这很可能就是段错误的原因 - #1 :找到"谁把你送走的"------调用者
process,传入了data指针 - #2/#3:定位业务入口
bash
(gdb) frame 1
(gdb) info locals
data_ptr = 0x5555555592a0
result = 0
index = 5
栈不是一条线,而是一棵树。很多人只看 #0,这是典型的新手误区。真正有价值的,往往在 #1 ~ #3。
5.4 函数调用
GDB允许在调试过程中直接调用程序的函数:
bash
(gdb) call printf("debug: i=%d\n", i) # 调用 printf
(gdb) call malloc(1024) # 调用 malloc
(gdb) print compute(3, 4) # 调用并打印返回值
(gdb) call my_debug_function() # 调用自定义调试函数
注意 :
call和
6.多线程与多进程调试
6.1 多线程调试基础
现代程序几乎都涉及多线程,GDB提供了完整的多线程调试支持。
bash
# 查看所有线程
(gdb) info threads
# Id Target Id Frame
# * 1 Thread 0x7f... "myapp" compute (a=0x0, b=42) at math.c:25
# 2 Thread 0x7f... "myapp" pthread_cond_wait (...) at thread.c:78
# 3 Thread 0x7f... "myapp" __libc_accept (...) at server.c:142
# 切换线程
(gdb) thread 2 # 切换到 2 号线程
(gdb) thread apply all bt # 所有线程的调用栈(极其常用!)
(gdb) thread apply all info locals # 所有线程的局部变量
# 对指定线程执行命令
(gdb) thread apply 1 3 bt # 只查看 1 号和 3 号线程的调用栈
(gdb) thread apply 2 print var # 查看 2 号线程的 var 变量
6.2 线程断点
bash
# 只在特定线程暂停
(gdb) break file.c:50 thread 2
# 在所有线程的指定行设断点
(gdb) break file.c:50 thread all
6.3 调度器锁定
多线程调试最大的痛点是:当你单步执行一个线程时,其他线程也在并行运行。GDB 提供了调度器锁定来解决这个问题:
bash
# 查看当前调度器锁定模式
(gdb) show scheduler-locking
# 模式说明:
(gdb) set scheduler-locking off # 默认:所有线程都可以执行
(gdb) set scheduler-locking on # 只有当前线程可以执行
(gdb) set scheduler-locking step # 单步时锁定,continue 时解锁
(gdb) set scheduler-locking replay # 回放模式(用于反向调试)
调试建议 :单步调试某个线程的逻辑时,开启 set scheduler-locking on,防止其他线程干扰;使用 continue 前切换回 off。
6.4 线程创建与退出事件
bash
# 在新线程创建时暂停
(gdb) set print thread-events on # 打印线程创建/退出消息(默认开启)
# 捕获线程相关事件
(gdb) catch pthread_create # 线程创建时暂停
6.5 多进程调试
调试fork产生的子进程:
bash
# 模式一:只调试父进程(默认)
(gdb) set detach-on-fork on
# 模式二:同时调试父子进程
(gdb) set detach-on-fork off
(gdb) set follow-fork-mode child # fork 后跟踪子进程
(gdb) set follow-fork-mode parent # fork 后跟踪父进程(默认)
# 查看所有被调试的进程
(gdb) info inferiors
# Num Description Executable
# * 1 process 12345 ./myapp
# 2 process 12346 ./myapp
# 切换进程
(gdb) inferior 2
6.6 多线程死锁排查实战
死锁产生的原因:
- 加锁顺序不当:线程 A 持有资源 1 想获取资源 2,线程 B 持有资源 2 想获取资源 1
- 重复加锁:同一线程对非递归锁重复加锁
- 加锁后未解锁:异常导致锁未释放
Shell初步判断:
bash
# 查看进程状态
ps aux | grep deadlock
# STAT 字段说明:S=中断睡眠,s=会话领导进程,l=多线程进程
# 深入查看线程资源占用
top -Hp <PID>
# 死锁特征:多个线程 CPU 使用率趋近于 0
GDB深入排查:
bash
# Attach 到进程(需要 sudo 权限)
sudo gdb attach <PID>
# 查看所有线程堆栈
(gdb) thread apply all bt
# 输出示例分析
Thread 3:
#0 0x00007ffff7b59d8f in pthread_mutex_lock () from /lib64/libpthread.so.0
#1 0x0000000000400934 in threadFunction2 () at deadlock.cpp:20
...
Thread 2:
#0 0x00007ffff7b59d8f in pthread_mutex_lock () from /lib64/libpthread.so.0
#1 0x0000000000400899 in threadFunction1 () at deadlock.cpp:13
...
分析要点:
- 两个线程都卡在
pthread_mutex_lock - 查看具体行号,分析加锁顺序
- 对比各线程的 mutex 持有情况
7.Core Dump 文件分析
7.1 什么是 Core Dump
当进程因为非法内存访问、除零、assert 失败等原因异常终止时,内核会把进程当时的内存、寄存器、线程栈等信息保存成一个core文件。
本质 :core 文件是一个 ELF 格式的进程内存快照,包含了崩溃瞬间的完整状态。
触发信号:
| 信号 | 触发场景 | 默认行为 |
|---|---|---|
| SIGSEGV | 访问非法内存地址(空指针、越界) | 终止 + core |
| SIGABRT | 调用 abort(),断言失败 |
终止 + core |
| SIGFPE | 除以零、浮点异常 | 终止 + core |
| SIGBUS | 内存对齐错误 | 终止 + core |
| SIGILL | 非法 CPU 指令 | 终止 + core |
7.2 启用Core Dump
默认情况下,很多 Linux 发行版禁用了 core dump 生成。需要手动开启:
bash
# 查看当前限制
ulimit -c
# 0 (表示禁用)
# 临时开启(当前终端生效)
ulimit -c unlimited
# 永久开启
echo "ulimit -c unlimited" >> ~/.bashrc
source ~/.bashrc
查看/修改 core文件格式:
bash
# 查看当前 core 文件格式
cat /proc/sys/kernel/core_pattern
# 自定义格式(需要 sudo)
echo "/tmp/core-%e-%p-%t" | sudo tee /proc/sys/kernel/core_pattern
# %e: 可执行文件名 %p: PID %t: 时间戳 %s: 信号编号 %h: 主机名
注意 :如果系统安装了
systemd-coredump,core 文件会被systemd接管,存到/var/lib/systemd/coredump/里。可以用coredumpctl list查看。
7.3 分析Core Dump
bash
# 加载 core 文件(必须同时指定可执行文件)
gdb ./myapp /tmp/core-myapp-12345-1700000000
# 常用分析步骤
(gdb) bt # 查看崩溃时的调用栈
(gdb) bt full # 调用栈 + 所有帧的局部变量
(gdb) info threads # 查看所有线程状态
(gdb) thread apply all bt # 所有线程的调用栈
(gdb) frame 0 # 切换到崩溃帧
(gdb) info locals # 查看局部变量
(gdb) print var # 查看关键变量
(gdb) x/10xb ptr # 检查指针指向的内存
7.4 Core Dump分析实战
bash
$ gdb ./server /tmp/core-server-28431-1700000000
(gdb) bt
#0 0x00007f8a9b3e2a8c in pthread_mutex_lock () from /lib/x86_64-linux-gnu/libpthread.so.0
#1 0x0000555555555342 in handle_request (conn=0x5555555592a0) at server.c:89
#2 0x0000555555555489 in worker_thread (arg=0x0) at server.c:142
#3 0x00007f8a9b3e0609 in start_thread () from /lib/x86_64-linux-gnu/libpthread.so.0
(gdb) frame 1
(gdb) print conn->mutex
$1 = __data = {__lock = 2, __owner = 28430, ...}
# __owner = 28430 但当前线程不是持有者 → 死锁!
7.5 Core Dump常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 生成不了 core 文件 | ulimit -c 为 0 |
ulimit -c unlimited |
| core 文件不在预期位置 | core_pattern 被修改 |
检查或修改 /proc/sys/kernel/core_pattern |
| 看不到源代码 | 编译时没加 -g |
重新编译加 -g |
符号显示为 ?? |
程序被 strip 过 |
保留 .debug 文件或用 addr2line |
7.6 线上环境调试建议
生产环境的二进制通常是剥离了调试信息的,但仍然可以分析:
保留独立调试信息文件:
bash
# 编译带调试信息
g++ -g -O2 myapp.cpp -o myapp
# 剥离调试信息单独保存
objcopy --only-keep-debug myapp myapp.debug
# 部署时 strip(删除调试信息)
strip myapp
# 分析时加载 debug 文件
gdb myapp core
(gdb) symbol-file myapp.debug
使用addr2line辅助定位:
bash
addr2line -e myapp 0x00401234
# 输出:myapp.cpp:42
8.内存检查与段错误排查
8.1 段错误定位
段错误(Segmentation Fault)是最常见的运行时错误之一,通常由以下原因引起:
| 原因 | 典型场景 |
|---|---|
| 解引用 NULL 指针 | *ptr,但 ptr == NULL |
| 访问已释放的内存 | use-after-free |
| 栈溢出 | 递归调用过深 |
| 数组越界 | array[100] 但数组只有 10 个元素 |
| 写入只读内存 | 修改字符串常量 |
基本定位步骤:
bash
# 方法一:直接运行,GDB 会自动在段错误处暂停
(gdb) run
# Program received signal SIGSEGV, Segmentation fault.
# 0x000055555555517a in compute (a=0x0, b=42) at math.c:25
# 25 return *a + b;
# 方法二:通过 core dump 分析(见第七章)
8.2 内存越界检查------AddressSanitizer
GDB配合编译器的地址消毒器(AddressSanitizer)可以自动检测内存越界:
bash
# 编译时启用 ASan
g++ -g -O0 -fsanitize=address -fno-omit-frame-pointer -o myapp myapp.cpp
# 运行时 ASan 会自动报告越界访问
$ ./myapp
# ==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000010
# READ of size 4 at 0x602000000010 thread T0
# #0 0x55555555517a in compute math.c:25
ASan 直接在越界那行报错,不用等到崩溃。这是排查"随机崩溃"的神器。
8.3 内存泄漏检测
bash
# 编译时启用 LeakSanitizer(ASan 自动包含)
g++ -g -O0 -fsanitize=address -o myapp myapp.cpp
# 运行程序后自动报告泄漏
$ ./myapp
# ==12345==ERROR: LeakSanitizer: detected memory leaks
# Direct leak of 1024 byte(s) in 1 object(s) allocated from:
# #0 0x55555555517a in malloc
# #1 0x5555555551a0 in process process.c:42
8.4 使用Valgrind检测内存问题
Valgrind是与GDB互补的内存检查工具,无需重新编译即可使用:
bash
# 安装
sudo apt install valgrind
# 基本内存检查
valgrind --leak-check=full --show-leak-kinds=all ./myapp
# 配合 GDB 使用
valgrind --vgdb=yes --vgdb-error=0 ./myapp
# 然后在另一个终端
gdb ./myapp
(gdb) target remote | vgdb
8.5 GDB中的内存映射查看
bash
# 查看进程的内存映射
(gdb) info proc mappings
# 查看指定地址所属的内存区域
(gdb) info files
# 保存内存区域到文件
(gdb) dump memory heap.dump 0x555555559000 0x55555557a000
9.反向调试------时光倒流的魔法
反向调试允许程序"倒着运行",回到之前的执行状态。这是GDB最强大也最容易被忽视的功能之一。
9.1 启用记录与回放
bash
# 启动程序
(gdb) start
# 开始记录执行过程
(gdb) record # 开始记录(软件方式)
(gdb) record btrace # 使用硬件分支追踪(性能更好)
# 现在可以正常运行程序
(gdb) continue
(gdb) step
(gdb) next
# 反向执行
(gdb) reverse-step # 反向单步(进入函数)
(gdb) reverse-next # 反向单步(跳过函数)
(gdb) reverse-stepi # 反向单条指令(进入)
(gdb) reverse-nexti # 反向单条指令(跳过)
(gdb) reverse-continue # 反向继续执行
(gdb) reverse-finish # 反向运行到函数被调用处
# 停止记录
(gdb) record stop
9.2 设置执行方向
更优雅的方式是直接改变执行方向:
bash
(gdb) set exec-direction reverse # 所有执行命令变成反向
(gdb) step # 等同于 reverse-step
(gdb) next # 等同于 reverse-next
(gdb) continue # 等同于 reverse-continue
(gdb) set exec-direction forward # 恢复正向执行(默认)
9.3 反向调试实战场景
场景 :你在第50行发现变量 result 的值不对,想知道它是何时被改成这个值的。
bash
# 1. 在发现问题的位置设断点
(gdb) break main.c:50
(gdb) continue
# 2. 确认 result 值不对
(gdb) print result
$1 = -1 # 不应该是 -1
# 3. 设置 watchpoint 监视 result
(gdb) watch result
# 4. 反向运行到 result 被修改的时刻
(gdb) reverse-continue
# Hardware watchpoint 2: result
# Old value = -1
# New value = 42
# 0x00005555555551a0 in compute_result () at result.c:30
# 30 result = compute(input); # 这里是罪魁祸首!
9.4 反向调试的限制
- 需要目标环境支持(大多数 Linux x86/x86-64 原生支持)
- 系统调用和设备 I/O 无法完美回退
- 优化过的代码可能导致行为异常
record有性能开销(约 2-5x 减速)- 多线程程序的支持有限
9.5 替代方案:rr(Record and Replay)
Mozilla开发的 rr 调试器是更强大的反向调试工具:
bash
# 安装
sudo apt install rr
# 记录程序执行
rr record ./myapp
# 回放并调试
rr replay
# 在 rr 中使用 GDB 的所有反向调试命令
(rr) reverse-continue
(rr) reverse-step
rr 的优势:性能开销更小、多线程支持更好、确定性回放。
10.远程调试与gdbserver
10.1 为什么需要远程调试
在嵌入式开发、容器化部署、跨平台调试等场景中,程序运行在远程机器上,而开发者希望在本地使用GDB调试。这时就需要 dbserver。
10.2 gdbserver 基本用法
远程端(目标机器):
bash
# 启动 gdbserver,监听指定端口
gdbserver :1234 ./myapp
# 附加到已运行的进程
gdbserver :1234 --attach <PID>
# 使用串口连接
gdbserver /dev/ttyS0 ./myapp
本地端(开发机器):
bash
# 启动 GDB 并连接到远程
gdb ./myapp
(gdb) target remote 192.168.1.100:1234
# 或一行命令
gdb -ex "target remote 192.168.1.100:1234" ./myapp
10.3 远程调试完整流程
bash
# ========== 远程端 ==========
# 1. 将编译好的带调试信息的程序复制到远程机器
scp ./myapp user@remote:/tmp/
# 2. 启动 gdbserver
user@remote:~$ gdbserver :1234 /tmp/myapp arg1 arg2
# Process /tmp/myapp created; pid = 5678
# Listening on port 1234
# ========== 本地端 ==========
# 3. 启动 GDB 连接
gdb ./myapp
(gdb) target remote 192.168.1.100:1234
# Remote debugging using 192.168.1.100:1234
# 4. 正常使用 GDB 命令调试
(gdb) break main
(gdb) continue
(gdb) step
(gdb) print var
10.4 gdbserver 高级选项
bash
# 多进程模式(允许 gdbserver 在后台运行)
gdbserver --multi :1234
# 指定调试信息文件搜索路径
(gdb) set sysroot /path/to/target/rootfs
(gdb) set solib-search-path /path/to/libs
# 使用 SSH 隧道增强安全性
ssh -L 1234:localhost:1234 user@remote
# 然后连接 localhost:1234
(gdb) target remote localhost:1234
11.GDB TUI图形化界面
11.1 启用 TUI模式
GDB内置了文本用户界面(TUI),可以在终端中显示源码、汇编、寄存器等窗口:
bash
# 方式一:启动时启用
gdb -tui ./myapp
# 方式二:在 GDB 中切换
(gdb) tui enable # GDB 10+ 启用 TUI
(gdb) Ctrl+x Ctrl+a # 快捷键切换 TUI
# 方式三:使用 layout 命令
(gdb) layout src # 源码窗口
(gdb) layout asm # 汇编窗口
(gdb) layout split # 源码 + 汇编窗口
(gdb) layout regs # 寄存器窗口
11.2 TUI窗口管理
bash
# 焦点切换
Ctrl+x o # 在窗口间切换焦点
# 窗口操作
(gdb) tui reg general # 显示通用寄存器
(gdb) tui reg float # 显示浮点寄存器
(gdb) tui reg system # 显示系统寄存器
(gdb) tui reg next # 下一组寄存器
# 刷新窗口(显示错乱时使用)
Ctrl+l # 刷新屏幕
11.3 TUI 快捷键
| 快捷键 | 功能 |
|---|---|
Ctrl+x Ctrl+a |
切换 TUI 模式 |
Ctrl+x 1 |
只显示一个窗口 |
Ctrl+x 2 |
切换双窗口布局 |
Ctrl+x o |
切换焦点窗口 |
Ctrl+l |
刷新屏幕 |
PageUp/PageDown |
滚动源码窗口 |
Ctrl+p / Ctrl+n |
历史命令(TUI 模式下方向键用于滚动) |
11.4 CGDB------增强版TUI
CGDB 是基于GDB的增强前端,提供类似vi的操作体验:
bash
# 安装
sudo apt install cgdb
# 使用
cgdb ./myapp
# CGDB 快捷键(vi 风格)
# ESC - 进入 vi 模式
# i - 进入 GDB 命令模式
# /pattern - 搜索源码
# :quit - 退出
# Space - 设置/取消断点
12.VS Code 图形化调试
12.1 为什么用 VS Code 调试
GDB功能强大,但对于大多数开发者来说,纯命令行界面看复杂的嵌套结构、追踪多线程调用栈、频繁跳转源代码,还是有不小的难度。VS Code可以解决这些问题:
- 把复杂的GDB指令转化为可视化的断点、变量监视窗口和调用堆栈树
- VS Code启动非常快,内存占用相对较低
- 配合Remote-SSH插件,可以在 Windows调试运行在 Linux服务器的 C++代码
核心原理:VS Code 本身没有调试 C++代码的能力。它只是一个前端代理:C/C++ 插件把界面操作翻译成GDB指令,VS Code 通过 Debug Adapter Protocol 跟 GDB 通信。
12.2 环境准备
bash
# Ubuntu/Debian
sudo apt update
sudo apt install build-essential gdb cmake -y
# Fedora/RedHat
sudo dnf groupinstall "Development Tools"
sudo dnf install gdb cmake -y
VS Code 插件:
| 插件名称 | 说明 |
|---|---|
| C/C++ (Microsoft) | 必装。提供语法高亮、代码补全、GDB 调试适配器 |
| C/C++ Extension Pack | 推荐,插件包,包含基础插件、CMake 工具、Doxygen 支持 |
| CMake Tools | 自动识别 CMakeLists.txt,一键配置、编译和调试 |
12.3 配置文件说明
VS Code调试的自动化通过 .vscode 文件夹的 JSON 配置文件实现:
| 配置文件 | 用途 |
|---|---|
tasks.json |
调用编译器生成可执行文件 |
launch.json |
调用调试器运行生成的文件 |
c_cpp_properties.json |
配置 IntelliSense,消除代码红波浪线 |
12.4 tasks.json 配置
json
{
"version": "2.0.0",
"tasks": [
{
"label": "C/C++: g++ build",
"type": "cppbuild",
"command": "/usr/bin/g++",
"args": [
"-g",
"${file}",
"-o",
"${fileDirname}/${fileBasenameNoExtension}"
],
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
12.5 launch.json 配置
json
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug C++ Program",
"type": "cppdbg",
"request": "launch",
"program": "${fileDirname}/${fileBasenameNoExtension}",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "为 gdb 启用整齐打印",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
"preLaunchTask": "C/C++: g++ build",
"miDebuggerPath": "/usr/bin/gdb"
}
]
}
12.6 调试工具栏
| 快捷键 | 按钮 | 功能 |
|---|---|---|
| F5 | Continue | 继续运行,直到遇到下一个断点 |
| F10 | Step Over | 逐过程。跳过当前行 |
| F11 | Step Into | 逐语句。钻进函数内部 |
| Shift+F11 | Step Out | 跳出。回到上一层函数 |
12.7 调试面板信息
变量(Variables):
Local显示当前作用域下的所有变量- 可以直接双击变量值进行修改!
监视(Watch):
- 点击
+号手动添加要监控的变量 - 跨作用域持续显示(不可见时显示
not available)
调用堆栈(Call Stack):
- 显示函数调用层级
- 点击堆栈层级,编辑器自动跳转到对应源代码位置
12.8 调试控制台
底部"调试控制台"可以:
- 输入变量名直接查看值
- 输入简单 C++ 表达式计算
- 直接跟 GDB 对话 :输入
-exec <gdb命令>
bash
# 示例 GDB 命令
-exec x/16xb &my_struct # 查看内存原始数据
-exec set scheduler-locking on # 多线程调试锁定
-exec info sharedlibrary # 查看库依赖
12.9 VS Code 调试常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 断点变成"空心圆" | 缺少调试符号/代码被优化 | 检查 -g 和 -O0 |
| 变量显示 "optimized out" | 开启了优化 | 确保 -O0,或用 volatile 声明变量 |
| 无法进入标准库源码 | 缺少调试版标准库 | sudo apt install libstdc++6-12-dbg |
| 调试需要 sudo 的程序 | 权限不足 | 使用 gdbserver 而非直接调试 |
| 找不到可执行文件 | 路径问题 | 使用 ${workspaceFolder} 绝对路径 |
| std::cin 无法输入 | 调试控制台不支持程序输入 | externalConsole: true 或用终端面板 |
13.信号处理与异常捕获
13.1 信号基础
Linux信号是进程间通信和异常处理的重要机制。GDB默认会拦截大多数信号:
bash
# 查看所有信号的处理方式
(gdb) info signals
(gdb) info handle # 同上
# Signal Stop Print Pass to program Description
# SIGSEGV Yes Yes No Segmentation fault
# SIGINT Yes Yes No Interrupt
# SIGPIPE No No Yes Broken pipe
13.2 自定义信号处理
bash
# handle <signal> <actions>
# stop/nostop: 收到信号时是否暂停
# print/noprint: 是否打印信号信息
# pass/nopass: 是否传递给程序
# 让 SIGPIPE 不被 GDB 拦截(常见于网络编程)
(gdb) handle SIGPIPE nostop noprint pass
# 捕获 SIGUSR1 用于调试
(gdb) handle SIGUSR1 stop print pass
# 忽略 SIGALRM(定时器信号频繁触发时)
(gdb) handle SIGALRM nostop noprint pass
13.3 主动发送信号
bash
# 向程序发送信号
(gdb) signal SIGUSR1 # 发送信号并继续运行
(gdb) signal 10 # 用编号发送(SIGUSR1=10)
# 注意:signal 命令会继续运行程序
# 如果只想发送信号但保持暂停,使用:
(gdb) call kill(getpid(), SIGUSR1)
13.4 C++ 异常调试
bash
# 在异常抛出时暂停
(gdb) catch throw
# 在异常被 catch 时暂停
(gdb) catch catch
# 在特定类型的异常抛出时暂停
(gdb) catch throw std::runtime_error
# 查看当前异常
(gdb) info exceptions # 需要 GDB 10+ 版本
14.自定义命令与 .gdbinit 配置
14.1 gdbinit 文件
.gdbinit 是GDB的配置文件,GDB启动时自动执行其中的命令。可以放在用户主目录(全局)或项目目录(项目级)。
全局配置 ~/.gdbinit:
gdb
# 基础设置
set print pretty on # 美化结构体输出
set print array on # 美化数组输出
set print array-indexes on # 显示数组索引
set print object on # 显示 C++ 对象的实际类型
set print vtbl on # 显示虚函数表
set print demangle on # C++ 名称还原
set pagination off # 关闭分页(长输出不暂停)
set confirm off # 关闭确认提示
# 历史记录
set history save on
set history size 10000
set history filename ~/.gdb_history
# 安全设置(允许加载项目级 .gdbinit)
set auto-load safe-path /
项目级配置 项目目录/.gdbinit:
gdb
# 自动设置断点
break main
break fatal_error
# 设置源码搜索路径
directory src/
directory include/
# 自定义函数
define start_debug
break main
run
end
define show_status
printf "=== Debug Status ===\n"
info threads
printf "\n=== Current Frame ===\n"
info locals
printf "\n=== Backtrace ===\n"
backtrace 5
end
安全提示 :GDB 默认不允许自动加载项目目录的
.gdbinit,需要在全局~/.gdbinit中添加set auto-load safe-path /或指定信任路径。
14.2 自定义命令(define)
bash
# 定义简单命令
(gdb) define ls
Type commands for ls, one per line.
End with a line saying just "end".
>shell ls -la
>end
# 定义带参数的命令
(gdb) define memcheck
>printf "Checking memory at %s\n", $arg0
>x/16xb $arg0
>end
# 使用
(gdb) memcheck 0x7fffffffe000
14.3 Hook 命令
Hook允许在特定命令执行前后自动插入自定义操作:
bash
# 在每次 continue 前打印当前行号
(gdb) define hook-continue
>printf "Continuing from line %d\n", $lineno
>end
# 在每次 step 后打印变量值
(gdb) define hookpost-step
>print i
>end
# 在 backtrace 前显示线程信息
(gdb) define hook-backtrace
>info threads
>end
14.4 常用gdbinit实用配置集合
gdb
# ===== 输出美化 =====
set print pretty on
set print array on
set print array-indexes on
set print union on
set print demangle on
set print object on
set print static-members on
set print vtbl on
set print sevenbit-strings off
# ===== 调试体验 =====
set pagination off
set confirm off
set print elements 0 # 不限制字符串/数组显示长度
set max-value-size unlimited # 不限制变量值大小
# ===== 历史记录 =====
set history save on
set history size 100000
set history filename ~/.gdb_history
# ===== 自定义命令 =====
define reload
kill
file $arg0
run
end
define regs
info registers
end
define ctx
printf "====== Context ======\n"
where
printf "\n--- Locals ---\n"
info locals
printf "\n--- Args ---\n"
info args
end
document ctx
Show current debugging context: backtrace, locals and args.
end
15.GDB Python脚本编程
15.1 Python脚本基础
GDB从7.0版本开始内嵌 Python 解释器,允许使用Python编写强大的调试脚本:
bash
# 在 GDB 中执行 Python 代码
(gdb) python print("Hello from Python")
# 执行多行 Python 代码
(gdb) python
>import gdb
>frame = gdb.selected_frame()
>print("Current function:", frame.name())
>print("Current line:", frame.find_sal().line)
>end
# 加载 Python 脚本文件
(gdb) source my_script.py
15.2 使用GDB Python API
python
import gdb
class PrintLocalVars(gdb.Command):
"""Print all local variables with their types."""
def __init__(self):
super().__init__("plv", gdb.COMMAND_DATA)
def invoke(self, arg, from_tty):
frame = gdb.selected_frame()
block = frame.block()
print("=" * 60)
print(f"Function: {frame.name()}")
print(f"File: {frame.find_sal().symtab.filename}")
print(f"Line: {frame.find_sal().line}")
print("=" * 60)
while block is not None:
for symbol in block:
if symbol.is_variable or symbol.is_argument:
try:
val = symbol.value(frame)
print(f" {symbol.type} {symbol.name} = {val}")
except gdb.error:
print(f" {symbol.type} {symbol.name} = <unavailable>")
block = block.superblock
PrintLocalVars()
在GDB中使用:
bash
(gdb) source print_locals.py
(gdb) plv
# ============================================================
# Function: compute_result
# File: result.c
# Line: 42
# ============================================================
# int count = 10
# double total = 3.14159
# char *name = 0x5555555592a0 "hello"
15.3 Pretty Printer------美化打印
Pretty Printer可以为自定义类型提供友好的输出格式:
python
import gdb
class MyStringPrinter:
"""Pretty printer for MyString type."""
def __init__(self, val):
self.val = val
def to_string(self):
try:
length = int(self.val['length_'])
data_ptr = self.val['data_']
return data_ptr.string('utf-8', length=length)
except gdb.error:
return "<invalid MyString>"
def display_hint(self):
return "string"
def my_string_lookup(val):
if str(val.type) == 'MyString':
return MyStringPrinter(val)
return None
gdb.pretty_printers.append(my_string_lookup)
使用效果:
bash
# 不使用 pretty printer
(gdb) print my_str
$1 = {length_ = 5, data_ = 0x5555555592a0 "hello", capacity_ = 16}
# 使用 pretty printer
(gdb) print my_str
$1 = "hello"
15.4 自定义断点(Python)
python
import gdb
class MemoryLeakBreakpoint(gdb.Breakpoint):
"""Breakpoint that tracks malloc/free calls."""
allocations = {}
def __init__(self):
# 在 malloc 和 free 处设断点
super().__init__("malloc", type=gdb.BP_BREAKPOINT)
self.silent = True
def stop(self):
# 获取 malloc 的参数和返回值
size = int(gdb.parse_and_eval("$rdi"))
# 将在 finish 时捕获返回值
print(f"malloc({size}) called from {gdb.selected_frame().older().name()}")
return False # 不暂停程序
MemoryLeakBreakpoint()
15.5 实用Python脚本示例
批量打印链表:
python
import gdb
def print_linked_list(head_name):
"""遍历并打印链表的所有节点"""
head = gdb.parse_and_eval(head_name)
node = head
count = 0
while node != 0:
count += 1
print(f"Node {count}:")
print(f" data = {node['data']}")
print(f" next = {node['next']}")
node = node['next'].dereference() if node['next'] else 0
if count > 1000:
print("... (truncated, possible cycle)")
break
print(f"Total nodes: {count}")
# 注册为 GDB 命令
class PrintLinkedList(gdb.Command):
def __init__(self):
super().__init__("pll", gdb.COMMAND_DATA)
def invoke(self, arg, from_tty):
print_linked_list(arg)
PrintLinkedList()
16.常用调试实战案例
16.1 案例一:定位段错误
c
// bug_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char* name;
int age;
} Person;
void print_person(Person* p) {
printf("Name: %s, Age: %d\n", p->name, p->age); // 可能崩溃
}
int main() {
Person* people = malloc(3 * sizeof(Person));
people[0].name = strdup("Alice");
people[0].age = 30;
free(people[0].name);
people[0].name = NULL; // 悬空指针
print_person(&people[0]); // 访问 NULL 指针 → 段错误
return 0;
}
bash
$ g++ -g -O0 -o bug_demo bug_demo.cpp
$ gdb ./bug_demo
(gdb) run
# Program received signal SIGSEGV, Segmentation fault.
# 0x00007f... in strlen () from /lib/x86_64-linux-gnu/libc.so.6
(gdb) bt
#0 0x00007f... in strlen ()
#1 0x00007f... in printf ()
#2 0x0000555555555195 in print_person (p=0x5555555592a0) at bug_demo.cpp:10
#3 0x00005555555551f4 in main () at bug_demo.cpp:22
(gdb) frame 2
(gdb) print p->name
$1 = 0x0 # name 是 NULL 指针!
(gdb) print p->age
$2 = 30
结论 :p->name 为 NULL,printf 中 %s 格式化 NULL 指针导致段错误。
16.2 案例二:调试多线程死锁
c
// deadlock_demo.cpp
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mutex_a = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex_b = PTHREAD_MUTEX_INITIALIZER;
void* thread1_func(void* arg) {
pthread_mutex_lock(&mutex_a); // 先锁 A
printf("Thread 1: locked mutex_a\n");
sleep(1);
pthread_mutex_lock(&mutex_b); // 再锁 B → 等待 Thread 2 释放 B
printf("Thread 1: locked mutex_b\n");
pthread_mutex_unlock(&mutex_b);
pthread_mutex_unlock(&mutex_a);
return NULL;
}
void* thread2_func(void* arg) {
pthread_mutex_lock(&mutex_b); // 先锁 B
printf("Thread 2: locked mutex_b\n");
sleep(1);
pthread_mutex_lock(&mutex_a); // 再锁 A → 等待 Thread 1 释放 A
printf("Thread 2: locked mutex_a\n");
pthread_mutex_unlock(&mutex_a);
pthread_mutex_unlock(&mutex_b);
return NULL;
}
bash
$ g++ -g -O0 -pthread -o deadlock_demo deadlock_demo.cpp
$ gdb ./deadlock_demo
(gdb) run
# 程序挂起...
# Ctrl+C 中断
(gdb) thread apply all bt
# Thread 3 (Thread 0x7f... "deadlock_demo"):
# #0 pthread_mutex_lock ...
# #1 thread2_func at deadlock_demo.cpp:22
# Thread 2 (Thread 0x7f... "deadlock_demo"):
# #0 pthread_mutex_lock ...
# #1 thread1_func at deadlock_demo.cpp:11
# Thread 1 等待 mutex_b,Thread 2 等待 mutex_a → 典型死锁
16.3 案例三:查找内存泄漏
c
// leak_demo.cpp
#include <stdlib.h>
void process_data(int n) {
for (int i = 0; i < n; i++) {
char* buffer = (char*)malloc(1024); // 每次分配 1KB
// 忘记 free(buffer)!
}
}
int main() {
process_data(1000); // 泄漏 1MB
return 0;
}
bash
# 方法一:使用 ASan + LSan
$ g++ -g -O0 -fsanitize=address -o leak_demo leak_demo.cpp
$ ./leak_demo
# =================================================================
# ==12345==ERROR: LeakSanitizer: detected memory leaks
# Direct leak of 1024000 byte(s) in 1000 object(s) allocated from:
# #0 0x55555555517a in malloc (.../asan_interceptors.cc:...)
# #1 0x000055555555519c in process_data leak_demo.cpp:5
# 方法二:使用 Valgrind
$ valgrind --leak-check=full ./leak_demo
# ==12345== 1,024,000 bytes in 1,000 blocks are definitely lost in loss record 1 of 1
# ==12345== at 0x4C29F73: malloc (vg_replace_malloc.c:309)
# ==12345== by 0x10898C: process_data (leak_demo.cpp:5)
16.4 案例四:条件断点优化循环调试
c
// 需要在第 1000 次循环时暂停
for (int i = 0; i < 10000; i++) {
if (data[i] == -1) {
// 某个元素被错误地设为 -1
data[i] = 0;
}
}
bash
# 方法一:条件断点(i == 999 时暂停)
(gdb) break process.cpp:12 if i == 999
# 方法二:忽略前 N 次命中
(gdb) break process.cpp:12
(gdb) ignore 1 999 # 忽略前 999 次
# 方法三:使用 watchpoint 监视数据变化
(gdb) watch data[500] # 监视特定元素
16.5 案例五:调试共享库问题
bash
# 查看已加载的共享库
(gdb) info sharedlibrary
# 设置共享库搜索路径
(gdb) set solib-search-path /opt/mylib/lib:/usr/local/lib
# 在共享库函数设断点
(gdb) break malloc # 系统库函数
(gdb) break mylib::process # 自定义库函数
# 查看共享库的符号表
(gdb) info functions malloc
(gdb) info variables my_global # 查找全局变量
17.附录 A GDB命令速查表
17.1 启动与退出
| 命令 | 说明 |
|---|---|
gdb program |
调试可执行文件 |
gdb program core |
调试 core dump |
gdb -p PID |
附加到运行中的进程 |
gdb --args prog arg1 arg2 |
带参数启动 |
gdb -tui prog |
TUI 模式启动 |
quit / q |
退出 GDB |
17.2 运行控制
| 命令 | 缩写 | 说明 |
|---|---|---|
run |
r |
运行程序 |
start |
--- | 运行到 main |
continue |
c |
继续运行 |
step |
s |
单步进入 |
next |
n |
单步跳过 |
stepi |
si |
指令级进入 |
nexti |
ni |
指令级跳过 |
finish |
--- | 运行到函数返回 |
until |
u |
跳出循环 |
kill |
k |
终止程序 |
17.3 断点
| 命令 | 说明 |
|---|---|
break func |
函数断点 |
break file:line |
行断点 |
break if cond |
条件断点 |
tbreak |
临时断点 |
rbreak regex |
正则断点 |
watch var |
写观察断点 |
rwatch var |
读观察断点 |
awatch var |
读写观察断点 |
catch throw |
捕获异常 |
catch syscall |
捕获系统调用 |
info break |
查看断点 |
delete N |
删除断点 |
disable N |
禁用断点 |
enable N |
启用断点 |
ignore N count |
忽略 N 次 |
condition N expr |
设置条件 |
commands N |
绑定命令 |
17.4 数据查看
| 命令 | 说明 |
|---|---|
print expr |
打印表达式 |
print/x expr |
十六进制打印 |
display expr |
持续显示 |
whatis var |
查看类型 |
ptype var |
详细类型 |
x/NFU addr |
检查内存 |
info locals |
局部变量 |
info args |
函数参数 |
set var = val |
修改变量 |
17.5 栈帧
| 命令 | 说明 |
|---|---|
backtrace / bt |
调用栈 |
bt full |
调用栈+变量 |
frame N |
切换帧 |
up / down |
上下移帧 |
info frame |
帧信息 |
17.6 多线程
| 命令 | 说明 |
|---|---|
info threads |
查看线程 |
thread N |
切换线程 |
thread apply all cmd |
所有线程执行 |
set scheduler-locking on |
锁定调度 |
17.7 反向调试
| 命令 | 说明 |
|---|---|
record |
开始记录 |
reverse-step |
反向单步进入 |
reverse-next |
反向单步跳过 |
reverse-continue |
反向继续 |
reverse-finish |
反向到调用处 |
set exec-direction reverse |
设置反向执行 |
17.8 源码与汇编
| 命令 | 说明 |
|---|---|
list / l |
查看源码 |
disassemble |
查看汇编 |
disassemble func |
函数汇编 |
info source |
源文件信息 |
directory dir |
添加源码路径 |
18.GCC编译选项速查
18.1 常用编译选项
| 参数 | 说明 |
|---|---|
-v / --version |
查看 gcc 版本号 |
-I 目录 |
指定头文件目录 |
-c |
只编译,生成 .o 文件,不进行链接 |
-g |
包含调试信息,用于支持 gdb 调试 |
-g3 |
包含宏定义等更详细的调试信息 |
-On (n=0~3) |
编译优化,n 越大优化得越多 |
-Wall |
提示更多警告信息 |
-D<DEF> |
编译时定义宏 |
-E |
生成预处理文件 |
-o |
指定输出文件名 |
18.2 库文件编译
bash
# 静态库制作
gcc -c add.c -o add.o
ar rcs lib库名.a add.o sub.o mul.o
# 静态库使用
gcc testlib.c libmymath.a -o test
# 动态库制作
gcc -c add.c -o add.o -fPIC
gcc -shared -o lib库名.so add.o sub.o mul.o
# 动态库使用
gcc testlib.c -o a.out -lmymath -L ./lib
ldd a.out # 查看依赖的链接库
19.总结
GDB是一个功能极其丰富的调试工具,本文涵盖了从基础到高级的主要功能:
- 基础操作:启动、运行控制、源码查看------这是日常使用最频繁的部分
- 断点体系:普通断点、条件断点、观察断点、捕捉断点------精准控制暂停时机
- 数据观察:print、x、display------深入理解程序状态
- 栈帧分析:backtrace、frame------追踪函数调用链
- 多线程/多进程:thread、scheduler-locking------应对并发调试
- 内存问题:ASan、Valgrind 配合------排查段错误和内存泄漏
- Core Dump:崩溃现场分析------事后定位问题
- 反向调试:record、reverse-*------时光倒流追溯问题
- 远程调试:gdbserver------嵌入式与容器化场景
- 脚本编程:Python API、.gdbinit------自动化提升效率
- 图形化调试:VS Code 集成------可视化调试体验
调试的终极原则:用最短的时间定位问题。掌握 GDB 的高级功能不是为了炫技,而是为了在面对复杂 bug 时拥有足够的工具储备。当 printf 调试法力不从心时,GDB 的这些技巧就是你最可靠的武器。
参考资料