技术干货丨通过wrap malloc定位C/C++的内存泄漏问题
摘要:用C/C++开发的程序执行效率很高,但却经常受到内存泄漏的困扰。本文提供一种通过wrap malloc查找memory leak的思路。
用C/C++开发的程序执行效率很高,但却经常受到内存泄漏的困扰。本文提供一种通过wrap malloc查找memory leak的思路,依靠这个方法,笔者紧急解决了内存泄漏问题,避免项目流血上大促,该方法在日后工作中大放光彩,发现了项目中大量沉疴已久的内存泄漏问题。
什么是内存泄漏?
动态申请的内存丢失引用,造成没有办法回收它(我知道杠jing要说进程退出前系统会统一回收),这便是内存泄漏。
Java等编程语言会自动管理内存回收,而C/C++需要显式的释放,有很多手段可以避免内存泄漏,比如RAII,比如智能指针(大多基于引用计数计数),比如内存池。
理论上,只要我们足够小心,在每次申请的时候,都牢记释放,那这个世界就清净了,但现实往往没有那么美好,比如抛异常了,释放内存的语句执行不到,又或者某菜鸟程序员不小心埋了一个雷,所以,我们必须直面真实的世界,那就是我们会遭遇内存泄漏。
怎么查内存泄漏?
我们可以review代码,但从海量代码里找到隐藏的问题,这如同大海捞针,往往两手空空。
所以,我们需要借助工具,比如valgrind,但这些找内存泄漏的工具,往往对你使用动态内存的方式有某种期待,或者说约束,比如常驻内存的对象会被误报出来,然后真正有用的信息会掩盖在误报的汪洋大海里。很多时候,甚至valgrind根本解决不了日常项目中的问题。
所以很多著名的开源项目,为了能用valgrind跑,都费大力气,大幅修改源代码,从而使得项目符合valgrind的要求,满足这些要求,用valgrind跑完没有任何报警的项目叫valgrind干净。
既然这些玩意儿都中看不中用,所以,求人不如求己,还是得自力更生。
什么是动态内存分配器?
动态内存分配器是介于kernel跟应用程序之间的一个函数库,glibc提供的动态内存分配器叫ptmalloc,它也是应用最广泛的动态内存分配器实现。
从kernel角度看,动态内存分配器属于应用程序层;而从应用程序的角度看,动态内存分配器属于系统层。
应用程序可以通过mmap系统直接向kernel申请动态内存,也可以通过动态内存分配器的malloc接口分配内存,而动态内存分配器会通过sbrk、mmap向kernel分配内存,所以应用程序通过free释放的内存,并不一定会真正返还给系统,它也有可能被动态内存分配器缓存起来。
google有自己的动态内存分配器tcmalloc,另外jemalloc也是著名的动态内存分配器,他们有不同的性能表现,也有不同的缓存和分配策略。你可以用它们替换linux系统glibc自带的ptmalloc。
new/delete跟malloc/free的关系
new是c++的用法,比如Foo *f = new Foo,其实它分为3步。
(1)通过operator new()分配sizeof(Foo)的内存,最终通过malloc分配。
(2)在新分配的内存上构建Foo对象。
(3)返回新构建的对象地址。
new=分配内存+构造+返回,而delete则是等于析构+free。
所以搞定malloc、free就是从根本上搞定动态内存分配。
chunk
每次通过malloc返回的一块内存叫一个chunk,动态内存分配器是这样定义的,后面我们都这样称呼。
wrap malloc
gcc支持wrap,即通过传递-Wl,--wrap,malloc的方式,可以改变调用malloc的行为,把对malloc的调用链接到自定义的__wrap_malloc(size_t)函数,而我们可以在__wrap_malloc(size_t)函数的实现中通过__real_malloc(size_t)真正分配内存,而后我们可以做搞点小动作。
同样,我们可以wrap free。malloc跟free是配对的,当然也有其他相关API,比如calloc、realloc、valloc,但这根本上还是malloc+free,比如realloc就是malloc + free。
怎么去定位内存泄漏呢?
我们会malloc各种不同size的chunk,也就是每种不同size的chunk会有不同数量,如果我们能够跟踪每种size的chunk数量,那就可以知道哪种size的chunk在泄漏。很简单,如果该size的chunk数量一直在增长,那它很可能泄漏。
光知道某种size的chunk泄漏了还不够,我们得知道是哪个调用路径上导致该size的chunk被分配,从而去检查是不是正确释放了。
怎么跟踪到每种size的chunk数量?
我们可以维护一个全局 unsigned int malloc_map[1024 * 1024]数组,该数组的下标就是chunk的size,malloc_map[size]的值就对应到该size的chunk分配量。
这等于维护了一个chunk size到chunk count的映射表,它足够快,而且它可以覆盖到0 ~ 1M大小的chunk的范围,它已经足够大了,试想一次分配一兆的块已经很恐怖了,可以覆盖到大部分场景。
那大于1M的块怎么办呢?我们可以通过log记录下来。
在__wrap_malloc里,++malloc_map[size]
在__wrap_free里,--malloc_map[size]
很简单,我们通过malloc_map记录了各size的chunk的分配量。
如何知道释放的chunk的size?
不对,free(void *p)只有一个参数,我如何知道释放的chunk的size呢?怎么办?
我们通过在__wrap_malloc(size_t)的时候,分配8+size的chunk,也就是多分配8字节,开始的8字节存储该chunk的size,然后返回的是(char*)chunk + 8,也就是偏移8个字节返回给调用malloc的应用程序。
这样在free的时候,传入参数void* p,我们把p往前移动8个字节,解引用就能得到该chunk的大小,而该大小值就是前一步,在__wrap_malloc的时候设置的size。
好了,我们真正做到记录各size的chunk数量了,它就存在于malloc_map[1M]的数组中,假设64个字节的chunk一直在被分配,数量一直在增长,我们觉得该size的chunk很有可能泄漏,那怎么定位到是哪里调用过来的呢?
如何记录调用链?
我们可以维护一个toplist数组,该数组假设有10个元素,它保存的是chunk数最大的10种size,这个很容易做到,通过对malloc_map取top 10就行。
然后我们在__wrap_malloc(size_t)里,测试该size是不是toplist之一,如果是的话,那我们通过glibc的backtrace把调用堆栈dump到log文件里去。
注意:这里不能再分配内存,所以你只能使用backtrace,而不能使用backtrace_symbols,这样你只能得到调用堆栈的符号地址,而不是符号名。
如何把符号地址转换成符号名,也就是对应到代码行呢?
addr2line
addr2line工具可以做到,你可以追查到调用链,进而定位到内存泄漏的问题。
至此,你已经get到了整个核心思想。
当然,实际项目中,我们做的更多,我们不仅仅记录了toplist size,还记录了各size chunk的增量toplist,会记录大块的malloc/free,会wrap更多的API。
总结一下:通过wrap malloc/free + backtrace + addr2line,你就可以定位到内存泄漏了,恭喜大家。
技术干货丨通过wrap malloc定位C/C++的内存泄漏问题的更多相关文章
- 如何定位 Node.js 的内存泄漏
基础知识 Node.js 进程的内存管理,都是有 V8 自动处理的,包括内存分配和释放.那么 V8 什么时候会将内存释放呢? 在 V8 内部,会为程序中的所有变量构建一个图,来表示变量间的关联关系,当 ...
- MAT工具定位分析Java堆内存泄漏问题方法
一.MAT概述与安装 MAT,全称Memory Analysis Tools,是一款分析Java堆内存的工具,可以快速定位到堆内泄漏问题.该工具提供了两种使用方式,一种是插件版,可以安装到Eclips ...
- 技术干货丨如何在VIPKID中构建MQ服务
小结: 1. https://mp.weixin.qq.com/s/FQ-DKvQZSP061kqG_qeRjA 文 |李伟 VIPKID数据中间件架构师 交流微信 | datapipeline201 ...
- 技术干货丨卷积神经网络之LeNet-5迁移实践案例
摘要:LeNet-5是Yann LeCun在1998年设计的用于手写数字识别的卷积神经网络,当年美国大多数银行就是用它来识别支票上面的手写数字的,它是早期卷积神经网络中最有代表性的实验系统之一.可以说 ...
- Memory Analyzer Tool定位Java heap space内存泄漏
java heap space是一个很蛋疼的问题,如果开发调试时遇到还好,如果是在项目上线后运行一段时间后,才抛出该异常,那真的很悲剧(那你得找代码中到底是哪里内存泄露了),这真是一个悲伤的故事. 1 ...
- malloc钩子和内存泄漏工具mtrace、Valgrind
一:malloc钩子函数 static void* (* old_malloc_hook) (size_t,const void *);static void (* old_free_hook)(vo ...
- 【Bugly技术干货】那些年我们用过的显示性能指标
Bugly 技术干货系列内容主要涉及移动开发方向,是由 Bugly 邀请腾讯内部各位技术大咖,通过日常工作经验的总结以及感悟撰写而成,内容均属原创,转载请标明出处. 前言: 注:Google 在自己文 ...
- H5技术干货
H5技术干货 meta标签相关知识 H5页面窗口自动调整到设备宽度,并禁止用户缩放页面 <meta name="viewport" content="width=d ...
- Git-【技术干货】工作中Git的使用实践
Git-[技术干货]工作中Git的使用实践 置顶 2019-09-17 21:02:16 web洋仔 阅读数 11444更多 分类专栏: Git 版权声明:本文为博主原创文章,遵循CC 4.0 B ...
随机推荐
- Copy-on-write + Proxy = ?
一.简介 Immer (German for: always) is a tiny package that allows you to work with immutable state in a ...
- 时间序列神器之争:prophet VS lstm
一.需求背景 我们福禄网络致力于为广大用户提供智能化充值服务,包括各类通信充值卡(比如移动.联通.电信的话费及流量充值).游戏类充值卡(比如王者荣耀.吃鸡类点券.AppleStore充值.Q币.斗鱼币 ...
- ubuntu18.04安装部署typecho个人博客
LNMP一键安装包安装 wget http://soft.vpser.net/lnmp/lnmp1.5.tar.gz -cO lnmp1.5.tar.gz && tar zxf lnm ...
- 构建操作mysql的类
<?phpclass PdoMySQL{ public static $config=array();//设置连接参数,配置信息 public static $link=null;//保存连接标 ...
- Cobbler自动化部署系统
1.cobbler简介 cobbler是一个LInux服务器安装的服务,可以通过网络启动(PXE)的方式来快速安装.重装物理服务器和虚拟机,同时还可以管理DHCP,DNS等 cobbler可以 ...
- 超详细实战教程丨多场景解析如何迁移Rancher Server
本文转自Rancher Labs 作者介绍 王海龙,Rancher中国社区技术经理,负责Rancher中国技术社区的维护和运营.拥有6年的云计算领域经验,经历了OpenStack到Kubernetes ...
- redis性能优化——生产中实际遇到的问题排查总结
背景 redis-K,V数据库,因其高性能的操作性和支持丰富的数据结构,目前大量被用于衔接应用层和关系数据库中间的缓存层.随着使用的场景越来越多,和数据量快速的递增,在生产环境中经常会遇到相关的性能瓶 ...
- 曹工改bug:centos下,mongodb开机不能自启动,systemctl、rc.local都试了,还是不行,要不要放弃?
问题背景 最近装个centos 7.6的环境,其中,基础环境包括,redis.nginx.mongodb.fastdfs.mysql等,其中,自启动使用的是systemctl,其他几个组件,都没啥问题 ...
- 消息队列——Kafka基本使用及原理分析
文章目录 一.什么是Kafka 二.Kafka的基本使用 1. 单机环境搭建及命令行的基本使用 2. 集群搭建 3. Java API的基本使用 三.Kafka原理浅析 1. topic和partit ...
- OO第三单元——JML之破分大法
一.Jml总结及应用工具链 总的来说,jml就是对java程序进行规格化设计的一种表示语言,其中最核心的就是规格化,将代码要实现的功能和各项要求与约束不是通过自然语言,而是通过严密的逻辑语言来表达,这 ...