找回密码
 用户注册

QQ登录

只需一步,快速开始

查看: 3964|回复: 0

代码测试之内存越界

[复制链接]
发表于 2011-12-20 09:56:15 | 显示全部楼层 |阅读模式

【 声明:版权所有,欢迎转载,请勿用于商业用途。  联系信箱:feixiaoxing @163.com】

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

  • #define MAX_SET_STR_LENGTH  50
  • #define MAX_GET_STR_LENGTH 100

  • int* process(char* pMem, int size)  
  • {  
  •     char localMemory[MAX_SET_STR_LENGTH] = {0};  
  •     int* pData = NULL;  

  •     /*  code process */
  •     memset(localMemory, 1, MAX_GET_STR_LENGTH);  
  •     memmove(pMem, localMemory, MAX_GET_STR_LENGTH);  
  •     return pData;  
  • }  


    这段代码看上去没有什么问题。我们本意是对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的时候就可以判断内存的开头和结尾处有没有指针溢出。朋友们可以试一下下面这段代码。
view plain

  • void heap_memory_leak()  
  • {  
  •     char* pMem = (char*)malloc(100);  
  •     pMem[-1] = 100;  
  •     pMem[100] = 100;  
  •     free(pMem);  
  • }  

    pMem[-1] = 100是堆左溢出, pMem[100]是堆右溢出。


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

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

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

    b) 定义内存节点结构
view plain

  • typedef
    struct _MEMORY_NODE  
  • {  
  •     char functionName[64];  
  •     int line;  
  •     void* pAddress;  
  •     int size;  
  •     struct _MEMORY_NODE* next;  

  • }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)
view plain

  • void MEMMOVE_PROCESS(void* dst, const
    void* src, int size)  
  • {  
  •     MEMORY_NODE* pMemNode = check_node_exist(dst);  
  •     if(NULL == pMemNode) return;  

  •     assert(dst >= (pMemNode->pAddress));  
  •     assert(((char*)dst + size) <= ((char*)pMemNode->pAddress + pMemNode->size));  
  •         memmove(dst, src, size);  
  •     return;  
  • }  



  
e)下面就是内存节点的删除工作。
    我们知道函数是需要反复使用堆栈的。不同时间相同的堆栈地址对应的是完全不同的指针内容,这就要求我们在函数返回的时候对内存地址进行清理,把内存节点从对应的链表删除。
    我们知道在函数运行后,ebp和esp之间的内存就是通常意义上临时变量的生存空间,所以下面的一段宏就可以记录函数的内存空间。
view plain

  • #ifdef MEMORY_LEAK_TEST
  • #define FUNCTION_LOCAL_SPACE_RECORD()\
  • {\  
  •     int* functionBpRecord = 0;\  
  •     int*  functionSpRecord = 0;\  
  • }  
  • #else
  • #define FUNCTION_LOCAL_SPACE_RECORD()
  • #endif

  • #ifdef MEMORY_LEAK_TEST
  • #define FUNCTION_LEAVE_PROCESS()\
  • {\  
  • __asm { mov functionBpRecord, bp\  
  •     mov functionSpRecord, sp}\  
  •     FREE_MEMORY_NODE(functionBpRecord, functionSpRecord)\  
  • }  
  • #else
  • #define FUNCTION_LEAVE_PROCESS()
  • #endif


    这两段宏代码,需要插在函数的起始位置和结束的位置,这样在函数结束的时候就可以根据ebp和esp删除堆栈空间中的所有内存,方便了堆栈的重复使用。如果是全局内存,因为函数的变化不会导致地址的变化,所以没有必要进行全局内存节点的处理。
内存溢出检查流程总结:
    (1)对memset进行重新设计,记录除了malloc指针外的一切内存;
    (2)对memmove, strcpy, strncpy,strcat,sprintf等全部函数进行重新设计,因为我们需要对他们的指针运行范围进行判断;
    (3)在函数的开头和结尾位置添加宏处理。函数运行返回前进行节点清除。

(全文完)
您需要登录后才可以回帖 登录 | 用户注册

本版积分规则

Archiver|手机版|小黑屋|ACE Developer ( 京ICP备06055248号 )

GMT+8, 2024-5-2 21:49 , Processed in 0.020008 second(s), 7 queries , Redis On.

Powered by Discuz! X3.5

© 2001-2023 Discuz! Team.

快速回复 返回顶部 返回列表