96、网络调试技巧:strace跟踪系统调用、gdb调试网络程序

网络程序出问题了,怎么办?

说实话,我见过太多人一上来就改代码、加日志、重新编译。折腾半天,问题还在。其实,很多时候根本不需要动代码——用对工具,一眼就能看出问题在哪。

今天聊两个我几乎每天都会用的调试利器:stracegdb。一个管系统调用,一个管程序内部。配合好了,网络问题基本无处遁形。

strace:看穿你的程序在干啥

strace 是什么?说白了,它就是个「偷窥器」。它能把你程序发起的每一次系统调用都记录下来——调用了什么函数、传了什么参数、返回了什么结果。

我习惯这么用:

# 跟踪正在运行的进程
strace -p 12345

# 跟踪新启动的程序
strace -f -o trace.log ./my_server

# 只看网络相关的系统调用
strace -e trace=network -p 12345

嗯,这里要注意:-f 参数很重要。如果你的程序用了多线程或 fork 子进程,不加 -f 只能看到主线程的调用,子线程的会被漏掉。我曾经因为这个排查了整整一个下午,最后发现是子线程的 connect 失败了,但 strace 根本没记录到。

实战:用 strace 定位 connect 超时

有一次,我写了一个客户端程序,连接服务器时总是卡住十几秒才报错。代码看起来没问题,我怀疑是网络环境的问题。

跑一下 strace:

strace -e trace=connect,recvfrom,sendto ./my_client

输出结果:

connect(3, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("192.168.1.100")}, 16) = -1 EINPROGRESS (Operation now in progress)
... 等待了 10 秒 ...
connect(3, ...) = -1 ETIMEDOUT (Connection timed out)

看到 EINPROGRESS 了吗?这说明 socket 是非阻塞模式。非阻塞 connect 返回后,程序应该用 select/poll 去检查连接状态。但我的代码里没有做这个检查,直接又调了一次 connect——结果就超时了。

问题找到了,不是网络问题,是代码逻辑问题。strace 帮我省了至少两小时的抓包时间。

strace 常用过滤选项:

  • -e trace=network:只看网络相关调用(connect、accept、send、recv 等)
  • -e trace=file:只看文件操作(open、read、write 等)
  • -e trace=signal:只看信号处理
  • -e read=fd:显示指定文件描述符的读取内容
  • -e write=fd:显示指定文件描述符的写入内容

gdb:钻进程序内部看状态

strace 能告诉你「程序做了什么」,但如果你想看「程序为什么这么做」,就得用 gdb 了。

gdb 调试网络程序,我个人最常用的几个场景:

  1. 断点调试:在 connect、accept、recv 等函数处下断点,看参数对不对
  2. 查看 socket 状态:用 print 命令看 errno、socket 选项
  3. 分析崩溃:程序 core dump 了,用 gdb 看调用栈

举个例子,调试一个 TCP 服务器:

# 启动 gdb 并附加到运行中的进程
gdb -p 12345

# 在 accept 处下断点
(gdb) b accept

# 继续运行
(gdb) c

# 当断点触发时,查看参数
(gdb) p sockfd
$1 = 3
(gdb) p (struct sockaddr_in *)&addr
$2 = (struct sockaddr_in *) 0x7fffffffe2a0
(gdb) p *(struct sockaddr_in *)0x7fffffffe2a0
$3 = {sin_family = 2, sin_port = htons(8080), sin_addr = {s_addr = 16777343}}

你看,客户端的 IP 和端口一目了然。如果发现 sin_port 是 0,那说明 accept 之前 socket 没 bind 好——这种低级错误我犯过一次,后来再也没忘过。

gdb 调试多线程网络程序

多线程网络程序调试起来比较麻烦。我常用的几个命令:

# 查看所有线程
(gdb) info threads

# 切换到指定线程
(gdb) thread 2

# 给指定线程下断点
(gdb) b thread_func thread 2

# 查看线程的调用栈
(gdb) bt

有一次,我的服务器程序在高并发下偶尔会崩溃。用 gdb 分析 core dump 文件,发现是线程 A 在释放一个 socket 连接对象的同时,线程 B 还在往这个 socket 里写数据。典型的竞态条件。gdb 的 info threadsbt 让我在五分钟内锁定了问题代码。

注意:gdb 调试多线程程序时,默认只在断点处暂停当前线程,其他线程继续运行。如果想暂停所有线程,需要设置:

(gdb) set scheduler-locking on

否则你看到的「全局状态」可能是不一致的。

strace + gdb 组合拳

这两个工具配合使用,效果翻倍。我一般的工作流程:

  1. 先用 strace 跑一遍,看系统调用层面有没有异常(比如 connect 返回 EINPROGRESS、recv 返回 EAGAIN 等)
  2. 如果 strace 看不出问题,再用 gdb 下断点,深入看程序内部状态
  3. 如果怀疑是内存问题(比如缓冲区溢出),用 gdb 的 watch 命令监视变量变化

举个例子,有一次我怀疑 recv 返回的数据长度不对。先用 strace 确认了 recv 确实返回了 1024 字节,但程序里只处理了 512 字节。然后用 gdb 在 recv 调用处下断点,发现是缓冲区指针传错了——传了一个已经偏移过的指针,导致数据被覆盖了。

小技巧:如果你在调试一个频繁创建和销毁 socket 的程序,可以用 strace 的 -e trace=close 看 socket 文件描述符是否被正确关闭。我曾经发现一个程序每处理一个请求就多一个 CLOSE_WAIT 状态的 socket,strace 一看,果然是 close 没被调用。

知识体系图

下面这张图总结了 strace 和 gdb 在网络调试中的定位和关系:

网络调试工具知识体系 网络程序 strace gdb 跟踪系统调用(connect/recv/send) 查看返回值、错误码、耗时 断点调试、查看变量、调用栈 多线程调试、内存分析、core dump strace 看「做了什么」,gdb 看「为什么这么做」

避坑指南

最后,分享几个我踩过的坑:

  • strace 输出太多怎么办?-e trace= 过滤,或者用 -c 参数只看统计摘要。我曾经在一个高并发服务器上跑 strace,结果 strace 本身成了性能瓶颈——输出太多了。
  • gdb 断点不生效? 检查一下是不是优化编译导致的。用 -O0 -g 编译,否则变量可能被优化掉,断点也可能跳来跳去。
  • strace 看不到数据内容?-s 1024 参数,默认只显示 32 字节。我调试 HTTP 协议时经常需要看完整的请求头。
  • gdb 调试子进程?set follow-fork-mode child,否则 gdb 默认只跟踪父进程。

嗯,工具就这些。但说实话,工具只是辅助,真正重要的是思路——先定位问题范围,再选择合适的工具。strace 和 gdb 就像医生的听诊器和 CT 机,一个听个大概,一个看个仔细。配合着用,大部分网络问题都能搞定。


公众号:蓝海资料掘金营,微信 deep3321