百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

如何定位内存泄漏(内存定位工具)

nanshan 2024-10-05 18:33 21 浏览 0 评论

介绍

本文主要介绍一种通过windbg分析内存泄漏的方法,方法也适用linux。

这个内存泄漏问题比较经典,我个人认为是自己这么多年bug定位中一个非常好的bug,并且在分析的过程中,也有许多需要思考的地方。通过该问题的分析,你可以了解到分析内存的基本方法和思路。

现象

后台检测程序在某天上午上报了内存警告,大概就是某程序的提交内存达到了1.0G。

这里需要解释下:在windows下32位应用程序如果提交内存大于某个阈值,比如我正常程序运行时提交内存最多应该只有500M,当检测程序发现该程序提交内存突然大于1.0G了,说明程序可能出现了内存泄漏。----当时就是这个进程的提交内存大于1.0G并发生了告警。

登陆后台查看,了解到如下信息:

  • 该进程已经连续运行了90天
  • 提交内存每天都在持续上涨,从启动到目前为止大概累计上升了800M。
  • 句柄、线程数等资源均正常

基本上可以确定程序存在内存泄漏,让运维通过工具保存了fulldump,并重启进程(否则内存告警会一直提示)。

这时候对于有经验的人员,这个问题因为并不没有对生产环境造成影响,且等到问题发生异常时还有比较长时间,所以可以不需要立刻恢复现场,否则当问题无法定位时而现场被破坏,将很难解决问题。

分析思路

  • 代码review:通过比较上个版本和上上个版本之间的差异,找到内存泄漏的地方。

的确是可以,但存在几个问题,因为本身每天内存泄漏的非常少,且之前版本大都一个月不到就升级了,不能确定这个问题是否是之前一个版本引入的,也可能是很多个版本前引入的?

其次:这个进程处理的消息类型很多,可能有问题的消息处理早就存在,只是最近一段时间其他服务升级,导致有bug的消息处理模块被触发。所以以上原因通过review近几个版本并不一定能找到。

还有,review可能能找到多个泄漏点,但可能存在遗漏的情况,并不是该问题的本质原因,修改后问题还可能存在。

但这个方法对于有人力富于的公司还是可以的,就让一个同事review代码,还是有效果的。

  • 静态代码检测工具:

公司没有基础,临时部署时间来不及。

  • 构建复现环境:由于问题出现原因不知,而复现时间太长,找不到快速复现的方法。

在平时工作中,通过复现注释代码缩小可疑模块,是我们大都会用的有效方法,但这个场景很难找到复现方法。

  • 规避问题:通过每周半夜重启程序,规避该问题。

这个方法在很多公司都存在,因为疑难问题的解决的确非常耗时,所以一般会有一个看门狗程序,在客户不知不觉时重启进程,快速恢复,也是非常常用的方法。这对于我来说,是下下策,不到万不得已,不会使用,印象中自己没怎么用过。

  • 通过技能查找问题的根本原因。

umdh:通过在A时间点获取一个进程内存镜像,然后一段时间出现内存泄漏后,在B时间点再获取一个进程内存镜像,通过比较找到之间的差异。理论可行,但对于这个问题意义不大,本身进程是一个高并发进程,每秒都要处理上百个消息,内存有上百次的申请和释放,A和B比较后差异会非常大,很难找到真实的内存泄漏模块。

通过以上思考,在有限人力下,通过windbg分析dump的内存,查找真实内存泄漏是快速并有效的方法,下面我就针对该问题给大家介绍下我的分析思路,最后问题的解决大致花费了半个工作日的时间。

准备工作

