当前位置: 首页 > news >正文

记一次使用Valgrind查找解决内存问题的玄幻旅程

文章目录

  • 前言
  • 玄幻旅途
    • 故事背景
    • 初入泥潭
    • 一片混沌
    • 追根溯源
    • 抽丝剥茧
    • 大海捞针
    • 祭出法宝
    • 屏蔽无关
    • 移形换位
    • 再请法宝
    • 风平浪静
    • 若有所思
  • 参考文章

前言

看题目来说这应该是一篇教程式文章,但为了突出“玄幻”二字,我们不讲细节只讲过程,在过程中体会解决问题的方式和方法,以及避免一些我在这个过程中绕的弯路,如果想找工具的详细使用方法可以去参考文章中翻一翻,有几篇文章写的真不错,下面我们开始扯淡啦。

玄幻旅途

本故事并非虚构,如有雷同,纯属命苦~

故事背景

作为本文主人公的我——小Z,是一个后端C/C++搬运工(这不废话吗,不是C系列谁老倒腾指针和内存?),在一个阳(yue)光(hei)明(feng)媚(gao)的下(wan)午(shang)接到一个完善游戏战斗系统的需求,然后便开始了紧张开发、积极调试的每一天,事情比较顺利,一切都在计划之中,不过平凡的日子总是无趣,没有一点点意外总让人感到有点意外。

初入泥潭

好吧,到目前为止一切都很顺利,服务器各个功能模块分批完成,终于完成了最后拼装,启动调试看结果,出现了一点点逻辑问题,这个战斗过程根本停不下来,整个程序一直在递归,最终导致函数调用栈溢出崩溃。不过这都是小问题,简单梳理逻辑后增加必要的出口判断条件,问题很快被解决。

再次启动调试,程序正常运行,符合预期结果。啥?这就完了,幸福来的有点突然啊,整个流程基本符合需求,只是缺少一些细节逻辑需要补充,感觉胜利就在前方了啊!

一片混沌

补充细节的过程中,也需要不断调试来验证结果,咦?怎么连不上服务器了?查看一下进程,果然服务器进程已经不在了,难道是我不小心关掉了,先记一下,解决掉手头上更重要的问题后再来看它。

上次的问题好几天都没有出现了,可能真的是我不小心把服务器进程关掉了,今天还有个小BUG需要修复一下,先搭建好调试环境准备定位一下问题。整个过程比较顺利,没过几分钟BUG就找到了,修复后调试看看结果,Duang!进程挂了,好在这次是在调试状态,能看到是哪里引发的崩溃,查看函数调用栈来看看是谁捣的鬼。

什么玩意,智能指针出作用域时自动析构挂了?这是什么鬼,从上到下看了一遍近百层的函数调用关系,感觉没什么问题啊,真是奇怪。

重新启动进程,开始了疯狂测试,跑了20几次相同的逻辑,没有任何问题啊,那刚刚发生了什么,转过头来继续看刚刚出现崩溃的位置,完全找不到问题。这个问题先放一放,继续补充细节,调试解决发现的BUG,在多次调试之后,Duang!进程又挂了,这次更离谱,在定义lambda表达式的时候崩了,看着函数调用栈依旧一头雾水,看不出是什么问题。

退出调试状态,重启进程,继续跑了10多次相同的逻辑,这次进程真的崩溃了,看来程序真的是有隐藏的BUG。再次重启,继续跑,这次又不崩溃了,这种状况让人有点头大啊。启动调试状态开始测试,跑了几次就崩溃了,原来和调试有关系呀!经过多次测试发现,如果在调试状态下测试几次就会出现崩溃的情况,如果在非调试状态下大概需要跑10多次才会崩。

为了查出问题便开始在调试状态下更加疯狂的测试,这次真的开了眼了,每次崩溃的位置都不太一样,有的在析构函数中,有的设置变量值时,有的在发送函数中,有的在申请内存时,总体来看基本都是围绕着内存出现的问题,但是问题原因未知。

追根溯源

虽然经过大量测试仍不能准确给出问题原因,但是几十次的崩溃结果中还是能看到一些规律的,其中有50%左右出现在第二场战斗释放之前战斗对象的时候,40%出现在玩家重新登录释放之前战斗对象的时候,这两种情况加在一起就占了绝大多数,所以要从这里开始入手,查看释放战斗对象的函数是不是存在问题。

因为程序中很少直接使用简单的指针,基本都会用智能指针来代替,所以在战斗对象析构时会有很多小对象自动析构,花了不少时间来看这些代码,结果一无所获,这就怪了,那么多次崩溃都是在这,居然找不到任何问题。

抽丝剥茧

