关于内存管理之泄露与越界

一.

在 我们个人编程的过程当中,内存泄露虽然不会像内存溢出那样造成各种莫名奇妙的问题,但是它的危害也是不可忽视的。一方面,内存的泄露导致我们的软件在运行 过程中占用了越来越多的内存,占有资源而又得不到及时清理,这会导致我们程序的效率越来越低;另一方面,它会影响我们用户的体验,失去市场的竞争能力。

    常见的内存泄露是这样的:

  1. void process(int size)  
  2. {  
  3.     char* pData = (char*)malloc(size);  
  4.   
  5.     /* other code  */  
  6.       
  7.     return; /* forget to free pData */  
  8. }  
    如上图所示,我们在函数process的处理过程中,每一次都需要对内存进行申请,但是在函数结束的时候却没有进行释放。如果这样的一段代码出现在业务 侧,那么后果是难以想象的。举个例子来说,如果我们服务器每秒钟需要接受100个用户的并发访问,每个用户过来的数据,我们都需要本地申请内存重新保存一 份。处理结束之后,如果内存没有得到很好地释放,就会导致我们服务器可用的物理内存越来越少。一旦达到某一个临界点之后,操作系统不得不通过内外存的调度 来满足我们申请新内存的需求,这在另一方面来讲又会降低服务器服务的质量。

    内存泄露的危害是不言而喻的,但是查找内存泄露却是一件苦难而且复杂的工作。我们都知道,解决bug是一件非常简单的事情,但是寻找bug的出处却是一 件非常吃力的事情。因此,我们有必要在自己编写代码的时候,就把查找内存泄露的工作放在很重要的位置上面。那么有没有什么办法来解决这一问题呢?

    我想要做到解决内存泄露,必须做到下面两个方面:

    (1)必须记录内存在哪个函数申请的,具体文件的行数是多少

    (2)内存应该什么时候被释放

   要完成第1个条件其实并不困难。我们可以用节点的方法记录我们申请的内存:

    a)设置节点的数据结构

  1. typedef struct _MEMORY_NODE  
  2. {  
  3.     char functionName[64];  
  4.     int line;  
  5.     void* pAddress;  
  6.     struct _MEMORY_NODE* next;  
  7.   
  8. }MEMORY_NODE;  
    其中 functionName记录函数名称,line记录行数, pAddress记录分配的地址, next记录下一个内存节点。

    

    b)修改内存的分配函数

    对业务侧的malloc进行函数修改,添加下面一句宏语句

    #define malloc(param)  MemoryMalloc(__FUNCTION__, __LINE__, param)

    在桩函数侧书写下面的代码

  1. void* MemoryMalloc(const char* name, int line, int size)  
  2. {  
  3.     void* pData = (void*)malloc(size);  
  4.     MEMORY_NODE* pMemNode = NULL;  
  5.     if(NULL == pData) return NULL;  
  6.     memset((char*)pData, 0, size);  
  7.   
  8.     pMemNode = (MEMORY_NODE*)malloc(sizeof(MEMORY_NODE));  
  9.     if(NULL == pMemNode){  
  10.         free(pData);  
  11.         return NULL;  
  12.     }  
  13.     memset((char*)pMemNode, 0, sizeof(MEMORY_NODE));  
  14.     memmove(pMemNode->functionName, name, strlen(name));  
  15.     pMemNode->line = line;  
  16.     pMemNode->pAddress = pData;  
  17.     pMemNode->next = NULL;  
  18.     add_memory_node(pMemNode);  
  19.   
  20.     return pData;  
  21. }  

    内存的分配过程中还涉及到了节点的添加,所以我们还需要添加下面的代码

  1. static MEMORY_NODE* gMemNode = NULL;  
  2.   
  3. void add_memory_node(MEMORY_NODE* pMemNode)  
  4. {  
  5.     MEMORY_NODE* pNode = gMemNode;  
  6.     if(NULL == pMemNode) return;  
  7.     if(NULL == gMemNode){  
  8.         gMemNode = pMemNode;  
  9.         return;  
  10.     }  
  11.   
  12.     while(NULL != pNode->next){  
  13.         pNode = pNode->next;  
  14.     }  
  15.     pNode->next = pMemNode;  
  16.     return;  
  17. }  
    文中gMemNode表示所有内存节点的根节点,我们每增加一次malloc过程就会对内存节点进行记录。在记录过程中,我们还会记录调用malloc的函数名称和具体文件行数,这主要是为了方便我们在后面进行故障定位的时候更好地查找。

   完成了第一个条件之后,我们就要对第二个条件进行完成。

   a)内存什么时候释放,这取决于我们在函数中是怎么实现的,但是我们在编写测试用例的时候却是应该知道内存释放没有,比如说如果测试用例全部结束了,我们有理由相信assert(gMemNode == NULL)这应该是恒等于真的。

    b)内存释放的时候,我们应该做些什么?和节点的添加一样,我们在内存释放的时候需要free指定的内存,free节点,free节点的内存,下面就是在释放的时候我们需要进行的操作


    对业务侧的free函数进行修改,添加下面一句宏代码,

    #define free(param)      MemoryFree(param)


    在桩函数侧输入下面的代码:

  1. void MemoryFree(void* pAddress)  
  2. {  
  3.     if(NULL == pAddress) return;  
  4.     delete_memory_node(pAddress);  
  5.     free(pAddress);  
  6. }  

    在删除内存的时候,需要删除节点,删除节点的内存

  1. void delete_memory_node(void* pAddress)  
  2. {  
  3.     MEMORY_NODE* pHead = gMemNode;  
  4.     MEMORY_NODE* pMemNode = gMemNode;  
  5.     while(NULL != pMemNode){  
  6.         if(pAddress == pMemNode->pAddress)  
  7.             break;  
  8.         pMemNode = pMemNode->next;  
  9.     }  
  10.     if(NULL == pMemNode) {  
  11.         assert(1 == 0);  
  12.         return;  
  13.     }  
  14.   
  15.     while(pMemNode != pHead->next){  
  16.         pHead = pHead->next;  
  17.     }  
  18.   
  19.     if(pMemNode == gMemNode){  
  20.         gMemNode = gMemNode->next;  
  21.     }else{  
  22.         pHead->next = pMemNode->next;  
  23.     }  
  24.     free(pMemNode);  
  25.     return;  
  26. }  

    有了上面一小段代码的帮助,我们在编写测试用例的时候,就可以在函数执行后,通过判断内存节点是否为空的方法判断内存是否已经释放。如果内存没有释放,我们还能通过节点的信息帮助我们是哪里发生了错误,但是这个方法还有两个缺点:

    (1)没有考虑缓存的情况,好多内存分配了之后并不会在函数中马上释放,而是放在缓存池中等待下一次调用,这就需要我们准确把握和判断了。

    (2)代码中节点删除和添加的时候没有考虑多进程的情形,应该考虑用一个互斥锁或者是信号量加以保护。

