前置知识
一、启用 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 即可
要点:
一、三种调试模式对比
| 启动调试 | 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
八、活进程调试与事后调试的区别
| 栈的含义 | 崩溃那一刻 | 当前这一刻 |
| 多线程 | 崩溃线程的栈 | 所有线程都有栈 |
| 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。本质:沿调用栈往上追,看坏状态从哪层开始。


