96、网络调试技巧:strace跟踪系统调用、gdb调试网络程序
网络程序出问题了,怎么办?
说实话,我见过太多人一上来就改代码、加日志、重新编译。折腾半天,问题还在。其实,很多时候根本不需要动代码——用对工具,一眼就能看出问题在哪。
今天聊两个我几乎每天都会用的调试利器:strace 和 gdb。一个管系统调用,一个管程序内部。配合好了,网络问题基本无处遁形。
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 调试网络程序,我个人最常用的几个场景:
- 断点调试:在 connect、accept、recv 等函数处下断点,看参数对不对
- 查看 socket 状态:用
print命令看 errno、socket 选项 - 分析崩溃:程序 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 threads 和 bt 让我在五分钟内锁定了问题代码。
注意:gdb 调试多线程程序时,默认只在断点处暂停当前线程,其他线程继续运行。如果想暂停所有线程,需要设置:
(gdb) set scheduler-locking on
否则你看到的「全局状态」可能是不一致的。
strace + gdb 组合拳
这两个工具配合使用,效果翻倍。我一般的工作流程:
- 先用 strace 跑一遍,看系统调用层面有没有异常(比如 connect 返回 EINPROGRESS、recv 返回 EAGAIN 等)
- 如果 strace 看不出问题,再用 gdb 下断点,深入看程序内部状态
- 如果怀疑是内存问题(比如缓冲区溢出),用 gdb 的
watch命令监视变量变化
举个例子,有一次我怀疑 recv 返回的数据长度不对。先用 strace 确认了 recv 确实返回了 1024 字节,但程序里只处理了 512 字节。然后用 gdb 在 recv 调用处下断点,发现是缓冲区指针传错了——传了一个已经偏移过的指针,导致数据被覆盖了。
小技巧:如果你在调试一个频繁创建和销毁 socket 的程序,可以用 strace 的 -e trace=close 看 socket 文件描述符是否被正确关闭。我曾经发现一个程序每处理一个请求就多一个 CLOSE_WAIT 状态的 socket,strace 一看,果然是 close 没被调用。
知识体系图
下面这张图总结了 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