C/C++ Linux 开发教程(GCC + CMake) / GDB 调试实战
GDB 调试实战
GDB(GNU Debugger)是 Linux 平台最强大的调试工具。本文从基础命令到高级技巧,全面讲解 GDB 调试方法。
启动 GDB
基本启动方式
# 编译时必须包含调试信息
gcc -g -O0 -Wall buggy.c -o buggy
# 方式 1: 直接加载可执行文件
gdb ./buggy
# 方式 2: 附加到运行中的进程
gdb -p <PID>
# 或
gdb attach <PID>
# 方式 3: 调试带参数的程序
gdb --args ./buggy arg1 arg2 arg3
# 方式 4: 调试 core dump 文件
gdb ./buggy core.12345
# 方式 5: 远程调试
gdb -ex "target remote localhost:1234" ./buggy
GDB 启动配置文件
# ~/.gdbinit — GDB 启动时自动执行
cat > ~/.gdbinit << 'EOF'
# 启用颜色
set confirm off
set pagination off
set history save on
set history filename ~/.gdb_history
# 美化输出
set print pretty on
set print array on
set print array-indexes on
# C++ 相关
set print object on
set print vtbl on
set demangle-style gnu-v3
# 安全
set disable-randomization on
EOF
常用命令
程序执行控制
| 命令 | 缩写 | 说明 |
|---|
run | r | 启动/重启程序 |
continue | c | 继续执行 |
next | n | 单步执行(不进入函数) |
step | s | 单步执行(进入函数) |
nexti | ni | 单步执行一条机器指令 |
stepi | si | 单步执行一条机器指令(进入函数) |
finish | fin | 执行到当前函数返回 |
until | u | 执行到指定行(跳出循环) |
kill | k | 终止程序 |
quit | q | 退出 GDB |
信息查看
# 查看源代码
list # 显示当前位置周围的源码
list main # 显示 main 函数
list buggy.c:42 # 显示指定文件的第 42 行
list 1,100 # 显示 1-100 行
# 查看变量
print x # 打印变量值
print *ptr # 打印指针指向的值
print arr[0]@10 # 打印数组前 10 个元素
print /x var # 以十六进制打印
print /t var # 以二进制打印
# 查看调用栈
backtrace # 显示调用栈(bt)
backtrace full # 显示调用栈及局部变量
frame 3 # 切换到第 3 帧
info locals # 查看当前帧的局部变量
info args # 查看当前帧的参数
# 查看寄存器
info registers # 查看所有寄存器
info registers rax # 查看指定寄存器
# 查看内存
x/10xw 0x7fffffffdd40 # 以十六进制查看 10 个 word
x/s 0x4005b0 # 查看字符串
x/20i $pc # 查看当前位置的 20 条指令
GDB 命令格式化输出
# 通用格式: x/[数量][格式][大小] 地址
# 格式: o(八进制), x(十六进制), d(十进制), u(无符号), t(二进制), f(浮点), a(地址), i(指令), c(字符), s(字符串)
# 大小: b(字节), h(半字/2字节), w(字/4字节), g(巨字/8字节)
# 示例
x/10dw &arr # 十进制查看 10 个 word
x/5xg $rsp # 十六进制查看栈顶 5 个 8 字节值
x/20i $pc # 反汇编当前 20 条指令
断点管理
普通断点
# 在函数处设断点
break main
break buggy.c:42
# 条件断点
break buggy.c:42 if x == 10
break buggy.c:42 if strlen(name) > 5
break buggy.c:42 if i == j
# 临时断点(命中一次后自动删除)
tbreak main
tbreak buggy.c:42
# 查看所有断点
info breakpoints # 或 info break
# 删除断点
delete 1 # 删除 1 号断点
delete # 删除所有断点
clear buggy.c:42 # 删除指定行的断点
# 禁用/启用断点
disable 1 # 禁用 1 号断点
enable 1 # 启用 1 号断点
# 忽略断点前 N 次命中
ignore 1 10 # 忽略 1 号断点的前 10 次命中
数据断点(Watchpoint)
# 监视变量变化(写入时触发)
watch global_var
watch *0x601040
# 监监视变量被读取或写入时触发
rwatch global_var
# 监视变量被访问时触发(读或写)
awatch global_var
# 查看监视点
info watchpoints
捕获点(Catchpoint)
# 捕获 C++ 异常
catch throw
catch catch
# 捕获系统调用
catch syscall open
catch syscall mmap
# 捕获 fork/exec
catch fork
catch exec
catch vfork
# 捕获信号
catch signal SIGSEGV
catch signal SIGABRT
断点命令列表
# 在断点命中时自动执行命令
break buggy.c:42
commands
silent
printf "x = %d, y = %d\n", x, y
continue
end
# 实用模式: 记录变量值但不停住
break buggy.c:42
commands
silent
printf "iteration %d: sum = %ld\n", i, sum
continue
end
调试核心转储
配置 Core Dump
# 启用 core dump
ulimit -c unlimited
# 设置 core 文件名模式(包含 PID 和时间)
echo "core.%e.%p.%t" | sudo tee /proc/sys/kernel/core_pattern
# 查看当前设置
ulimit -a | grep "core file"
cat /proc/sys/kernel/core_pattern
生成和分析 Core Dump
// crash.c — 故意制造崩溃
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void crash_function(char *input) {
char buf[10];
strcpy(buf, input); // 缓冲区溢出!
printf("buf = %s\n", buf);
}
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <string>\n", argv[0]);
return 1;
}
int *ptr = NULL;
// 只在特定条件下崩溃
if (strlen(argv[1]) > 5) {
crash_function(argv[1]);
}
*ptr = 42; // 空指针解引用
return 0;
}
# 编译并运行(生成 core dump)
gcc -g -O0 -Wall crash.c -o crash
./crash "this_is_too_long"
# Segmentation fault (core dumped)
# 使用 GDB 分析 core dump
gdb ./crash core.crash.*
# 在 GDB 中
(gdb) bt # 查看崩溃时的调用栈
(gdb) frame 0 # 切换到栈顶帧
(gdb) info locals # 查看局部变量
(gdb) print input # 查看参数值
(gdb) info registers # 查看寄存器状态
Core Dump 排查技巧
# 查看系统 core dump 信息
coredumpctl list # systemd 系统
coredumpctl info <PID>
coredumpctl gdb <PID> # 直接启动 GDB
# 使用 GDB 脚本自动分析
cat > analyze_core.gdb << 'EOF'
bt full
info registers
x/10i $pc
thread apply all bt full
EOF
gdb -batch -x analyze_core.gdb ./crash core.crash.*
多线程调试
线程信息查看
# 查看所有线程
info threads
# 输出示例:
# Id Target Id Frame
# * 1 Thread 0x7ffff7fc1740 (LWP 1234) "app" main () at app.c:20
# 2 Thread 0x7ffff77bf700 (LWP 1235) "app" worker () at worker.c:15
# 3 Thread 0x7ffff6fbd700 (LWP 1236) "app" timer () at timer.c:30
# 切换到指定线程
thread 2
# 查看当前线程的调用栈
bt
# 查看所有线程的调用栈
thread apply all bt
# 查看所有线程的调用栈和局部变量
thread apply all bt full
# 只查看所有线程的调用栈(简洁版)
thread apply all bt 3
线程特定断点
# 在特定线程命中断点
break worker.c:15 thread 2
break worker.c:15 thread 2 if result < 0
# 查看断点信息
info breakpoints
多线程调试实战
// thread_demo.c
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#define NUM_THREADS 4
typedef struct {
int id;
int *shared_counter;
pthread_mutex_t *mutex;
} ThreadArg;
void *worker(void *arg) {
ThreadArg *targ = (ThreadArg *)arg;
for (int i = 0; i < 5; i++) {
pthread_mutex_lock(targ->mutex);
(*targ->shared_counter)++;
printf("Thread %d: counter = %d\n", targ->id, *targ->shared_counter);
pthread_mutex_unlock(targ->mutex);
usleep(100000);
}
return NULL;
}
int main() {
pthread_t threads[NUM_THREADS];
ThreadArg args[NUM_THREADS];
int counter = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
for (int i = 0; i < NUM_THREADS; i++) {
args[i].id = i;
args[i].shared_counter = &counter;
args[i].mutex = &mutex;
pthread_create(&threads[i], NULL, worker, &args[i]);
}
for (int i = 0; i < NUM_THREADS; i++) {
pthread_join(threads[i], NULL);
}
printf("Final counter: %d\n", counter);
return 0;
}
# 编译时需要链接 pthread
gcc -g -O0 -Wall thread_demo.c -lpthread -o thread_demo
# GDB 调试
gdb ./thread_demo
# 设置线程相关的断点
(gdb) break worker.c:15 thread 2
(gdb) run
(gdb) info threads
(gdb) thread 2
(gdb) print *targ
(gdb) thread apply all bt
GDB TUI 模式
TUI 基本使用
# 启动 TUI 模式
gdb -tui ./buggy
# 在 GDB 中切换 TUI 模式
(gdb) tui enable # 启用 TUI
(gdb) tui disable # 禁用 TUI
(gdb) layout src # 源码布局
(gdb) layout asm # 汇编布局
(gdb) layout split # 源码 + 汇编
(gdb) layout regs # 源码 + 寄存器
# TUI 窗口切换
(gdb) focus next # 切换焦点到下一个窗口
(gdb) focus prev # 切换焦点到上一个窗口
(gdb) focus cmd # 焦点切到命令窗口
# 滚动源码窗口
(gdb) winheight src +5 # 增加源码窗口高度
(gdb) winheight src -5 # 减少源码窗口高度
(gdb) refresh # 刷新屏幕
(gdb) update # 更新源码位置
TUI 快捷键
| 快捷键 | 说明 |
|---|
Ctrl+x a | 切换 TUI 模式 |
Ctrl+x 1 | 单窗口模式 |
Ctrl+x 2 | 双窗口模式 |
Ctrl+x o | 切换窗口焦点 |
Ctrl+l | 刷新屏幕 |
Ctrl+p | 上一条命令 |
Ctrl+n | 下一条命令 |
GDB Python 扩展
Python 脚本基础
# gdb_helper.py
import gdb
class BreakpointLogger(gdb.Breakpoint):
"""记录断点命中次数和时间的自定义断点类"""
def __init__(self, spec):
super().__init__(spec)
self.hit_count = 0
self.silent = True
def stop(self):
self.hit_count += 1
frame = gdb.selected_frame()
func = frame.name()
line = frame.find_sal().line
print(f"[Breakpoint] #{self.hit_count} at {func}:{line}")
return False # 不停止执行
# 使用方法
# (gdb) source gdb_helper.py
# (gdb) python BreakpointLogger("main.c:42")
自定义 GDB 命令
# gdb_commands.py
import gdb
class DumpArrayCommand(gdb.Command):
"""打印数组内容"""
def __init__(self):
super().__init__("dump-array", gdb.COMMAND_DATA)
def invoke(self, arg, from_tty):
args = gdb.string_to_argv(arg)
if len(args) < 2:
print("Usage: dump-array VAR COUNT")
return
var_name = args[0]
count = int(args[1])
val = gdb.parse_and_eval(var_name)
for i in range(count):
elem = val[i]
print(f"[{i}] = {elem}")
DumpArrayCommand()
# 使用方法
# (gdb) source gdb_commands.py
# (gdb) dump-array arr 10
Pretty Printer
# pretty_printers.py
import gdb
class VectorPrinter:
"""打印 std::vector 内容"""
def __init__(self, val):
self.val = val
def to_string(self):
size = int(self.val['_M_impl']['_M_finish'] -
self.val['_M_impl']['_M_start'])
capacity = int(self.val['_M_impl']['_M_end_of_storage'] -
self.val['_M_impl']['_M_start'])
return f"std::vector (size={size}, capacity={capacity})"
def children(self):
start = self.val['_M_impl']['_M_start']
finish = self.val['_M_impl']['_M_finish']
i = 0
while start != finish:
yield ('[%d]' % i, start.dereference())
start += 1
i += 1
def register_printers(obj):
gdb.printing.register_pretty_printer(
obj, VectorPrinter
)
# (gdb) source pretty_printers.py
# (gdb) python register_printers(gdb.current_objfile())
GDB 与 CMake 集成
Debug 构建配置
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyApp C CXX)
# Debug 模式: 启用调试信息,禁用优化
set(CMAKE_C_FLAGS_DEBUG "-g -O0 -Wall -Wextra -DDEBUG")
set(CMAKE_CXX_FLAGS_DEBUG "-g -O0 -Wall -Wextra -DDEBUG")
# Release 模式: 启用优化
set(CMAKE_C_FLAGS_RELEASE "-O2 -DNDEBUG")
set(CMAKE_CXX_FLAGS_RELEASE "-O2 -DNDEBUG")
add_executable(app main.c)
# 构建 Debug 版本
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
# 调试
gdb ./build/app
CMake 调试信息增强
# 启用地址消毒器
if(ENABLE_ASAN)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fno-omit-frame-pointer")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fsanitize=address")
endif()
# 启用线程消毒器
if(ENABLE_TSAN)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=thread")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=thread")
endif()
cmake -B build -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON
远程调试(gdbserver)
基本远程调试
# 在目标机器上启动 gdbserver
gdbserver localhost:1234 ./app
# 或者附加到已运行的进程
gdbserver --attach localhost:1234 <PID>
# 在开发机器上连接
gdb ./app
(gdb) target remote 192.168.1.100:1234
(gdb) break main
(gdb) continue
跨架构远程调试
# 目标机器(ARM)
# 安装 gdbserver
apt install gdbserver
# 启动调试
gdbserver :1234 ./app_arm
# 开发机器
# 安装交叉调试器
apt install gdb-multiarch
# 连接并指定目标架构
gdb-multiarch ./app_arm
(gdb) set architecture arm
(gdb) target remote 192.168.1.100:1234
(gdb) break main
(gdb) continue
SSH 隧道远程调试
# 建立 SSH 隧道
ssh -L 1234:localhost:1234 user@remote-host
# 在远程机器上启动 gdbserver
gdbserver localhost:1234 ./app
# 在本地连接(通过隧道)
gdb ./app
(gdb) target remote localhost:1234
GDB 调试技巧
调试动态加载的库
# 设置共享库搜索路径
(gdb) set solib-search-path /path/to/libs
# 查看已加载的共享库
(gdb) info sharedlibrary
# 在共享库加载时停止
(gdb) set stop-on-solib-events 1
调试优化代码
# 编译时同时启用优化和调试信息
gcc -O2 -g -fno-omit-frame-pointer main.c -o main
# GDB 调试优化代码的技巧:
# 1. 变量可能被优化掉 → 使用 print registers
# 2. 代码可能被重排 → 使用 layout asm
# 3. 内联函数 → 使用 info frame
反向调试(Reverse Debugging)
# 录制执行过程
(gdb) target record-full
(gdb) run
# 反向执行
(gdb) reverse-continue # 反向继续
(gdb) reverse-next # 反向单步
(gdb) reverse-step # 反向步入
(gdb) reverse-finish # 反向跳出函数
# ⚠️ 注意: 反向调试仅支持单线程程序,且会显著降低运行速度
检查内存问题
# 检查内存泄漏(使用 Valgrind 配合 GDB)
valgrind --vgdb=yes --vgdb-error=0 ./app
# 在另一个终端
gdb ./app
(gdb) target remote | vgdb
# 使用 GDB 检查堆内存
(gdb) print malloc_usable_size(ptr)
(gdb) info proc mappings
⚠️ 注意点
- 编译选项:调试时务必使用
-g -O0,优化后的代码调试体验很差 - strip:发布版本用
strip 移除调试信息,但保留一份带符号的用于崩溃分析 - 多线程:GDB 默认只停止当前线程,使用
set scheduler-locking on 可锁定其他线程 - Core dump 限制:core 文件可能很大,注意磁盘空间
- 安全:不要在生产环境长期运行 gdbserver
- TUI 模式:终端窗口过小会导致 TUI 显示异常
💡 提示
- 快速定位段错误:
run 后直接看崩溃点,使用 bt 查看调用栈 - 记录调试过程:
set logging on 将 GDB 输出记录到文件 - 宏调试:使用
-g3 -gdwarf-2 编译,GDB 可以展开宏 - 类型转换:
print (MyStruct *)ptr 可以强制类型转换 - 调用函数:在 GDB 中可以直接调用程序中的函数:
call printf("debug: %d\n", x) - 保存断点:
save breakpoints gdb_bp.txt 和 source gdb_bp.txt 可以持久化断点
工程场景
场景 1:调试生产环境崩溃
# 步骤 1: 收集信息
ulimit -c unlimited
# 重现崩溃,获取 core dump
# 步骤 2: 分析 core dump
gdb /usr/local/bin/app core.app.*
(gdb) bt full
(gdb) info registers
(gdb) thread apply all bt
# 步骤 3: 使用 debuginfo 包
# Debian/Ubuntu
apt install app-dbgsym
# CentOS/RHEL
debuginfo-install app
场景 2:调试内存错误
# 使用 AddressSanitizer + GDB
gcc -g -O0 -fsanitize=address -fno-omit-frame-pointer membug.c -o membug
ASAN_OPTIONS="abort_on_error=1" gdb ./membug
(gdb) run
# ASAN 报告错误后
(gdb) bt
场景 3:调试死锁
# 在 GDB 中调试死锁的程序
gdb ./app <PID>
(gdb) info threads
(gdb) thread apply all bt
# 找到等待锁的线程,分析锁的持有关系
(gdb) thread 2
(gdb) print mutex.__data.__owner
扩展阅读