GDB调试技巧与指令完全指南---个人学习篇

文章目录

  • [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

阅读顺序(从下往上):

  1. #0 :确认"死因"------compute 函数中 a 为 NULL 指针,这很可能就是段错误的原因
  2. #1 :找到"谁把你送走的"------调用者 process,传入了 data 指针
  3. #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()                  # 调用自定义调试函数

注意callprint 调用的函数中对全局变量和静态变量的修改,在函数返回后会被恢复到调用前的值。这是 GDB 的安全机制,确保调试操作不会意外改变程序状态。

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 多线程死锁排查实战

死锁产生的原因

  1. 加锁顺序不当:线程 A 持有资源 1 想获取资源 2,线程 B 持有资源 2 想获取资源 1
  2. 重复加锁:同一线程对非递归锁重复加锁
  3. 加锁后未解锁:异常导致锁未释放

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是一个功能极其丰富的调试工具,本文涵盖了从基础到高级的主要功能:

  1. 基础操作:启动、运行控制、源码查看------这是日常使用最频繁的部分
  2. 断点体系:普通断点、条件断点、观察断点、捕捉断点------精准控制暂停时机
  3. 数据观察:print、x、display------深入理解程序状态
  4. 栈帧分析:backtrace、frame------追踪函数调用链
  5. 多线程/多进程:thread、scheduler-locking------应对并发调试
  6. 内存问题:ASan、Valgrind 配合------排查段错误和内存泄漏
  7. Core Dump:崩溃现场分析------事后定位问题
  8. 反向调试:record、reverse-*------时光倒流追溯问题
  9. 远程调试:gdbserver------嵌入式与容器化场景
  10. 脚本编程:Python API、.gdbinit------自动化提升效率
  11. 图形化调试:VS Code 集成------可视化调试体验

调试的终极原则:用最短的时间定位问题。掌握 GDB 的高级功能不是为了炫技,而是为了在面对复杂 bug 时拥有足够的工具储备。当 printf 调试法力不从心时,GDB 的这些技巧就是你最可靠的武器。

参考资料

相关推荐
亚林瓜子3 小时前
AWS Glue Python Shell任务中pip安装依赖库
python·shell·pip·aws·glue·job
亚林瓜子3 小时前
AWS Glue Python Shell任务中读取Athena数据库
数据库·python·shell·aws·glue·athena
SilentSamsara1 天前
Linux磁盘与存储管理:分区、LVM 与 IO 性能全栈分析
linux·运维·服务器·ssh·shell
pluvium274 天前
记对 xonsh shell 的使用, 脚本编写, 迁移及调优
linux·python·shell·xonsh
ShineWinsu7 天前
对于Linux:文件操作以及文件IO的解析
linux·c++·面试·笔试·io·shell·文件操作
Cyber4K8 天前
【Shell专项】循环及交互的使用
linux·shell
kali-Myon15 天前
CTFshow-Pwn142-Off-by-One(堆块重叠)
c语言·数据结构·安全·gdb·pwn·ctf·
vangie16 天前
你还在手敲长命令?这个 Shell 插件帮你自动提醒别名和现代替代工具
shell·命令行
哈里谢顿19 天前
服务器操作卡顿问题解决
shell