因为之前测试时需要完成跑完整个战斗流程,严重影响了测试效率,既然感觉释放战斗对象这部分代码有问题,那就单独跑这一段逻辑呗,单独建个分支,改代码!!!另外还发现一个事情,本来在我机器上需要在调试状态下跑好几次才能重现出的问题,在另一台发布机上两三次就能重现,干脆用它来验证结果。

说干就干,从原来的逻辑中,剥离出创建、释放战斗对象的代码,每次测试重复创建和释放过程几百次,这样就应该很容易就能重现问题了,修改完本地先测试,结果跑了十几次也没出现,部署到发布机上测试多次也没出现问题,和预想的完全不一样,实验失败,这个结果基本说明我的方向错误,并不是这段释放战斗对象的逻辑代码问题,又得重新寻找线索了。

大海捞针

上面的验证虽说失败了,但也给我提了醒,既然释放战斗对象的逻辑代码没问题,但是绝大多数奔溃还发生在这里,那肯定是别人把它影响了,结合之前看到的内存问题,应该是有其他的逻辑写错了内存数据,导致释放战斗对象的内存时出现了问题。

这个崩溃在主分支是没有出现过的,在我开发完这个新需求之后才出现了这个问题,那么需要查新加了哪些代码,但是这个版本单单是新的文件就增加了几十个,要想从中找到一个内存问题犹如大海捞针一样。

祭出法宝

在大量代码中直接寻找内存问题,非寻常人所能企及,这时可以考虑借助第三方力量——比如检测工具,根据以往经验,我用的最多的内存检测工具是 ValgrindAddressSanitizer,起初 Valgrind 用的比较多的,后来认识了 AddressSanitizer 之后发现使用 Valgrind 后程序运行太慢了,而使用 AddressSanitizer 虽然需要重新编译一次,但是基本不影响原有程序的运行速度,所以渐渐偏向了 ASAN

但是,这次我先用了 Valgrind,还原代码,重新编译,调整参数后启动服务器程序,果然是半天没反应,测试多次之后居然没崩溃,查看了它的检测报告也没发现什么问题,决定换 ASAN 试试,因为每次用 Valgrind 启动和运行真的太慢了。

修改Makefile重新编译,使用 AddressSanitizer 来进行检测,这次更奇怪,添加了 ASAN 选项的程序编译后,貌似代码逻辑感觉到了它(ASAN)的存在,程序运行逻辑直接变了,原来能完整跑完的战斗逻辑,总是跑到一半因为条件不满足停下了,不过有几次跑到了最后,也出现了崩溃的情况,但是从检测报告中未查到问题的原因,仅仅找到一处内存泄漏问题,修改完崩溃问题依旧存在。

屏蔽无关

既然上面的工具没能提供帮助,那么还得依靠我硬啃代码了,还是先来分析之前各种崩溃结果,发现每次析构对象前都给客户端发了消息,而这些消息使用了 protobufoneof 结构,这个结构之前没用过,会不会因为使用不当,把内存写坏了。

这次我没有直接去看代码的细节,而是采用了屏蔽的方式,将一些不影响战斗逻辑的消息数据精简,不断注释代码,不断发布测试,结果依旧崩溃,最后仅剩一处同步技能的协议,其中也用了 oneof 结构,这时我更加感觉它有问题,但是它不能被注释掉,需要通过它发消息给客户端,然后客户端请求放技能才能将战斗进行下去,测试暂时卡在这了。

移形换位

必须想一种办法把这仅剩的一条消息同步去掉,如果不给客户端的同步消息,客户端就不能通知服务器放技能,那只好服务器自己把这些事都做了,修改服务器代码,采用延迟触发的方法,来驱动整个战斗进程能进行下去,最终把仅剩的那一条消息屏蔽掉了,同时把所有的try-catch也屏蔽了。

打包部署发布服,启动测试,问题依旧存在,唉,我麻了!

再请法宝

因为 ASAN 这个工具我一直在观察着输出的报告,并没有发现什么值得注意的问题,所以我打算换为 Valgrind,因为它们两个有点冲突,所以得把Makefile还原回去,重新编译再使用 Valgrind 来测试。

启动程序,依旧卡的像时间静止了一样,启动客户端开始了常规的疯狂测试,Duang!进程挂了,赶紧打开 Valgrind 的输出报告看看,亲人呐,我在里面找到了 Invalid write 的字样。

赶紧去查看这段报告对应的代码问题,其中包含了 std::sort 函数的使用,但是自定义的排序函数不满足严格弱排序规则,感觉这逻辑确实有问题,把它先注释掉来试一下。

风平浪静

注释掉 std::sort 之后,在本地机器测试半小时未发生崩溃,重新编译打包发布,几十次测试之后也没有发生崩溃的情况,一切又恢复了平静。

若有所思

如果在第一次使用工具时,我给予 Valgrind 多一点点宽容就好了。

