2. GDB基础回顾

说实话,很多做后端的朋友对GDB的印象还停留在「调试段错误」上。但在我眼里,GDB是定位网络粘包、拆包问题的利器。你想想看,当你的服务在线上跑得好好的,突然某个连接发来的数据包被错误地拆分或合并了——这时候你没法停服务,也没法加日志重新编译。怎么办?

嗯,这时候GDB的attach功能就派上用场了。它能直接挂到正在运行的进程上,让你现场看内存、看变量、看堆栈。我个人习惯在排查网络问题时,先用GDB attach上去,看看接收缓冲区里的数据到底长什么样。

2.1 GDB核心命令速览

先过一遍最常用的命令。这些命令我几乎每天都在用,尤其是调试网络服务的时候。

命令 缩写 作用 我常用的场景
break b 设置断点 在recv()或read()函数上打断点,观察每次读取的数据量
continue c 继续执行 看完一次数据后,让程序继续跑
next n 单步跳过 跳过函数调用,只看当前函数的逻辑
step s 单步进入 进入recv()内部,看它怎么处理缓冲区的
print p 打印变量值 打印接收缓冲区的内容,看是不是粘包了
backtrace bt 查看调用栈 看当前线程是从哪个入口进来的
info threads i th 查看所有线程 多线程服务中,看哪个线程在处理哪个连接
thread th 切换线程 切换到处理特定连接的线程
frame f 切换栈帧 看上层调用传了什么参数
list l 查看源代码 确认当前执行到哪一行了
我的小技巧:调试网络程序时,我习惯在 recv() 和 send() 上各打一个断点。这样每次收发数据都会停下来,我可以仔细看缓冲区里的内容。曾经有一次,我就是这样发现客户端发来的数据里多了一个空字节,导致拆包逻辑出错。

2.2 如何用GDB attach到正在运行的进程

这是排查线上问题的核心技能。说白了,就是让GDB「钻」进一个已经在跑的程序里。

2.2.1 找到进程PID

先用 ps 或 pgrep 找到目标进程的PID:

# 假设你的服务叫 my_network_server
pgrep my_network_server
# 输出:12345

或者用 ps aux | grep my_network_server 也能看到。

2.2.2 执行attach

sudo gdb -p 12345

注意,这里用了 sudo。为什么?因为很多线上服务是以普通用户跑的,但GDB attach需要你有足够的权限。我曾经因为忘了加sudo,折腾了十分钟才发现是权限问题。

执行后,GDB会输出类似这样的信息:

GNU gdb (Ubuntu 9.2-0ubuntu1~20.04) 9.2
...
Attaching to process 12345
Reading symbols from /usr/local/bin/my_network_server...
Reading symbols from /usr/local/bin/my_network_server.debug...
0x00007f1234567890 in __GI___recv (fd=3, buf=0x7f..., n=1024, flags=0) at ../sysdeps/unix/sysv/linux/recv.c:28
28      return SYSCALL_CANCEL (recv, fd, buf, n, flags);
(gdb)

看到 (gdb) 提示符,说明你已经成功挂上去了。这时候程序会暂停在当前位置。

注意:attach 上去后,目标进程会暂停执行。如果你是在生产环境操作,一定要快!我一般控制在30秒以内,看完关键信息就 detach 掉。长时间暂停会导致服务超时。

2.2.3 开始调试

挂上去之后,你就可以用前面说的那些命令了。比如:

(gdb) bt
#0  0x00007f1234567890 in __GI___recv (fd=3, buf=0x7f..., n=1024, flags=0) at ../sysdeps/unix/sysv/linux/recv.c:28
#1  0x0000555a12345678 in handle_client (client_fd=3) at server.c:120
#2  0x0000555a12345679 in worker_thread (arg=0x0) at server.c:200
#3  0x00007f1234567891 in start_thread () from /lib/x86_64-linux-gnu/libpthread.so.0
#4  0x00007f1234567892 in clone () from /lib/x86_64-linux-gnu/libc.so.6

看到调用栈了。当前在 recv() 里,上层是 handle_client,再上层是 worker_thread。嗯,典型的线程池模型。

然后我可以看看当前接收缓冲区的内容:

(gdb) p buf
$1 = (void *) 0x7f...
(gdb) p *(char(*)[64])buf
$2 = "GET /api/data HTTP/1.1\r\nHost: ..."

这里我用了类型转换,把 buf 当作一个64字节的字符数组来打印。这样能直接看到收到的数据。如果发现数据里混了多个请求,那就是粘包了。

2.2.4 调试完毕,安全退出

看完之后,记得 detach:

(gdb) detach
Detaching from program: /usr/local/bin/my_network_server, process 12345
(gdb) quit

这样程序会继续正常运行,不受影响。

核心要点:attach 调试的关键是「快进快出」。我一般先想好要看什么,再 attach 上去。比如「我要看第120行附近的 buf 内容」,上去直接设断点、打印、detach,一气呵成。

2.3 知识体系:GDB调试网络问题的核心逻辑

下面这张图是我自己总结的,每次排查粘包拆包问题时,我都会按这个流程走一遍。

GDB调试网络粘包/拆包问题核心流程 步骤1:找到进程PID pgrep / ps aux 步骤2:GDB attach sudo gdb -p PID 步骤3:设置断点 b recv / b send 步骤4:查看缓冲区 p buf / x/64bx buf 步骤5:分析数据 判断是否粘包/拆包 步骤6:detach退出 detach → quit 判断结果 粘包 多个请求混在一个缓冲区里 拆包 一个请求被分成多次接收 正常 数据边界清晰,无异常 修复代码 → 重新编译 → 验证

这张图把整个调试流程串起来了。从找到进程开始,到attach、设断点、看缓冲区、分析数据,最后detach退出。我曾经按照这个流程,帮同事定位过一个诡异的拆包问题——客户端每次发1024字节,但服务端recv()有时只收到512字节。用GDB attach上去一看,发现是TCP的Nagle算法在作怪。

避坑指南:我曾经在生产环境上attach到一个高并发的WebSocket服务上,结果忘了detach就退出了终端。那个进程一直处于暂停状态,导致所有连接超时。后来我学乖了,每次attach之前先在另一个终端开好 watch -n 1 'ps aux | grep my_server',确保进程状态正常。

好了,GDB的基础回顾就到这里。记住,attach调试是线上排查的「核武器」,用得好能快速定位问题,用不好可能引发事故。我个人建议先在测试环境多练几次,把命令练熟了再上生产。