2. GDB基础回顾:GDB核心命令速览、如何用GDB attach到正在运行的进程
说实话,很多做后端的朋友对GDB的印象还停留在「调试段错误」上。但在我眼里,GDB是定位网络粘包、拆包问题的利器。你想想看,当你的服务在线上跑得好好的,突然某个连接发来的数据包被错误地拆分或合并了——这时候你没法停服务,也没法加日志重新编译。怎么办?
嗯,这时候GDB的attach功能就派上用场了。它能直接挂到正在运行的进程上,让你现场看内存、看变量、看堆栈。我个人习惯在排查网络问题时,先用GDB attach上去,看看接收缓冲区里的数据到底长什么样。
2.1 GDB核心命令速览
先过一遍最常用的命令。这些命令我几乎每天都在用,尤其是调试网络服务的时候。
| 命令 | 缩写 | 作用 | 我常用的场景 |
|---|---|---|---|
| break | b | 设置断点 | 在recv()或read()函数上打断点,观察每次读取的数据量 |
| continue | c | 继续执行 | 看完一次数据后,让程序继续跑 |
| next | n | 单步跳过 | 跳过函数调用,只看当前函数的逻辑 |
| step | s | 单步进入 | 进入recv()内部,看它怎么处理缓冲区的 |
| 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) 提示符,说明你已经成功挂上去了。这时候程序会暂停在当前位置。
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
这样程序会继续正常运行,不受影响。
2.3 知识体系:GDB调试网络问题的核心逻辑
下面这张图是我自己总结的,每次排查粘包拆包问题时,我都会按这个流程走一遍。
这张图把整个调试流程串起来了。从找到进程开始,到attach、设断点、看缓冲区、分析数据,最后detach退出。我曾经按照这个流程,帮同事定位过一个诡异的拆包问题——客户端每次发1024字节,但服务端recv()有时只收到512字节。用GDB attach上去一看,发现是TCP的Nagle算法在作怪。
watch -n 1 'ps aux | grep my_server',确保进程状态正常。
好了,GDB的基础回顾就到这里。记住,attach调试是线上排查的「核武器」,用得好能快速定位问题,用不好可能引发事故。我个人建议先在测试环境多练几次,把命令练熟了再上生产。