其实事后看来好像没有多磨曲折,但是真实情况却是,前面的步骤交叉进行,经常会出现反复的情况,前前后后调试了近3天。

为什么如此执着?因为如果类似的问题不再早期发现时解决,后面要想再解决所付出的成本会更大,所以早发现早解决。

参考文章

  • AddressSanitizerLeakSanitizer
  • 内存错误检测工具-AddressSanitizer(ASAN)
  • 查找内存错误
  • c++中智能指针使用小结
  • 静态或者全局智能指针使用的注意几点
  • 谈谈如何利用 valgrind 排查内存错误
  • 几个C++内存泄漏和越界检测工具简介
  • 内存泄漏检测工具valgrind神器
  • 使用valgrind检查内存问题
  • Valgrind学习笔记(一)
  • 关于C#:valgrind-地址是在分配大小为16的块之前的8个字节
  • c++ seg fault issue: __gnu_cxx::__exchange_and_add
  • 记一次 TCMalloc Debug 经历
  • Segmentation fault in __gnu_cxx::__exchange_and_add () from /usr/lib64/libstdc++.so.6
  • C++中使用std::sort自定义排序规则时要注意的崩溃问题

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

靠想象打开未来一扇扇大门,靠理性选择其中正确的一扇~

相关文章:

  • 网络工具nc的常见功能和用法
  • git常用配置——git show/diff tab 显示宽度
  • Windows设置防火墙允许指定应用正常使用网络
  • 2021年终总结——脚踏实地,为下一次腾飞积蓄力量
  • 通过WindowsStore安装QuickLook小工具方便文件预览
  • linux环境下随时照看服务器进程的ps和top命令
  • 简单梳理下git的使用感受,思考git中最重要的是什么
  • 总结下各种常见树形结构的定义及特点(二叉树、AVL树、红黑树、Trie树、B树、B+树)
  • epoll的LT模式(水平触发)和ET模式(边沿触发)
  • C++可变参数模板的展开方式
  • 恶搞一下std::forward函数
  • C++11新式洗牌std::shuffle与老式洗牌函数std::random_shuffle的区别
  • linux环境下常用的网络命令ping、telnet、traceroute、tcpdump
  • .bat批处理(十一):替换字符串中包含百分号%的子串
  • linux环境下常用的查找命令find、which、grep
  • 收藏网友的 源程序下载网
  • 【140天】尚学堂高淇Java300集视频精华笔记(86-87)
  • 2017前端实习生面试总结
  • ES6--对象的扩展
  • java8-模拟hadoop
  • Java新版本的开发已正式进入轨道,版本号18.3
  • jdbc就是这么简单
  • Python爬虫--- 1.3 BS4库的解析器
  • RedisSerializer之JdkSerializationRedisSerializer分析
  • swift基础之_对象 实例方法 对象方法。
  • 爱情 北京女病人
  • 半理解系列--Promise的进化史
  • 不发不行!Netty集成文字图片聊天室外加TCP/IP软硬件通信
  • 不上全站https的网站你们就等着被恶心死吧
  • 官方解决所有 npm 全局安装权限问题
  • 简单基于spring的redis配置(单机和集群模式)
  • 如何学习JavaEE,项目又该如何做?
  • 这几个编码小技巧将令你 PHP 代码更加简洁
  • Semaphore
  • 关于Kubernetes Dashboard漏洞CVE-2018-18264的修复公告
  • # 再次尝试 连接失败_无线WiFi无法连接到网络怎么办【解决方法】
  • (AtCoder Beginner Contest 340) -- F - S = 1 -- 题解
  • (Java岗)秋招打卡!一本学历拿下美团、阿里、快手、米哈游offer
  • (三)c52学习之旅-点亮LED灯
  • (深入.Net平台的软件系统分层开发).第一章.上机练习.20170424
  • (轉貼) 資訊相關科系畢業的學生,未來會是什麼樣子?(Misc)
  • *ST京蓝入股力合节能 着力绿色智慧城市服务
  • .net redis定时_一场由fork引发的超时,让我们重新探讨了Redis的抖动问题
  • .net 怎么循环得到数组里的值_关于js数组
  • .Net接口调试与案例
  • @GetMapping和@RequestMapping的区别
  • @ModelAttribute注解使用
  • [AIGC] Java 和 Kotlin 的区别
  • [BUUCTF NewStarCTF 2023 公开赛道] week4 crypto/pwn
  • [C]编译和预处理详解
  • [Google Guava] 1.1-使用和避免null
  • [HAOI2016]食物链
  • [INSTALL_FAILED_TEST_ONLY],Android开发出现应用未安装
  • [Java并发编程实战] 共享对象之可见性
  • [LeetCode 127] - 单词梯(Word Ladder)