强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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

常用命令

程序执行控制

命令缩写说明
runr启动/重启程序
continuec继续执行
nextn单步执行(不进入函数)
steps单步执行(进入函数)
nextini单步执行一条机器指令
stepisi单步执行一条机器指令(进入函数)
finishfin执行到当前函数返回
untilu执行到指定行(跳出循环)
killk终止程序
quitq退出 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

⚠️ 注意点

  1. 编译选项:调试时务必使用 -g -O0,优化后的代码调试体验很差
  2. strip:发布版本用 strip 移除调试信息,但保留一份带符号的用于崩溃分析
  3. 多线程:GDB 默认只停止当前线程,使用 set scheduler-locking on 可锁定其他线程
  4. Core dump 限制:core 文件可能很大,注意磁盘空间
  5. 安全:不要在生产环境长期运行 gdbserver
  6. TUI 模式:终端窗口过小会导致 TUI 显示异常

💡 提示

  1. 快速定位段错误run 后直接看崩溃点,使用 bt 查看调用栈
  2. 记录调试过程set logging on 将 GDB 输出记录到文件
  3. 宏调试:使用 -g3 -gdwarf-2 编译,GDB 可以展开宏
  4. 类型转换print (MyStruct *)ptr 可以强制类型转换
  5. 调用函数:在 GDB 中可以直接调用程序中的函数:call printf("debug: %d\n", x)
  6. 保存断点save breakpoints gdb_bp.txtsource 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

扩展阅读