部分内存泄露与内存崩溃原因

部分内存泄露与内存崩溃原因

分析

内存管理是c++中无法避免的问题,因为语言本身基本没有自动的内存管理,所以如何更好的管理内存,发现潜在的容易引起内存问题的地方是十分重要的一个部分。

内存分配方式

c++里面内存分为5个部分,分别是堆、栈、自由存储区、全局/静态存储区、常量存储区。

常量存储区: 在编译时就已经确定值的常量会被存储在常量存储区。该区域内的值在程序的整个生命周期里都是可用的,而且不能被修改。修改该区域的内容是一个未定义的行为。其中会有一些编译器底层的优化,例如重叠字符串等。

栈: 栈存储自动变量,因为只有指针变化,所以分配通常比动态分配的内存要快很多。对象在分配空间后立即构造,在被销毁的时候会立即释放空间,所以不会有未初始化的栈空间被分配。

自由存储区: 两个动态内存区之一,通过new分配,delete释放。对象生命周期可以比空间分配的周期要短,也就是可以先分配空间而并不初始化对象,也可以销毁对象后不释放空间。在对象生命周期之外,依然可能通过void*来操作,但此时既没有非静态成员也没有成员函数可以被访问,它们的地址可能已被其他人使用或操作。

堆: 另一个动态内存区,通过malloc分配,free释放。虽然默认的全局new和delete函数可能在特定编译器下就是通过malloc和free来实现,但堆和自由存储区不能认为是一样的内存空间,同样由malloc分配的内存空间不能通过delete来释放,反之new和free也是不配对的。

全局/静态存储区: 在程序启动时分配,但在程序开始执行时才初始化。例如函数中的静态变量会在程序第一次执行到它的定义是才被初始化。跨编译单元的全局变量,初始化顺序是不确定的,需要特别注意管理全局/静态变量之间的依赖关系。通常,在对象生命周期之外,未初始化的对象也依然可以通过void*操作,但此时同样没有非静态成员和成员函数可以使用。

堆和自由存储区

虽然我们通常认为通过new/free操作的就是堆空间,但根据上面的描述其实可以看出堆和自由存储区还是不一样的两个概念,我觉得简单来说堆空间是操作系统维护的一块可以动态分配内存的空间,而自由存储区是属于C++定义的new和delete抽象出来的动态内存分配释放概念。c++默认编译器可以通过malloc和free来实现new和delete,此时自由存储区等于堆空间,但同样也可以通过重载new/delete操作符,来使用其他空间。例如可以做一个全局变量的对象池等等。所以不能认为堆和自由存储区是同样的概念。

堆与栈

首先简单回顾一下内存堆的特点,调用malloc会分配一块动态内存(堆)。

int *p = (int *)malloc(sizeof(int)*10);

这样的表达式,首先会通过new分配一块可以容纳10个int的数组的堆内存,然后p会分配一块栈空间,p里面存的值指向刚刚分配出来的堆空间。

堆和栈的区别如下:

  1. 管理方式:堆是由我们自己申请,自己释放,可能会由内存泄露的问题;栈是交由编译器自动管理,不需要手动释放。
  2. 空间大小:堆内存一般在32位系统下最大可以申请到4G,但是栈内存一般都有一定大小,例如VS里面默认大小只有1M,可以设置。
  3. 内存碎片:堆因为默认分配方式的问题,new/malloc寻找空闲适合大小内存的方式,释放时候十分容易造成大量碎片,而栈因为使用后入先出的模式释放,不会出现这个问题。
  4. 分配方式:堆内存从底向上分配,内存地址逐渐增加;而栈内存是从顶部向下分配,内存地址减小的方向,栈内存可以通过alloca函数动态分配,但是分配方式与堆内存不同。
  5. 效率:堆内存的分配效率比栈要低很多,栈有专门的指令分配内存。堆内存分配可能引起用户态和核心态的切换,代价很大。因此,函数调用过程中的参数、返回地址、EBP和局部变量都在栈中存储。

常见内存错误及相应的防错办法

  1. 内存泄露:常见原因是申请的空间未释放以及循环引用。

  2. 继续使用已被释放的内存:

    一般有下面几种情况:

    1. 调用复杂,循环调用释放等的操作引起。
    2. return 返回了指向栈的引用或指针,而该内存在函数体结束时已被释放。
    3. 释放内存后,没有把指针设为NULL,导致野指针。

    避免野指针的出现,有几个方面需要注意。

    • new/malloc分配内存之后应该及时检测,防止分配未成功的情况出现了而继续使用。
    • 指针或者数字声明之后应该立刻初始化赋初值,防止未初始化就被使用。
    • 数组和指针使用下标时要特别注意是否有越界,特别是边界情况。
    • 动态内存分配和释放一定要做好配对,防止内存泄露。
    • 内存被free或者delete后,应该立即把指针设为NULL,防止指针指向已被释放的空间。
  3. 读写内存越界:常见的也有两种情况,数组越界和内存覆盖(strcpy, strncpy, memcpy, strcat等函数使用不当造成)。

  4. 使用未初始化的内存:空悬指针

  5. 内存分配失败

    内存分配是可能出现不成功的情况的,可以考虑在new/malloc申请内存之后,使用诸如

    if (p == NULL)
    {
    	cout << “Out Of Memory” << endl;
    	exit(1);
    }
    

    的检测,并在判断出分配不成功的时候作其他处理。如果指针是一个函数参数,也可以考虑在函数入口先加上断言assert(p != NULL),以便在Debug模式下提前发现问题。

  6. 堆栈溢出:缓冲区溢出造成的堆栈溢出可能会造成数据覆盖在合法数据上,改变返回时返回函数的地址,严重时会让攻击者执行恶意程序。

