GDB 调试实战:启动调试、附加调试与 core 分析全解析

发布时间:2026/11/2 11:18:11
GDB 调试实战:启动调试、附加调试与 core 分析全解析

前置知识

一、启用 GDB 调试(编译期前提)

1. 编译必须带 -g

  • 作用:生成调试信息(变量名、行号、类型映射)
  • 没有 -g:GDB 只能看到机器指令地址,看不到源码行号和变量名
g++ -g -O0 -o app main.cpp
  • -g:生成调试信息
  • -O0:不优化,变量不会被优化掉,info locals才能看到值

2. 用 CMake 构建时,记得显式指定构建类型

cmake -DCMAKE_BUILD_TYPE=Debug .. # 自动 -g -O0 cmake -DCMAKE_BUILD_TYPE=Release .. # 自动 -O3 -DNDEBUG(调试困难) make

调试用 Debug,发布用 Release。

3. Makefile

CFLAGS = -Wall -g CXXFLAGS = -Wall -g

二、启用 core dump(崩溃时留下现场)

Linux 默认不生成 core(ulimit -c为 0)。三步:开限制、设路径、关拦截。

第一步:开发板上开启 core 大小限制

ulimit -c # 查看,0 表示禁止生成 ulimit -c unlimited # 临时开启(当前终端及其子进程有效)

.....剩下的用到了询问 ai 即可

要点:

一、三种调试模式对比

方式命令进程状态是否 attachGDB 控制活进程
启动调试gdb ./app+run由 GDB 启动否是(自己启动的)
附加调试gdb -p <pid>/attach <pid>已在运行是是(接管的)
程序崩溃后 coregdb ./app core已崩溃否(只读 core)否(无活进程)

补充:完整崩溃定位还有dmesg + addr2line这条不用 GDB 的路径,适合“没 core、不能复现”的场景。

二、三种模式的本质区别

  • 启动调试:GDB --fork/exec--> 被调试进程(GDB 是父进程,被调试进程是子进程),GDB 控制进程的创建和运行。
  • 附加调试:GDB 使用-p或attach接管已运行进程,GDB 接管一个“别人启动的”活进程。
  • 程序崩溃后 core:GDB --加载文件--> core 文件(进程已死),GDB 只读内存快照,不控制任何进程。

关键:

  • 前两种 GDB 都控制一个进程(启动的 or 接管的)
  • 第三种 GDB 不控制任何进程,只分析文件

三、每种模式什么时候用

场景用哪种理由
开发阶段,自己跑程序复现 bug启动调试能从头控制、设断点、单步
板端程序已跑起来,卡住/CPU 高/内存涨附加调试不能重启(会丢现场),只能接入看
程序崩了,有 core 文件程序崩溃后 core进程已死,只能分析快照
程序崩了,没 core,但能复现启动调试 + run复现崩溃,bt 看栈
程序崩了,没 core,不能复现dmesg + addr2line用 ip 地址反查源码行

四、三种调试方法的写法

方法 1:启动调试

gdb ./app (gdb) run (gdb) run --device /dev/video0 # 带参数启动

说明:GDB 自己 fork+exec 启动被调试进程,GDB 是父进程。能从头设断点、单步、控制运行。

方法 2:附加调试(两种入口,效果相同)

# 写法 A:启动时直接指定 pid gdb -p 12345 写法 B:先启动 gdb,再 attach gdb (gdb) attach 12345

说明:GDB 接管一个已在运行的活进程(ptrace PTRACE_ATTACH)。两种入口效果完全一样,只是命令行入口不同。附加后进程默认暂停,看完用detach让进程继续,不要 kill。

方法 3:事后 core 分析

gdb ./app core # 或 gdb ./app core.12345

说明:进程已崩溃,GDB 只加载 core 内存快照文件进行分析。不 attach 任何活进程,不控制进程,只读文件。

五、调用栈与三个核心命令(三种模式通用)

调用栈:

  • 每调用一个函数,栈上压一帧(frame),函数返回弹出
  • 帧编号从 0 开始:#0= 当前正在执行 / 崩溃发生的函数(最顶层),数字越大越靠外层调用者,最底层是 main

bt / backtrace

  • 作用:打印整个调用栈(谁调了谁、崩在哪行)
  • 用途:崩溃后第一件事,看调用链

frame N

  • 作用:把视角切到第 N 层,看那一层的现场
  • 用途:根因常在上层(上层传了坏参数),必须切上去看
  • 注意:GDB 默认在 #0,变量检查只能看当前帧

info locals

  • 作用:一次性列出当前帧所有局部变量及值
  • 用途:快速看函数内状态,比逐个 p 快
  • 注意:只能看当前帧,要先frame N切过去
  • 相关:info args看参数,info globals看全局变量

六、三者配合流程(崩溃定位标准动作)

# 1. 崩溃后,先看调用栈 (gdb) bt #0 memcpy () #1 capture_frame (buf=0x0, size=614400) at v4l2_capture.cpp:88 #2 capture_thread () at v4l2_capture.cpp:150 2. 当前在 #0,看不到 capture_frame 的局部变量,切到 #1 (gdb) frame 1 #1 capture_frame (buf=0x0, size=614400) at v4l2_capture.cpp:88 3. 看 capture_frame 的局部变量 (gdb) info locals frame = 0x7fff... ret = -1 buf = 0x0 <- 找到根因:buf 是空指针 4. 看源码第 88 行前后,确认 buf 为什么是 0 (gdb) list

