欢迎光临
我们一直在努力

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> 已在运行 是 是(接管的)
程序崩溃后 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。本质:沿调用栈往上追,看坏状态从哪层开始。

 

赞(0)
未经允许不得转载:171主机测评 » GDB 调试实战:启动调试、附加调试与 core 分析全解析
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址