当时的dump我保存到了百度网盘。

  • [下载地址](https://pan.baidu.com/s/1vUjAr7edFTxxcKGnGEaatQ "下载地址")(提取码:11bg)
  • 设置好系统的pdb
e:\mylocalsymbols;SRV*e:\mylocalsymbols*http://msdl.microsoft.com/download/symbols

分析方法

C++的release版程序,内存携带的信息是非常有限的,大致就是三个维度:

  • 内存大小:每次malloc申请的大小,通过大小,我们可以找到对应的结构体、类
  • 内存地址内容:通过查看内存地址内容,比如有字符串、有特殊的值,找到申请的模块
  • 内存申请次数:通过每小时申请的频率,可以找到具体的消息类型

下面就是通过这三个维度找到具体的原因。

查找内存大小

打印所有堆块信息

!heap -s

显示如下

0:000> !heap -s
HEAPEXT: Unable to read ntdll!RtlpDisableHeapLookaside
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-----------------------------------------------------------------------------
006f0000 00000002 1246976 1241928 1246976 982 236 81 0 a LFH
00190000 00001002 3136 1564 3136 390 7 3 0 0 LFH
External fragmentation 24 % (7 free blocks)
00110000 00001002 256 4 256 1 1 1 0 0
02050000 00001002 256 176 256 1 18 1 0 0 LFH
02240000 00001002 256 4 256 2 1 1 0 0
006a0000 00001002 64 12 64 4 2 1 0 0
044f0000 00001002 256 216 256 7 4 1 0 0 LFH
119d0000 00001002 7424 5820 7424 134 133 4 0 c8 LFH
14290000 00001003 256 4 256 2 1 1 0 bad
141d0000 00001003 256 4 256 2 1 1 0 bad
17f20000 00001003 256 4 256 2 1 1 0 bad
19030000 00001003 256 4 256 2 1 1 0 bad
191b0000 00001003 256 4 256 2 1 1 0 bad
19380000 00001003 256 4 256 2 1 1 0 bad
19300000 00001003 256 4 256 2 1 1 0 bad
155f0000 00001003 256 4 256 2 1 1 0 bad
-----------------------------------------------------------------------------

通过观察,我们知道了是006f0000堆块占用了大量内存

HEAPEXT: Unable to read ntdll!RtlpDisableHeapLookaside
Heap Flags Reserv Commit Virt Free List UCR Virt Lock Fast
(k) (k) (k) (k) length blocks cont. heap
-----------------------------------------------------------------------------
006f0000 00000002 1246976 1241928 1246976 982 236 81 0 a LFH

查看堆块内存百分比

内存持续上涨可能是某块固定大小内存被重复申请,所以统计下该堆块中各个内存大小的分配次数

!heap -stat -h 006f0000

查找堆中各个内存大小占用的百分比

0:000> !heap -stat -h 006f0000
unable to resolve ntdll!RtlpStackTraceDataBase
heap @ 006f0000
group-by: TOTSIZE max-display: 20
size #blocks total ( %) (percent of total busy bytes)
14 23acbbe - 2c97ead8 (92.78)
a4 2ba0c - 1bf2fb0 (3.63)
1000 8f5 - 8f5000 (1.16)
1a4 3b9c - 61cbf0 (0.79)
20c 15fb - 2cfdc4 (0.37)
25 b77d - 1a8511 (0.22)
64 3ba0 - 174a80 (0.19)
24 75ae - 108c78 (0.13)
11c e4a - fda18 (0.13)
84c 164 - b89b0 (0.09)
400 172 - 5c800 (0.05)
234 265 - 54684 (0.04)
1c 2c2e - 4d508 (0.04)
1c0 287 - 46c40 (0.04)
c00 4b - 38400 (0.03)
20 1a12 - 34240 (0.03)
3bc ce - 30148 (0.02)
50 8da - 2c420 (0.02)
800 4c - 26000 (0.02)
2ba d2 - 23c94 (0.02)

size #blocks total ( %) (percent of total busy bytes)
14 23acbbe - 2c97ead8 (92.78)

TOP 20 中显示,最多的一个大小为 0x014 的分配次数为 0x23acbbe 次, 总共大概有700M左右。基本接近内存泄漏的总数。

所以这里得出几个结论:

  • 每次内存泄漏的大小是20字节。
  • 总共分配了0x23acbbe次,运行了90天,也就是每小时17318次/小时

定位内存来源

找到了大量的内存是0x014字节大小的,但是根据这个条件我们也找不到具体的代码啊?下面是几个思路

  • 根据大小

根据内存大小(0x14)去代码中查找大小为(0x14)的类、结构体、宏等等相关代码,然后找到原因。有几个问题:

1)、进程包含了很多其他组的dll,有的我没代码权限,无法遍历

