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 即可
要点:
一、三种调试模式对比
| 方式 | 命令 | 进程状态 | 是否 attach | GDB 控制活进程 |
|---|---|---|---|---|
| 启动调试 | gdb ./app+run | 由 GDB 启动 | 否 | 是(自己启动的) |
| 附加调试 | gdb -p <pid>/attach <pid> | 已在运行 | 是 | 是(接管的) |
| 程序崩溃后 core | gdb ./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。本质:沿调用栈往上追,看坏状态从哪层开始。