怎么使用GDB快速定位问题?

作者: | 更新日期:

看到很多人不怎么会使用GDB, 这里简单记录一下我的实践.

本文首发于公众号:天空的代码世界,微信号:tiankonguse

大家好, 这里是tiankonguse的公众号(tiankonguse-code).
tiankonguse曾是一名ACMer, 现在是鹅长视频部门的后台开发.
这里主要记录工作中的技术架构与经验,计算机相关的技术,数学、算法、生活上好玩的东西
是的, 这里一般只讲技术, 不讲其他的.

这篇文章从公众号tiankonguse-code自动同步过来.
如果转载请加上署名:公众号tiankonguse-code,并附上公众号二维码,谢谢。

零、定位问题的两大神器

作为c/c++后台开发, 平常开发服务的时候,避免不了程序挂掉.
比如这周四, 我接手的微博中转在测试环境就出现问题了, 于是使用两大神器慢慢的解开了问题的面纱.

  1. 第一神器: printf 或者 LOG
  2. 第二神器: GDB

printf和LOG由于太强大了, 这里就不多说了.
这里来说说第二个神器GDB吧.

两年前这个时候已经写过gdb 学习记录(文章底部点击原文可以查看连接).
看到很多人不怎么使用GDB, 这里介绍我常用的一些命令, 然后介绍一下我的实践.
如果你们有GDB的疑问, 或者不知道怎么定位程序core的话, 可以留言, 我会提出一些建议.

一、基本实践

基本用法由于google上到处都是, 这里就不细说了, 只简单提一下.
这里不会讲这些基本操作.

  • 程序挂到GDB gdb [exe] [PID|core]
  • 设置命令行参数
  • 基本的打断点 break file:line|file:function,info breakpoints,delete
  • 看源码 directory path,list
  • 启动程序 run,next,continue
  • 函数操作 step,finish
  • 当前堆栈 where, bt
  • 打印 print

二、实践开始

GDB稍微复杂的命令如果掌握几个, 可以快速定位到程序的问题.

一般程序core了, 能马上找到对应的行数, 然后我们直接看源代码看那里为什么错误就能解决问题了.
但是有时候我们的程序是主动退出的, 如调用exit退出的.

对应的代码可能大量的调用了exit.
这个时候很有效的方式是exit上打断点b exit,然后堆栈移动up, 然后就可以快速找到问题了where.
一种更好的方法是直接结束exit函数return,然后对后面的单步调试.

三. 挂在哪了

有时候我们不知道程序挂在哪了.
对于这种情况, 可以使用信号断点handle Signals stop, 然后堆栈移动frame|up|down到正常的函数里即可.
这个方法也可以用于定位因为使用STL导致堆栈全是问号的问题.

四. 异常值

有时候我们会发现某个变量出现了非预期的值, 想看看是什么时候变的.
很多初学者会直接一路next下去, 如果没有循环还好, 逻辑复杂了循环多了, 将会耗费大量的人力与时间.
我们可以利用watch|rwatch|awatch三个变量名来帮助我们快速定位到具体位置.

五. 多线程调试

多线程问题一般是竞争资源没加锁导致的, 这篇文章不讲这个话题.

这里说说多线程常用的场景.

info thread用于显示线程信息
thread <ID> 切换到某个线程
break filename:line thread ID 给某个线程设置断点
set scheduler-locking off|on|step 其他线程都停止
set follow-fork-mode child是子进程的,这里也提一下.

六、宏

有时候我们希望看看宏的值, 但是宏预处理时就没了.
所以我们编译的时候需要告诉编译器保留宏信息-ggdb3.
然后就可以使用info macro 查看宏了.
当然也可以使用macro expand展开宏.

七、变量

有时候我们在GDB里需要进行逻辑运算, 复杂了就需要定义几个变量.
GDB中变量名以$开始.
比如set $index = 0.
如果要修改程序的变量,则可以直接set var name=value.

八. 自动查看变量

对于逻辑复杂的程序, 我们单步调试时可能会频繁的输出某些变量的值.
此时我们可以使用display命令来自动打印变量的值.

九、执行函数查询值

比如我们有一个hash函数, 想看看我们指定的参数运算后的值, 就可以手动的调用函数了call fun(argv).

十、我定位出的问题

微博中转有一个进程监听子进程的状态,子进程死了就会重新拉取子进程。
发生的问题是子进程一直在死.

所以我第一步是通过set follow-fork-mode child进入到子进程的.

子进程是主动调用exit退出的, 但是八万行代码, 大量的使用了exit.
所以我在exit上打了断点b exit,然后栈移动up找到在哪行代码调用exit的.

找到程序退出的代码位置后, 开始在对应的地方单步调试next, 查看为什么会进入退出逻辑.
发现调用了一个网络框架代码, 并返回了一个错误码, 于是打印错误码p iRet,然后进入函数step.

进入函数后发现访问的socket为0才返回错误退出的, socket是子进程初始化的时候统一创建的, 所以需要对初始化逻辑单步调试.

初始化逻辑里面找到创建socket的地方, 发现最后一个socket创建失败, 并打印错误码p errno发现为24.

然后查询到24为FD不够了,然后查询进程的FD最大数量cat /proc/num/limits.
查询进程已经使用的fd数量lsof -p num,发现确实使用完了.

阅读网络框架, 发现网络框架存在BUG, 给用户预留的FD比较少, 结果父进程拉起子进程时, 已经使用了很多个了, 子进程就不够了.
于是修改网络框架, 预留多点FD修复问题.

这里我不会告诉你GDB可以像c语言一样使用指针,数组,对象访问, 如p (pInfo->NumList)[index<<1] + *pData.

十一、参考资料

十二、其他文章


长按图片关注公众号, 接受最新文章消息.

本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。

关注公众号,接收最新消息

tiankonguse +
穿越