二.

 内存越界是我们软件开发中经常遇到的一个问题。不经意间的复制常常导致很严重的后果。经常使用memset、memmove、strcpy、 strncpy、strcat、sprintf的朋友肯定对此印象深刻,下面就是我个人在开发中实际遇到的一个开发问题,颇具典型。

  1. #define MAX_SET_STR_LENGTH  50  
  2. #define MAX_GET_STR_LENGTH 100  
  3.   
  4. int* process(char* pMem, int size)  
  5. {  
  6.     char localMemory[MAX_SET_STR_LENGTH] = {0};  
  7.     int* pData = NULL;  
  8.   
  9.     /*  code process */  
  10.     memset(localMemory, 1, MAX_GET_STR_LENGTH);  
  11.     memmove(pMem, localMemory, MAX_GET_STR_LENGTH);  
  12.     return pData;  
  13. }  

    这段代码看上去没有什么问题。我们本意是对localMemory进行赋值,然后拷贝到pMem指向的内存中去。其实问题就出在这一句memset的大 小。根据localMemory初始化定义语句,我们可以看出localMemory其实最初的申明大小只有MAX_SET_STR_LENGTH,但是 我们赋值的时候,却设置成了MAX_GET_STR_LENGTH。之所以会犯这样的错误,主要是因为MAX_GET_STR_LENGTH和 MAX_SET_STR_LENGTH极其相似。这段代码编译后,产生的后果是非常严重的,不断冲垮了堆栈信息,还把返回的int*设置成了非法值。

    那么有没有什么好的办法来处理这样一个问题?我们可以换一个方向来看。首先我们查看,在软件中存在的数据类型主要有哪些?无非就是全局数据、堆数据、栈 临时数据。搞清楚了需要控制的数据之后,我们应该怎么对这些数据进行监控呢,一个简单有效的办法就是把memset这些函数替换成我们自己的函数,在这些 函数中我们严格对指针的复制、拷贝进行判断和监督。

    (1)事实上,一般来说malloc的数据是不需要我们监督的,因为内存分配的时候,通常库函数会比我们要求的size多分配几个字节,这样在free的时候就可以判断内存的开头和结尾处有没有指针溢出。朋友们可以试一下下面这段代码。

  1. void heap_memory_leak()  
  2. {  
  3.     char* pMem = (char*)malloc(100);  
  4.     pMem[-1] = 100;  
  5.     pMem[100] = 100;  
  6.     free(pMem);  
  7. }  
    pMem[-1] = 100是堆左溢出, pMem[100]是堆右溢出。

    

    (2)堆全局数据和栈临时数据进行处理时,我们利用memset初始化记录全局指针或者是堆栈临时指针

    a) 首先对memset处理,添加下面一句宏语句

    #define memset(param, value, size)      MEMORY_SET_PROCESS(__FUNCTION__, __LINE__, param, value, size)

 

    b) 定义内存节点结构

  1. typedef struct _MEMORY_NODE  
  2. {  
  3.     char functionName[64];  
  4.     int line;  
  5.     void* pAddress;  
  6.     int size;  
  7.     struct _MEMORY_NODE* next;  
  8.   
  9. }MEMORY_NODE;  

    其中functionName记录了函数名称,line记录文件行数, pAddress记录了指针地址, size指向了pAddress指向的内存大小,next指向下一个结构节点。

 

     c)记录内存节点属性

    在MEMORY_SET_PROCESS处理过程中,不仅需要调用memset函数,还需要对当前内存节点进行记录和保存。可以通过使用单链表节点的方 法进行记录。但是如果发现pAddress指向的内存是malloc时候分配过的,此时就不需要记录了,因为堆内存指针溢出的问题lib库已经帮我们解决 了。

 

    d)改造原有内存指针操作函数

    比如对memmove等函数进行改造,不失去一般性,我们就以memmove作为范例。

    添加宏语句 #define memmove(dst, src, size)        MEMMOVE_PROCESS(dst, src, size)

  1. void MEMMOVE_PROCESS(void* dst, const void* src, int size)  
  2. {  
  3.     MEMORY_NODE* pMemNode = check_node_exist(dst);  
  4.     if(NULL == pMemNode) return;  
  5.   
  6.     assert(dst >= (pMemNode->pAddress));  
  7.     assert(((char*)dst + size) <= ((char*)pMemNode->pAddress + pMemNode->size));  
  8.         memmove(dst, src, size);  
  9.     return;  
  10. }  

 

    e)下面就是内存节点的删除工作。

    我们知道函数是需要反复使用堆栈的。不同时间相同的堆栈地址对应的是完全不同的指针内容,这就要求我们在函数返回的时候对内存地址进行清理,把内存节点从对应的链表删除。

    我们知道在函数运行后,ebp和esp之间的内存就是通常意义上临时变量的生存空间,所以下面的一段宏就可以记录函数的内存空间。

  1. #ifdef MEMORY_LEAK_TEST  
  2. #define FUNCTION_LOCAL_SPACE_RECORD()\  
  3. {\  
  4.     int* functionBpRecord = 0;\  
  5.     int*  functionSpRecord = 0;\  
  6. }  
  7. #else  
  8. #define FUNCTION_LOCAL_SPACE_RECORD()  
  9. #endif  
  10.   
  11. #ifdef MEMORY_LEAK_TEST  
  12. #define FUNCTION_LEAVE_PROCESS()\  
  13. {\  
  14. __asm { mov functionBpRecord, bp\  
  15.     mov functionSpRecord, sp}\  
  16.     FREE_MEMORY_NODE(functionBpRecord, functionSpRecord)\  
  17. }  
  18. #else  
  19. #define FUNCTION_LEAVE_PROCESS()  
  20. #endif  

    这两段宏代码,需要插在函数的起始位置和结束的位置,这样在函数结束的时候就可以根据ebp和esp删除堆栈空间中的所有内存,方便了堆栈的重复使用。如果是全局内存,因为函数的变化不会导致地址的变化,所以没有必要进行全局内存节点的处理。

内存溢出检查流程总结:

    (1)对memset进行重新设计,记录除了malloc指针外的一切内存;

    (2)对memmove, strcpy, strncpy,strcat,sprintf等全部函数进行重新设计,因为我们需要对他们的指针运行范围进行判断;

    (3)在函数的开头和结尾位置添加宏处理。函数运行返回前进行节点清除。

转载于:https://my.oschina.net/u/159782/blog/34250

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值