记一次CRT内存泄漏的监测,兼论断点的高级用法

本文记录排查CRT内存泄漏的全过程,在排查过程中,涉及到高级断点功能的使用,觉得很有必要记录下。

问题描述

在每次程序退出后,都会输出如下内存泄漏报告,以下为3次退出内存泄漏示例:


Detected memory leaks!
Dumping objects ->
{1273664} normal block at 0x0BE6F910, 97 bytes long.
 Data: <        A   A   > 00 00 00 00 00 00 00 00 41 00 00 00 41 00 00 00 
Object dump complete.

Detected memory leaks!
Dumping objects ->
{1288542} normal block at 0x0A5DB4D0, 97 bytes long.
 Data: <        A   A   > 00 00 00 00 00 00 00 00 41 00 00 00 41 00 00 00 
Object dump complete.

Detected memory leaks!
Dumping objects ->
{1292283} normal block at 0x07730480, 97 bytes long.
 Data: <        A   A   > 00 00 00 00 00 00 00 00 41 00 00 00 41 00 00 00 
Object dump complete.

上述由微软的C运行时库(CRT)内存调试功能输出。

预备知识

开启CRT调试功能后,他会提供详细的内存分配信息,包括分配的内存块编号、大小、位置等。该功能可通过如下方式开启:


#define  _CRTDBG_MAP_ALLOC   // 定义该宏,会将 malloc 映射为 调试版的 malloc,从而记录更加详细的分配信息  
#include <crtdbg.h>   

_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);

CRT 报告中的内存块分为 normal(普通块)、client(客户端块)和 CRT 块。重点关注 normal 和 client 块,因为它们是由你的程序分配的内存。

问题分析

分析上述内存泄漏报告,可以明确的是,这个泄漏是可以稳定重现的,这一点非常重要。

无论调试什么类型的Bug,只要它能稳定重现,那它就一定能够解决,这是支持我们前行的动力和解决它的底气。

从上面来看,每次泄漏的内存编号和地址是不确定的,唯一确定的是内存分配大小,因此从这一点入手。另外,

程序从启动到退出,中间经历了太多的流程,很容易迷惑解决方向,因此,先隔离,把前端和后端整个

隔离开,具体做法是把所有服务器地址设置为无效,启动后退出程序,发现没有内存泄漏,于是,逐个地址恢复,
查看是哪个地址的连接导致了泄漏,最后定位到是数据服务器,只要连接上数据服务器,退出就会有此泄漏。

大方向已定,接下来就是逐步细化。

通过走查代码,分析数据服务连接过程中的内存分配,最终落脚点定位到创建数据连接请求后,然后退出,就必定会有

内存泄漏,至于具体是哪里引起的,还需要借助工具,进一步定位,接下来就到了断点的妙处了。

因为是调试版本的内存分配,我们到 dbghealp.c 这个文件中来,通过设置几次断点,最终将核心的内存分配 _heap_alloc_dbg_impl 中。

image.png

这个函数实现了核心的内存分配功能,入口记录了在哪个文件的哪行,进行多大的内存分配,看下内部实现:

image.png

内部的 _heap_alloc_base 实际上调用的是 HeapAlloc 系统API函数。每一次分配都有一个分配序号,记录在 _lRequestCurr 中。

实际申请结构如下:

image.png

上面收尾的gap区域用来填充特殊值,便于问题检测,有以下几个特殊值。

image.png

调试版本下,未初始化的堆内存会以 0xCD 填充,已释放的堆内存会以 0xDD 填充,用于分割的gap区域以 0xfd 填充。

话说回来,考虑到是某个特定动作开始后造成的内存泄漏,那就在这个动作之前加上无条件断点,然后在 _heap_alloc_dbg_impl
函数入口处增加以下的断点:

  1. 在数据服务发起连接前,设一个断点。 《-- 减少内存分配的断点检测次数

  2. 增加一个条件断点 97 == nSize。 《---- 方便检测在特定大小的内存申请

  3. 在相同位置,再增加一个命中条件断点,当断点命中时,打印以下消息
    caller:CALLERMemoryReqNumber:lRequestCurrnSize:nSizeszFileName:szFileNameCALLSTACK

    image.png

    取消勾选继续执行选项 《---- 方便中断时,输出内存申请编号、调用堆栈等其他信息

  4. 在相同位置,再增加一个断点命中次数,具体条件看调试输出来决定。

准备上述0、1、2这三个断点,先是能 0 号断点,程序停住后,再开启1、2号断点,运行程序。

因为加上了条件断点,且此时不确定是哪一次的97字节内存分配未释放,所有需要执行完一趟程序后,正常退出,收集在调试输出窗口
中记录到的所有条件断点输出消息,这个过程会比较漫长,方向是对的,再慢也能抵达终点。

收集到所有数据以及最后的CRT内存泄漏分配编号,逐个对比,定位是哪一次的97字节分配触发了泄漏。

打比比方,例如是第5次的97字节泄漏,那么,为了验证的确是第5次的97字节分配造成的泄漏,可以启动 3 号断点,在中断命中次数达到 5
时,才命中断点。

image.png

如此,在下一轮验证中,在特定次数、特定大小的断点下,通过 调用堆栈 窗口,就能发现是哪里的内存泄漏以及

对应的断点编号,通过退出后的CRT断点报告编号查看,两者一致,于是乎,定位了内存泄漏所在处,bingo!!

通过加载相关组件的pdb文件,再次进行调试,立马定位到内存泄漏之处,这个是底层网络组件导致的泄漏,非常隐蔽,告知网络组同事解决即可。

问题小结

任何疑难问题的分析,都不可能是一帆风顺的。就上述提到的内存泄漏问题来说,这个泄漏应该潜伏了3、4年之久,每次退出时,都
会看到泄漏报告,不解决掉它,总感觉心理膈应。

上述问题解决了三天,不是说三天时间都花在上面,而是第一天花了1个小时,定位个大概后没什么头绪,先把这个问题放在一边,第二天再在第一天基础上,继续花1个小时左右来解决,有些许进展后又卡住了,再放在一边,再过两天,突然脑子里想到可以
这样使用条件断点,再试它1个小时,到最后定位到有问题的分配编号和CRT提示的内存泄漏编号一致,才真真确确的确认,此问题
得到解决,心头的一块石头落地了。

在已经解决的基础上,回顾这段历程,不断的尝试、修正、回顾、反思,每步在上一步的基础上前进,能复现,就肯定能解决。

经过这次分析历程,对CRT内存检测相关机制、断点的高级用法等有了更深刻的了解,特此总结成这篇文章,向读者分享笔者如释重负的愉快心情。

posted @   浩天之家  阅读(38)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
历史上的今天:
2022-12-27 结构思考力-学习笔记
2014-12-27 Word中批量替换软回车
点击右上角即可分享
微信分享提示