Malloc内存泄露和内存越界问题的研究

标签:
不幸太多标识思路大小 |
分类: Linux的学习 |
Malloc内存泄露和内存越界问题的研究
------内存跟踪与检测篇
1.
熟悉c语言的人都知道,内存泄露,特别是内存越界是软件界非常棘手,甚至防不胜防的问题。由于这种问题一般为概率问题,时而出现时而不出现,这样给问题的定位分析带来很大的困难,后期排查的代价也比较大,因此,这个问题一直困扰着软件开发人员和软件界。不管多牛的技术高手,甚至技术专家都不敢拍着胸脯说,他负责的项目没有内存泄露和内存越界问题。
那如何解决这种问题呢?
解决这种问题无非有两种方案,一:进行后期内存跟踪,即对分配的内存进行跟踪,检查它们有没有内存泄露和内存越界问题,如有修改之;二:进行前期内存检测,即在分配和使用内存前,先检测其合法性。第一个方案的优点就是逻辑相对简单,容易实现些,且基本能够发现90%以上的内存问题;缺点就是需要额外的内存开销,同时问题的发现依赖于问题的复现。第二种方案其实就是完整的内存管理,由于其实现难道比较大,要求运行效率比较高,同时需要解决内存碎片和编码习惯等问题。因此,这里只研究通过后期内存跟踪与检查来发现malloc内存是否泄漏和越界问题。至于第二种方案和栈内存,静态内存,全局内存等越界问题,以后有时间再慢慢研究。
2.
2.1
对于malloc内存泄露,只要遵循:”谁申请谁释放,在同一函数中申请在同一函数中释放”原则基本上可以杜绝内存泄露问题。当然,在很多情况下,不可避免地需要在不同的函数中申请和释放,甚至需要在不同的任务中申请和释放。这,才是造成堆内存泄露的主要原因。
相对于堆内存越界问题,堆内存泄露问题的排查相对简单的多。但谁要是敢拍胸脯说他做的系统绝对不会有内存泄露问题,要么就是他参与的项目很少涉及在不同任务中分配和释放等复杂情况,要么就是他太过轻率。
2.2
http://blog.chinaunix.net/attachment/201208/14/27629626_13449172296hjg.jpg
内存越界问题一直以来是开发人员,甚至软件界非常棘手的问题,但仔细分析起来无非三种情况:踩了别人,被人踩了,既踩了别人同时也被人踩了,如上图1,图2,图3所示。
下面我们将针对内存越界的三种情况采取对应的解决方案。
2.3
2.3.1
http://blog.chinaunix.net/attachment/201208/14/27629626_1344917317ZB5i.jpg
<1>.
在用户分配的内存中插入头尾信息,一般来说,当一个内存对象需要踩掉别人时,必须先踩掉自己的尾部,它就是凶手,如图4;当一个内存块的头部被踩时,它肯定是受害者,如图5;当一个内存对象的头尾都被踩时,它有可能是受害者(头尾被踩穿),也有可能它既是凶手也是受害者(被人踩的同时也踩了别人)。至于自己踩自己的头部,那属于我们所谓的自杀行为,它本身既是凶手也是是受害者。
需要说明的是,我们将这里的内存块看着对象,即内存块对象。它包含了内存块(数据)和内存块访问行为(操作)。因为内存块本身是不会被踩的,它只是块数据而已,只有行为才构成威胁。而如果仅仅只研究内存块访问行为,那么我们将很难根据内存跟踪的方法查找谁踩了谁?甚至连嫌疑对象都很难查找。否则就变成了另一个研究课题”分配和使用内存前,必须先检测其合法性”。这,不是我们本次研究的对象。
<2>.
http://blog.chinaunix.net/attachment/201208/14/27629626_1344917362cCKM.jpg
如果一个内存对象踩了别人,如果在内存块中插入头尾信息,那么它必须先踩自己的尾部,然后才可能踩到别人;因此,只要它的尾部被踩,不管它有没有踩到别人,说明它肯定内存越界啦,如图4所示。至于被别人击穿而导致的尾部被踩的情况,我们将在后面讨论。
踩了别人又分为三种情况:第一种是只踩了自己的尾部;第二种是既踩了自己的尾部还踩了别人的头部;第三种就是踩到别的地方去了,当然也踩了自己的尾部。不管怎么样,它都是凶手,至少是嫌疑犯。
2
2
2
<3>.
|
如果一个内存块被踩了,如果在内存块中插入头尾信息,那么它的头部信息肯定被踩掉了;因此,不管怎么样,它都是受害者,如图5所示。
被人踩了大致可以分为三种情况:第一种是头部被踩了,但尾部没有被踩;第二种是头尾都被踩了。第三种是自杀行为。
2
2
2
<4>.
http://blog.chinaunix.net/attachment/201208/14/27629626_1344917440p7aD.jpg
既踩了别人,也被人踩了,有点像它既是凶手,也是受害者。这种情况比较复杂,涉及到多种不同的组合;。我们大致分为三类:第一情况是头部被踩了,但尾部没被踩,同时还踩了别人,如图6.1所示;第二种情况是头尾被踩,同时也踩了别人,如图6.2所示;第三种情况是头尾被踩,还有自杀行为,如图6.3所示。至于它踩了别人后自杀了,已在<3>.
<5>.
从内存块对象的数据角度来看,分为三种情况:头被踩,尾被踩,头尾都被踩;从内存对象的行为来看,也分为三种情况:踩了别人,被人踩了,既踩了别人,也被人踩啦。
从查案的线索来看,这个最复杂。复杂?不复杂找你干嘛,不直接找个清洁工来查呀,你看人家不但任劳任怨,还不讨价还价。嘿嘿~~放松一下!言归正传,从查案的线索来看,分为:(头晕啦,先放着吧)
这里说到用插入头尾法标识内存块有没有被踩来判断谁踩了踩,然而我们怎么跟踪和查看这些内存信息呢?
2.3.2
http://blog.chinaunix.net/attachment/201208/14/27629626_1344917478ZNk8.jpg
图7表示增加了头尾信息的内存块,图8表示用于记录内存块信息的链表,图9表示链表节点信息。其链表节点由blkId,
blkAddr, blklength组成;
需要注意的是,实现这个功能时,我们需要对malloc和free进行封装,当用户调用封装的malloc接口时,我们需要分配的实际内存大小为头长度+用户内存长度+尾长度;因此我们记录的blkLength为内存块总长度,blkAddr为实际内存块的首地址。头尾长度为多少?头尾信息是什么呢?为了节省内存开销,建议采用约定的方式实现。当然也可以包含在链表头节点中。
同样,对于堆内存泄露问题,当我们发现某个模块的内存块不停地被申请而没有被释放的话,就需要相关人员来确认是否有内存泄露的嫌疑。注意:这只是嫌疑而已,因为到底是否为内存泄露有时很难确定。对于像分配一块单板就分配一块内存,删除一块单板就释放一块内存来说,就比较容易判断。如果我分配了10次,删除了5次,如果内存块较长时间多于5个的话,肯定就有内存泄露,如果少于5个的话,说明由于哪个状态标志问题多释放了内存块。
3.
既然malloc的内存块可能被踩,当然记录内存块的链表也可能被踩掉,如果链表的头结点被踩掉的话,我们将无法通过链表信息来跟踪和检测它。如果我们再用别的内存来记录这个链表的话,谁能保证记录链表的这个内存不被踩掉?这不就陷入到死循环中了?
怎么办呢?
通过和bsp组交流后发现,他们在bootload中可以为我们划分一块保留内存,然后封装一套接口供我们使用。用户态无法直接访问这块内存,必须通过他们封装的接口才能访问,这样就可以解决链表被踩的问题。
4.
n
n
5.
http://blog.chinaunix.net/attachment/201208/14/27629626_1344917512hC6n.jpg
图1:
一个典型的Linux C程序内存空间由如下几部分组成:
- 代码段(.text)。这里存放的是CPU要执行的指令。代码段是可共享的,相同的代码在内存中只会有一个拷贝,同时这个段是只读的,防止程序由于错误而修改自身的指令。
- 初始化数据段(.data)。这里存放的是程序中需要明确赋初始值的变量,例如位于所有函数之外的全局变量:int val=100。需要强调的是,以上两段都是位于程序的可执行文件中,内核在调用exec函数启动该程序时从源程序文件中读入。
- 未初始化数据段(.bss)。位于这一段中的数据,内核在执行该程序前,将其初始化为0或者null。例如出现在任何函数之外的全局变量:int sum;
- 堆(Heap)。这个段用于在程序中进行动态内存申请,例如经常用到的malloc,new系列函数就是从这个段中申请内存。
- 栈(Stack)。函数中的局部变量以及在函数调用过程中产生的临时变量都保存在此段中
根据以上内存布局可以知道,除非是堆边界内存可能被初始化或非初始化或栈内存踩掉,一般情况下不用担心这种问题。因此这种特殊情况,基本上不用太多的担心。