结论:memcpy 往空指针写 → 段错误,根因是 buf 没分配成功(mmap 失败没判空)。

七、附加调试结合 Linux 进程的四个场景

场景 1:进程卡住,看它卡在哪

ps aux | grep capture_app # 找 PID gdb -p <pid> # 附加 (gdb) info threads # 看所有线程状态 (gdb) thread 1 (gdb) bt # 看该线程卡在哪个函数 (gdb) frame 1 (gdb) info locals # 看卡住时变量值

场景 2:CPU 100%,看哪个线程空转

top -H -p <pid> # 按线程看 CPU gdb -p <pid> (gdb) info threads (gdb) thread <高 CPU 线程号> (gdb) bt # 定位到 spin_wait 等函数 (gdb) thread apply all bt # 一次看所有线程栈(核心命令)

场景 3:内存一直涨

  • GDB 附加后用call malloc_stats()看堆统计
  • 真正泄漏定位靠 ASan(-fsanitize=address)或 Valgrind(x86)
  • GDB 用途:看可疑对象被谁持有
(gdb) p some_object->buffer (gdb) p some_object->ref_count

场景 4:崩溃但没 core

dmesg | tail -20 # 看 segfault 地址 # capture_app[1234]: segfault at 0 ip 0000000000401234 ... addr2line -e capture_app -f -C 0x401234 # 根据 ip 地址反查源码行 # 输出:capture_frame /path/v4l2_capture.cpp:88 # 能复现则 gdb ./app 跑,崩后 bt

八、活进程调试与事后调试的区别

对比项事后调试(core)活进程(attach 附加调试)
栈的含义崩溃那一刻当前这一刻
多线程崩溃线程的栈所有线程都有栈
bt 范围默认当前线程默认当前线程,thread apply all bt看全部
变量值崩溃时的值活进程当前值(可能被其他线程改)
附加后状态已死默认暂停,需 continue 恢复
退出quit 即可detach 让进程继续,quit 退出

关键:附加调试时 GDB 暂停整个进程,bt 看的是暂停那一刻的快照。看完用detach,不要 kill 活进程。

九、完整流程:活进程多线程卡死排查

# 1. 进程在 ps aux | grep capture_app top -H -p <pid> # 看线程,某线程 0% CPU 卡住 2. 附加 gdb -p <pid> 3. 看所有线程,一次看全部栈 (gdb) info threads (gdb) thread apply all bt 4. 选可疑线程 (gdb) thread 2 (gdb) bt #0 pthread_mutex_lock () #1 encoder::encode () at mpp_h264_encoder.cpp:200 (gdb) frame 1 (gdb) info locals mutex_ = 0x... # 等这个锁 5. 找持锁线程 (gdb) thread 3 (gdb) bt #0 sleep () #1 capture_thread () at capture.cpp:80 <- 持锁却在 sleep,死锁 6. 进程继续跑 (gdb) detach (gdb) quit

结论:线程 2 等锁,线程 3 持锁却 sleep → 死锁。
修复:调整锁的持有范围,别在持锁时 sleep。

十、常用命令速查

命令说明
info threads列出所有线程
thread N切到第 N 个线程
bt当前线程调用栈
thread apply all bt所有线程调用栈(多线程排查核心)
frame N切到栈第 N 层
info locals当前帧所有局部变量
info args当前帧函数参数
p var打印变量
list看当前源码
detach脱离进程,让活进程继续
continue让暂停的进程继续

十一、常见误区

  • 只在 #0 看变量 → 根因常在上层,要 frame 切上一层
  • frame 后忘了自己在哪层 → frame 会打印当前帧号
  • info locals 看到空 → 被 -O2 优化掉,用 -O0/-O1 编译,egg:编译使用g++ -g -O2 -o app main.cpp
  • 以为 bt 的 #0 是 main → #0 是当前/崩溃帧,数字越大越靠外层
  • 附加后直接 quit 杀掉进程 → 先 detach 让程序继续运行,再 quit 杀死 GDB 进程
  • 事后 core 当成 attach → core 分析不碰活进程,只读文件

十二、一句话总纲

GDB 三种模式:启动调试(GDB 启动进程)、附加调试(attach 活进程)、事后 core 分析(只读崩溃快照,不碰活进程)。前两种 GDB 控制活进程,第三种只分析文件。补充dmesg + addr2line应对“没 core、不能复现”。

核心命令:bt打印整个调用栈;frame N切到第 N 层;info locals列当前帧局部变量。三者顺序配合:bt 看全景 → 发现可疑层 → frame N 切过去 → info locals 看那层变量 → 找到根因(常是上层传了坏参数)。

附加调试扩展:info threads→thread N→bt→frame N→info locals。多线程用thread apply all bt一次看全,快速发现“谁等锁、谁持锁、谁空转、谁阻塞 read”。附加调试记得 detach 而不是 kill。本质:沿调用栈往上追,看坏状态从哪层开始。