4、GDB调试实战:GDB基础使用、断点与单步调试、多线程调试、Core Dump分析
GDB,说白了就是嵌入式开发者的“照妖镜”。代码跑飞了、段错误了、逻辑不对了,你总不能全靠printf去猜吧?我刚开始做Tina Linux项目时,遇到一个诡异的死锁问题,加了二十多个打印都没定位到,最后用GDB单步跟了一遍,五分钟就找到了问题。嗯,从那以后,我再也不敢轻视GDB了。
这一章,咱们就直奔主题。我会把GDB最常用的三板斧教给你:基础操作、断点单步、多线程调试,还有最实用的Core Dump分析。你跟着走一遍,以后遇到问题至少知道从哪里下手。
4.1 GDB基础使用:启动、附加与基本命令
在Tina Linux环境下,GDB的使用场景主要有两种:一种是直接调试一个正在运行的进程,另一种是调试一个即将启动的程序。我个人更常用的是附加到进程的方式,因为很多问题是在程序跑起来之后才暴露的。
4.1.1 启动GDB的三种方式
| 方式 | 命令 | 适用场景 |
|---|---|---|
| 直接调试程序 | gdb ./myapp |
程序刚启动就出问题 |
| 附加到运行进程 | gdb -p <PID> |
程序运行中卡死或异常 |
| 调试Core Dump | gdb ./myapp core |
程序崩溃后分析现场 |
这里有个坑,我刚开始用的时候踩过:附加到进程时,目标程序必须是用-g编译的。否则你看到的全是汇编,根本没法跟源码对应。Tina Linux的编译选项里,记得加上 -g -O0,-O0是为了防止编译器优化导致代码跳来跳去。
CFLAGS += -g -O0 -DDEBUG。发布版本再去掉-g,避免固件体积过大。
4.1.2 常用基础命令速查
你不需要记住GDB的所有命令,记住下面这几个就够了。我平时80%的调试场景,靠它们就能搞定。
- run / r:启动程序,可以带参数,比如
r arg1 arg2 - continue / c:继续运行,直到下一个断点
- next / n:单步跳过(不进入函数内部)
- step / s:单步步入(进入函数内部)
- print / p:打印变量值,比如
p my_var - backtrace / bt:查看调用栈
- list / l:查看源码上下文
- quit / q:退出GDB
举个例子,假设你怀疑某个全局变量被意外修改了:
(gdb) p global_flag
$1 = 0
(gdb) watch global_flag
Hardware watchpoint 1: global_flag
(gdb) c
Continuing.
Hardware watchpoint 1: global_flag
Old value = 0
New value = 1
main() at test.c:45
45 if (global_flag) {
看到了吗?watch命令直接帮你抓到了是谁改了这个变量。我在项目中定位过一个内存越界问题,就是用watch找到的——一个野指针恰好踩到了关键标志位。
4.2 断点与单步调试:精准定位问题
断点不是随便打的。我见过很多新手一上来就在main函数打一个断点,然后一路next到底。这样效率太低了。好的做法是:先想清楚问题可能出在哪,再在可疑位置下断点。
4.2.1 断点的几种玩法
| 断点类型 | 命令示例 | 说明 |
|---|---|---|
| 行号断点 | b test.c:42 |
在源文件第42行停下 |
| 函数断点 | b my_function |
进入函数时停下 |
| 条件断点 | b test.c:42 if count > 5 |
满足条件才停下 |
| 临时断点 | tb test.c:42 |
只停一次,自动删除 |
条件断点特别实用。比如你有一个循环执行1000次,只在第500次时出错。你总不能按500次next吧?直接 b loop.c:10 if i == 500,GDB会自动帮你跳过前499次。
4.2.2 单步调试的节奏感
单步调试不是机械地按n或s。我总结了一个“三步法”:
- 先看调用栈:用bt确认当前在哪个函数、谁调了它
- 再看关键变量:用p打印怀疑的变量,确认值是否符合预期
- 然后决定下一步:如果函数内部逻辑复杂,用s进去看;如果只是赋值操作,用n跳过
举个例子,你发现某个函数的返回值不对:
(gdb) b process_data
Breakpoint 1 at 0x1234: file app.c, line 80.
(gdb) c
Continuing.
Breakpoint 1, process_data (buf=0xbeff1234, len=1024) at app.c:80
80 int result = validate(buf, len);
(gdb) n
81 if (result < 0) {
(gdb) p result
$2 = -1
(gdb) s # 进入validate函数看看为什么返回-1
validate (buf=0xbeff1234, len=1024) at app.c:120
120 if (buf[0] != HEADER_MAGIC) {
看到了吗?通过单步,你很快发现是数据包头部的魔数不对。这时候你就能顺着这个线索往前查——是谁传了错误的数据进来。
4.3 多线程调试:别让线程“打架”
多线程调试是GDB的进阶用法,也是很多人的噩梦。我记得有一次,一个Tina项目里两个线程同时操作一个链表,导致段错误。用GDB的多线程调试功能,我很快就锁定了问题。
4.3.1 线程相关命令
| 命令 | 作用 |
|---|---|
info threads |
列出所有线程及其状态 |
thread <ID> |
切换到指定线程 |
thread apply all bt |
查看所有线程的调用栈 |
set scheduler-locking on |
单步时只运行当前线程 |
这里重点说一下 scheduler-locking。默认情况下,你单步调试一个线程时,其他线程还在跑。这会导致你根本没法跟踪——你刚停住线程A,线程B已经把数据改了。设置成on之后,只有当前线程能执行,其他线程全部暂停。这样你才能看清楚每一步发生了什么。
thread apply all bt 看所有线程在干什么,然后切换到可疑线程,设置 scheduler-locking on,单步跟踪变量修改的路径。
4.3.2 死锁定位技巧
死锁的典型表现是程序卡住不动,CPU占用率很低。这时候用GDB附加到进程:
(gdb) thread apply all bt
Thread 2 (Thread 0xb6f00470):
#0 __lll_lock_wait () at .../lowlevellock.c:46
#1 _L_lock_854 () from /lib/libpthread.so.0
#2 pthread_mutex_lock (mutex=0x1234) at pthread_mutex_lock.c:78
#3 worker_thread (arg=0x0) at app.c:200
Thread 1 (Thread 0xb6f23470):
#0 __lll_lock_wait () at .../lowlevellock.c:46
#1 _L_lock_854 () from /lib/libpthread.so.0
#2 pthread_mutex_lock (mutex=0x5678) at pthread_mutex_lock.c:78
#3 main_thread (arg=0x0) at app.c:150
看到两个线程都在等锁,而且等的是不同的锁——这就是典型的死锁。接下来用 p mutex 查看锁的地址,再配合代码分析锁的获取顺序,就能找到问题。
4.4 Core Dump分析:事故现场回放
Core Dump是程序崩溃时系统保存的内存快照。说白了,就是事故现场的“黑匣子”。我最喜欢用Core Dump来排查段错误,因为它能直接告诉你程序死在了哪一行。
4.4.1 开启Core Dump
在Tina Linux上,默认可能没开Core Dump。你需要手动设置:
# 查看当前限制
ulimit -c
# 设置不限制大小
ulimit -c unlimited
# 设置Core文件路径(可选)
echo "/tmp/core-%e-%p-%t" > /proc/sys/kernel/core_pattern
这里 %e 是程序名,%p 是PID,%t 是时间戳。这样生成的Core文件不会重名,方便管理。
4.4.2 分析Core Dump的步骤
拿到Core文件后,分析流程很固定:
- 加载Core文件:
gdb ./myapp /tmp/core-myapp-1234-5678 - 查看崩溃位置:GDB会自动停在崩溃的那一行,或者用
bt看调用栈 - 查看变量值:用
p打印局部变量和参数 - 查看内存:用
x/10x &buf查看内存内容
举个例子,一个典型的段错误分析:
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x00012345 in process_packet (pkt=0x0) at network.c:85
85 memcpy(dst, pkt->data, pkt->len);
(gdb) p pkt
$1 = (struct packet *) 0x0
(gdb) bt
#0 process_packet (pkt=0x0) at network.c:85
#1 0x00012456 in handle_event (ev=0xbeff1234) at event.c:42
#2 0x00012567 in main_loop () at main.c:100
看到了吗?pkt 是空指针,导致 memcpy 访问了非法地址。调用栈显示是 handle_event 传进来的。接下来你只需要去 event.c:42 看看为什么传了空指针进来。
4.4.3 没有源码也能分析
有时候你拿到的Core文件是客户现场的,没有源码。这时候可以用 disassemble 看汇编,结合寄存器值来定位:
(gdb) info registers
r0: 0x00000000 # 这就是pkt的值,是0
r1: 0xbeff5678 # dst的地址
pc: 0x00012345 # 崩溃的指令地址
(gdb) disassemble $pc-16, $pc+16
Dump of assembler code from 0x12335 to 0x12355:
0x12344: ldr r2, [r0, #4] # 从r0+4取数据,r0是0,所以段错误
0x12348: ldr r1, [r0, #8] # 同上
0x1234c: bl memcpy
从汇编可以确认,r0是0,导致取数据时访问了地址0。虽然没有源码,但你已经知道是哪个函数、哪个指令出了问题。配合符号表(如果有),还能反推出源码位置。
好了,GDB调试的实战内容就这些。从基础命令到断点单步,再到多线程和Core Dump,每一步都是我在项目中反复用过的。你把这些练熟了,Tina Linux上90%的软件问题都能自己搞定。