2)、结构体、类太多了,人眼遍历太难了(针对这个问题我后来开发了一个工具,通过pdb文件可以找到程序中指定大小的所有结构体和类,后续章节讲解)

  • 内存内容

显示所有大小为(0x14)内存的地址,看它的地址内容有没有什么特点,比如是否有特殊的字符串、固定的二进制头??? 显示所有分配大小为 0x14的内存

命令
!heap -flt s 14
0:000> !heap -flt s 14
unable to resolve ntdll!RtlpStackTraceDataBase
_HEAP @ 6f0000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
0071c038 0004 0000 [00] 0071c040 00014 - (busy)
0071c2e8 0004 0004 [00] 0071c2f0 00014 - (busy)
0071e498 0004 0004 [00] 0071e4a0 00014 - (busy)
0071e4f8 0004 0004 [00] 0071e500 00014 - (busy)
0071e518 0004 0004 [00] 0071e520 00014 - (busy)
0071e5f8 0004 0004 [00] 0071e600 00014 - (busy)
0071e638 0004 0004 [00] 0071e640 00014 - (busy)
0071e658 0004 0004 [00] 0071e660 00014 - (busy)
0071e798 0004 0004 [00] 0071e7a0 00014 - (busy)
007374f0 0004 0004 [00] 007374f8 00014 - (busy)
00737510 0004 0004 [00] 00737518 00014 - (busy)
00737530 0004 0004 [00] 00737538 00014 - (busy)
00737550 0004 0004 [00] 00737558 00014 - (busy)
00737570 0004 0004 [00] 00737578 00014 - (busy)
00737590 0004 0004 [00] 00737598 00014 - (busy)
007375b0 0004 0004 [00] 007375b8 00014 - (busy)
007375d0 0004 0004 [00] 007375d8 00014 - (busy)
007375f0 0004 0004 [00] 007375f8 00014 - (busy)
00737610 0004 0004 [00] 00737618 00014 - (busy)
00737630 0004 0004 [00] 00737638 00014 - (busy)
00737650 0004 0004 [00] 00737658 00014 - (busy)
00737670 0004 0004 [00] 00737678 00014 - (busy)
00737690 0004 0004 [00] 00737698 00014 - (busy)
..............
..............

随机抽查几个地址,看下地址内存,都是00 00 00 00 00

大都是这样的值,实在是看不出规律。

建议

一般公司都会封装malloc、new函数,并分配一个模块号,每个内存地址头部都会携带id号,如下:

xxx_malloc(int nModleID,size_t size);

这样通过地址空间头也可以找到分配的模块。

  • 分配次数

大小0x14的内存在90天时间内总共分配了23acbbe 次, 0x23acbbe = 37407678/(90(天)*24(小时) ≈ 17318次/小时。 这个内存几乎每小时被申请17318次。进程有个统计功能:每个小时会统计处理的消息类型次数,那分析下数量级在1w~3w左右的消息即可,大概是4个消息类型,然后通过对这四个代码review才发现内存泄漏点。

if(total_fee){
LPADD_FEE pAddFee = new ADD_FEE;
ZeroMemory(pAddFee, sizeof(ADD_FEE));
pAddFee->nFee = total_fee;
gdt.nTotalFee = total_fee;
}

结构体 ADD_FEE ,刚好是20字节

typedef struct _tagADD_FEE{
int nFee;
int nReserved[4];
}ADD_FEE, *LPADD_FEE;

完全符合!! 问题解决

总结

这是一个低级错误导致的。为了避免类视问题,引入代码静态检测

1)、cppcheck

2)、pclint

最后选了pclint。配合jenkins,每天凌晨进行代码静态检查,并输出和上个版本的diff文件,下次就不会出现这么低级的问题。

在大公司里面都会有非常多的检测工具、流程、方法论,都是前人经验的积累,虽然有点冗余繁琐,但却非常有效。当你离开这个平台后,缺少了这些流程,一旦遇到疑难问题你才发现自己能用的手段真的很少。

相关推荐

F5负载均衡器如何通过irules实现应用的灵活转发?

F5是非常强大的商业负载均衡器。除了处理性能强劲,以及高稳定性之外,F5还可以通过irules编写强大灵活的转发规则,实现web业务的灵活应用。irules是基于TCL语法的,每个iRules必须包含...

