内存泄漏与防范
一个内存泄漏的例子
如果我们使用malloc()申请的内存在使用结束后没有及时被释放,则C标准库中的内存分配器ptmalloc和内核中的内存管理子系统都失去了对这块内存的追踪和管理。
#include <stdlib.h>
int main(void){
char *p;
p=(char *)malloc(32);
strcpy(p,“hello");
puts(p);
return 0;
}
在函数退出之前,如果我们没有使用free()函数及时 将 这 块 内 存 归 还 给 内 存 分 配 器 ptmalloc或内存管理子系统,ptmalloc和内存管理子系统就失去了对这块内存的控制权,它们可能认为用户还在使用这片内存。等下次去申请内存时,内存分配器和内存管理子系统都没有这块内存的信息,所以不可能把这块内存再分配给用户使用。
图中有大小为548 Byte和504 Byte的两个内存块,一开始这两个内存块是在空闲链表中的,当用户使用malloc()申请内存时,内存分配器ptmalloc将这两个内存块节点从空闲链表中摘除,并把内存块的地址返给用户使用。如果用户使用后忘了归还,那么空闲链表中就没有这两个内存块的信息,这两块内存也就无法继续使用了,在内存中就产生了两个漏洞,即发生内存泄漏。
预防内存泄漏
预防内存泄漏最好的方法就是:内存申请后及时地释放,两者要配对使用,内存释放后要及时将指针设置为NULL,使用内存指针前要进行非空判断。
内存泄漏检测:MTrace
MTrace,Valgrind,Dmalloc,purify,KCachegrind,MallocDebug
#include <mcheck.h>
void mtrace(void);
void muntrace(void);
如果想检测一段代码是否有内存泄漏,则可以把这两个函数添加到要检测的程序代码中。
#include <stdlib.h>
#include <string.h>
#include <mcheck.h>
int main (void){
mtrace();
//开启跟踪
char *p,*q;
p =(char *)malloc(8);
9 =(char *)malloc(8);
strcpy(p, “hello”);
strcpy(9,“world”);
free(p);
muntrace();//关闭跟踪
return 0;
}
根据动态内存的使用记录,我们可以很快定位到内存泄漏发生在mcheck.c文件中的第11行代码。
#mtrace a.out mtrace.log
Memory not freed:
address Size Caller
0x0901a380 0x8 at /home/c/mem leak/mcheck.c:11
广义上的内存泄漏
广义上的内存泄漏指系统频繁地进行内存申请和释放,导致内存碎片越来越多,无法再申请一片连续的大块内存。如fast bins,主要用来保存用户释放的小于80 Bytes(M_MXFAST)的内存,在提高内存分配效率的同时,带来了大量的内存碎片。
为了最大化地提高系统性能,我们可以通过一些参数对glibc的内存分配器进行调整,使之与我们的实际业务需求达到更大的匹配度,更高效地应对实际业务的需求。
glibc底层实现了一个mallopt()函数,可以通过这个函数对上面的各种参数进行调整。
#include <malloc.h>
int mallopt(int param, int value);
常见的内存错误及检测
处理器引入MMU后,操作系统接管了内存管理的工作,负责虚拟空间和物理空间的地址映射和权限管理。
内存管理子系统将一个进程的虚拟空间划分为不同的区域,如代码段、数据段、BSS段、堆、栈、mmap映射区域、内核空间等,每个区域都有不同的读、写、执行权限。
通过内存管理,每个区域都有具体的访问权限,如只读、读写、禁止访问等。数据段、BSS段、堆栈区域都属于读写区,而代码段则属于只读区,如果你往代码段的地址空间上写数据,就会发生一个段错误。
对于应用程序来说,常见的内存错误一般主要分为以下几种类型:内存越界、内存踩踏、多次释放、非法指针。
段错误
当我们往一个只读区域的地址空间执行写操作时,或者访问一个禁止访问的地址(如零地址)时,都会发生段错误。内核空间、零地址、堆和mmap区域之间的内存空间,这部分地址空间要么被内核占用,要么还处于“未开发”状态,需要申请才能使用。
(1)在调试链表时,通常通过指针来操作每一个节点。如果指针在遍历链表时已经指向链表的末尾或头部,指针已经指向NULL了,此时再通过该指针去访问节点的成员,就相当于访问零地址了,也会发生一个段错误,这个指针也就变成了非法指针。
(2)每一个用户进程默认有8MB大小的栈空间,如果在函数内定义大容量的数组或局部变量,就可能造成栈溢出,会引发一个段错误。内核中的线程也是如此,每一个内核线程只有8KB的内核栈,在实际使用中也要非常小心,防止堆栈溢出。
(3)使用malloc()申请的堆内存,如果不小心多次使用free()进行释放,通常也会触发一个段错误。
由于C语言语法检查的宽松性,程序中对内存访问的各种操作并不报错,或者给一个警告信息,这会导致程序在运行期间出现段错误时很难定位。此时我们可以借助一些第三方工具来快速定位段错误。
使用core dump调试段错误
在Linux环境下运行的应用程序,由于各种异常或Bug,会导致程序退出或被终止运行。此时系统会将该程序运行时的内存、寄存器状态、堆栈指针、内存管理信息、各种函数的堆栈调用信息保存到一个core文件中。
core dump功能开启后运行a.out,发生段错误后就会在当前目录下生成一个core文件,然后我们就可以使用gdb来解析这个core文件来定位程序到底出错在哪里。在GDB交互环境下,我们通过bt查看调用栈信息,可以查看错误的具体行数。
内存踩踏
比如申请两块动态内存,对其中的一块内存写数据时产生了溢出,就会把溢出的数据写到另一块缓冲区里。在缓冲区释放之前,系统是不会发现任何错误的,也不会报任何提示信息,但是程序却可能因为误操作,覆盖了另一块缓冲区的数据,造成程序莫名其妙的错误。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main (void){
char *p,*q;
p= malloc(16);
9= malloc(16);
strcpy(p, "hello world! hello zhaixue.cc!\n");
printf("%s\n",p);
printf("%s\n",q);
while (1);
free(q);
free(p);
return 0;
}
如果一个进程中有多个线程,多个线程都申请堆内存,这些堆内存就可能彼此相邻,使用时需要谨慎,提防越界。在内核驱动开发中,驱动代码运行在特权状态,对内存访问比较自由,多个驱动程序申请的物理内存也可能彼此相邻。
内存踩踏监测:mprotect
mprotect()是Linux环境下一个用来保护内存非法写入的函数,它
会监测要保护的内存的使用情况,一旦遇到非法访问就立即终止当前
进程的运行,并产生一个core dump。
#include <sys/mman.h>
int mprotect(void *addr, size t len, int prot);
mprotect()函数的第一个参数为要保护的内存的起始地址,len表示内存的长度,第三个参数prot表示要设置的内存访问权限。
内存检测神器:Valgrind
Valgrind包含一套工具集,其中一个内存检测工具Memcheck可以对我们的内存进行内存覆盖、内存泄漏、内存越界检测。