查找内存问题的方法

Windows

如果使用VS编写程序,可以使用VS自带的工具来做内存泄露等内存问题的检查。

#define _CRTDBG_MAP_ALLOC
#include <stdlib.h>
#include <crtdbg.h>

#include <stdio.h>

using namespace std;

void func()
{
	int *p = (int *)malloc(10 * sizeof(int));
}

int main(int argc, char* argv[])
{
	func();

	cout << "Memory Info:" << endl;
	_CrtDumpMemoryLeaks();

	return 0;
}

包含 ctrdbg.h 头文件后,会将 malloc和free函数映射到debug版本 _malloc_dbg 和 _free_dbg,然后在需要转储内存信息的地方调用 _CrtDumpMemoryLeaks() 即可显示内存情况。

上面的代码运行后,会在输出区域显示:

Detected memory leaks!
Dumping objects ->
{147} normal block at 0x00134E98, 40 bytes long.
 Data: <                > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD 
Object dump complete.

Linux

使用valgrind查找内存问题

#include <stdlib.h>

using namespace std;

void func()
{
    int *p = (int*)malloc(10 * sizeof(int));
    p[10] = 1;

    p = NULL;
    cout << "error out:" << *p << endl;
}

int main(int argc, char* argv[])
{
    func();

    return 0;
}

可以看出上面的测试程序会出现几个内存问题,甚至会引起core。使用valgrind执行程序后,可以看到如下的提示:

root@herm-ThinkPad-X1-Carbon-2nd:/home/herm/Code/Learn# valgrind ./main
==30606== Memcheck, a memory error detector
==30606== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==30606== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==30606== Command: ./main
==30606== 
==30606== Invalid write of size 4
==30606==    at 0x108978: func() (main.cpp:15)
==30606==    by 0x1089D7: main (main.cpp:23)
==30606==  Address 0x5b7dca8 is 0 bytes after a block of size 40 alloc'd
==30606==    at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==30606==    by 0x10896B: func() (main.cpp:14)
==30606==    by 0x1089D7: main (main.cpp:23)
==30606== 
==30606== Invalid read of size 4
==30606==    at 0x1089A0: func() (main.cpp:18)
==30606==    by 0x1089D7: main (main.cpp:23)
==30606==  Address 0x0 is not stack'd, malloc'd or (recently) free'd
==30606== 
==30606== 
==30606== Process terminating with default action of signal 11 (SIGSEGV): dumping core
==30606==  Access not within mapped region at address 0x0
==30606==    at 0x1089A0: func() (main.cpp:18)
==30606==    by 0x1089D7: main (main.cpp:23)
==30606==  If you believe this happened as a result of a stack
==30606==  overflow in your program's main thread (unlikely but
==30606==  possible), you can try to increase the size of the
==30606==  main thread stack using the --main-stacksize= flag.
==30606==  The main thread stack size used in this run was 8388608.
error out:==30606== 
==30606== HEAP SUMMARY:
==30606==     in use at exit: 40 bytes in 1 blocks
==30606==   total heap usage: 3 allocs, 2 frees, 73,768 bytes allocated
==30606== 
==30606== LEAK SUMMARY:
==30606==    definitely lost: 40 bytes in 1 blocks
==30606==    indirectly lost: 0 bytes in 0 blocks
==30606==      possibly lost: 0 bytes in 0 blocks
==30606==    still reachable: 0 bytes in 0 blocks
==30606==         suppressed: 0 bytes in 0 blocks
==30606== Rerun with --leak-check=full to see details of leaked memory
==30606== 
==30606== For counts of detected and suppressed errors, rerun with: -v
==30606== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
Segmentation fault (core dumped)

首先可以看到一个4字节的内存写入错误,发生在p[10] = 1那一行,在上面的提示中就可以看到具体的代码位置,和具体的地址。
然后还能看到一个段错误,因为对0地址的读取,导致程序崩溃。同样可以看到发生在cout << "error out:" << *p << endl; 这一行。
最后还能看到内存泄露,有40字节的内存没有释放。

通过上面一个简单的例子可以看到valgrind检测十分强大,使用也比较简单。

原文地址:https://www.cnblogs.com/zhqherm/p/11885523.html