映射域名到NAS

前面介绍已经将域名映射到家庭路由器上,现在只需要在路由器上设置一下端口转发即可。假设NAS在内网的IP是192.168.1.100,NAS管理端口2000.你的域名是www.xxx.com,配置外部端...

转发(Forward)和重定向(Redirect)的区别

转发是服务器行为,重定向是客户端行为。转发(Forward)通过RequestDispatcher对象的forward(HttpServletRequestrequest,HttpServletRe...

SpringBoot应用中使用拦截器实现路由转发

1、背景项目中有一个SpringBoot开发的微服务,经过业务多年的演进,代码已经累积到令人恐怖的规模,亟需重构,将之拆解成多个微服务。该微服务的接口庞大,调用关系非常复杂,且实施重构的人员大部分不是...

公司想搭建个网站,网站如何进行域名解析?

域名解析是将域名指向网站空间IP,让人们通过注册的域名可以方便地访问到网站的一种服务。IP地址是网络上标识站点的数字地址,为方便记忆,采用域名来代替IP地址标识站点地址。域名解析就是域名到IP地址的转...

域名和IP地址什么关系?如何通过域名解析IP?

一般情况下,访客通过域名和IP地址都能访问到网站,那么两者之间有什么关系吗?本文中科三方针对域名和IP地址的关系和区别,以及如何实现域名与IP的绑定做下介绍。域名与IP地址之间的关系IP地址是计算机的...

分享网站域名301重定向的知识

网站域名做301重定向操作时,一般需要由专业的技术来协助完成,如果用户自己在维护,可以按照相应的说明进行操作。好了,下面说说重点,域名301重定向的操作步骤。首先,根据HTTP协议,在客户端向服务器发...

NAS外网到底安全吗?一文看懂HTTP/HTTPS和SSL证书

本内容来源于@什么值得买APP,观点仅代表作者本人|作者:可爱的小cherry搭好了NAS,但是不懂做好网络加密,那么隐私泄露也会随时发生!大家好,这里是Cherry,喜爱折腾、玩数码,热衷于分享数...

ForwardEmail免费、开源、加密的邮件转发服务

ForwardEmail是一款免费、加密和开源的邮件转发服务,设置简单只需4步即可正常使用,通过测试来看也要比ImprovMX好得多,转发近乎秒到且未进入垃圾箱(仅以Mailbox.org发送、Out...

使用CloudFlare进行域名重定向

当网站变更域名的时候,经常会使用域名重定向的方式,将老域名指向到新域名,这通常叫做:URL转发(URLFORWARDING),善于使用URL转发,对SEO来说非常有用,因为用这种方式能明确告知搜索引...

要将端口5002和5003通过Nginx代理到一个域名上的操作笔记

要将端口5002和5003通过Nginx代理到域名www.4rvi.cn的不同路径下,请按照以下步骤配置Nginx:步骤说明创建或编辑Nginx配置文件通常配置文件位于/etc/nginx/sites...

SEO浅谈:网站域名重定向的三种方式

在大多数情况下,我们输入网站访问网站的时候,很难发现www.***.com和***.com的区别,因为一般的网站主,都会把这两个域名指向到同一网站。但是对于网站运营和优化来说,www.***.com和...

花生壳出现诊断域名与转发服务器ip不一致的解决办法

出现诊断域名与转发服务器ip不一致您可以:1、更改客户端所处主机的drs为223.5.5.5备用dns为119.29.29.29;2、在windows上进入命令提示符输入ipconfig/flush...

涨知识了!带你认识什么是域名

1、什么是域名从技术角度来看,域名是在Internet上解决IP地址对应的一种方法。一个完整的域名由两个或两个以上部分组成,各部分之间用英文的句号“.”来分隔。如“abc.com”。其中“com”称...

域名被跳转到其他网站是怎么回事

当你输入域名时被跳转到另一个网站,这可能是由几种原因造成的:一、域名可能配置了域名转发服务。无论何时有人访问域名,比如.com、.top等,都会自动重定向到另一个指定的URL,这通常是在域名注册商设...

取消回复欢迎 发表评论: