VC在debug版和release版中分配堆的区别

问题现象及测试代码

     前几天有同事无意中用debug版的主程序调用了release版的dll中的某个输出函数,该函数new了一块内存并将指针返回给主程序并由后者释放,主程序在调用delete释放内存时断言失败。在排查问题时,同事念叨了一句“难道debug版和release版分配的内存不一样?”,这个问题我之前的确没有注意过,于是便进行了简单探索。本文内容主要针对VC6进行探索,其他版本VC的实现请大家自行研究。

探索过程

     既然问题出在释放环节,那么我就直接先从两个编译版本的堆释放部分看起。在debug版下比较好办,我们可以直接看到源代码,delete最终调用了DBGHEAP.C中的_free_dbg函数,该函数如下:

void __cdecl _free_dbg(void * pUserData, intnBlockUse)

{

       _CrtMemBlockHeader * pHead;

 

       /* 判断是否每次请求分配和释放内存时检查内存*/

       if (_crtDbgFlag & _CRTDBG_CHECK_ALWAYS_DF)

           _ASSERTE(_CrtCheckMemory());

 

                             /*检查传入的内存指针是否为空*/

       if (pUserData == NULL)

           return;

 

       /* 调用内存钩子函数,未做深入研究*/

       if (!(*_pfnAllocHook)(_HOOK_FREE, pUserData, 0,nBlockUse, 0L, NULL, 0))

       {

           _RPT0(_CRT_WARN, "Client hook free failure.\n");

 

           return;

       }

 

       /*判断指针是否是在本地堆*/

       _ASSERTE(_CrtIsValidHeapPointer(pUserData));

 

       /*获取内存对应的内存块头指针*/

       pHead = pHdr(pUserData);

 

       /* 验证块类型,在pHead->nBlockUse的低位字不为4和2,并且pHead->nBlockUse不等于1和3时断言报错*/

       _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));

 

       /*判断是否每次请求分配和释放内存时检查内存*/

       if (!(_crtDbgFlag & _CRTDBG_CHECK_ALWAYS_DF))

       {

           /* 检测堆下溢 */

           if (!CheckBytes(pHead->gap, _bNoMansLandFill, nNoMansLandSize))

                _RPT3(_CRT_ERROR, "DAMAGE:before %hs block (#%d) at 0x%08X.\n",

                   szBlockUseName[_BLOCK_TYPE(pHead->nBlockUse)],

                    pHead->lRequest,

                    (BYTE *) pbData(pHead));

 

                                           /*检测堆上溢 */

           if (!CheckBytes(pbData(pHead) + pHead->nDataSize, _bNoMansLandFill,nNoMansLandSize))

                _RPT3(_CRT_ERROR, "DAMAGE:after %hs block (#%d) at 0x%08X.\n",

                   szBlockUseName[_BLOCK_TYPE(pHead->nBlockUse)],

                    pHead->lRequest,

                    (BYTE *) pbData(pHead));

       }

 

                             /*关闭调试堆操作选项被打开*/

       if (pHead->nBlockUse == _IGNORE_BLOCK)

       {

                                           /*pHead->nLine != 0xFEDCBABC || pHead->lRequest != 0就断言失败报错 */

           _ASSERTE(pHead->nLine == IGNORE_LINE && pHead->lRequest ==IGNORE_REQ);

           /* 用_bDeadLandFill填充整个内存块(从内存块头开始) */

           memset(pHead, _bDeadLandFill,

                sizeof(_CrtMemBlockHeader) +pHead->nDataSize + nNoMansLandSize);

           _free_base(pHead);

           return;

       }

 

       /* CRT blocks can be freed as NORMALblocks */

       if (pHead->nBlockUse == _CRT_BLOCK && nBlockUse ==_NORMAL_BLOCK)

           nBlockUse = _CRT_BLOCK;

 

       /* Error if freeing incorrect memory type */

       _ASSERTE(pHead->nBlockUse == nBlockUse);

 

       /* 更新已分配的内存数 */

       _lCurAlloc -= pHead->nDataSize;

 

       /* 可再生内存不释放,提高性能*/

       if (!(_crtDbgFlag & _CRTDBG_DELAY_FREE_MEM_DF))

       {

           /* 更新链表 */

           if (pHead->pBlockHeaderNext)

           {

                pHead->pBlockHeaderNext->pBlockHeaderPrev= pHead->pBlockHeaderPrev;

           }

           else

           {

                _ASSERTE(_pLastBlock == pHead);

                _pLastBlock =pHead->pBlockHeaderPrev;

           }

 

           if (pHead->pBlockHeaderPrev)

           {

               pHead->pBlockHeaderPrev->pBlockHeaderNext =pHead->pBlockHeaderNext;

           }

           else

           {

                _ASSERTE(_pFirstBlock ==pHead);

                _pFirstBlock =pHead->pBlockHeaderNext;

            }

 

           /*用_bDeadLandFill填充整个内存块*/

           memset(pHead, _bDeadLandFill,

                sizeof(_CrtMemBlockHeader) +pHead->nDataSize + nNoMansLandSize);

           _free_base(pHead);

       }

       else

       {

           pHead->nBlockUse = _FREE_BLOCK;

 

           /*  将内存格式化为未分配状态*/

           memset(pbData(pHead), _bDeadLandFill, pHead->nDataSize);

       }

}

内存块头结构如下:

typedef struct _CrtMemBlockHeader
{
// Pointer to the block allocated just before thisone:
   struct _CrtMemBlockHeader*pBlockHeaderNext;
// Pointer to the block allocated just after thisone:
   struct _CrtMemBlockHeader*pBlockHeaderPrev;
   char *szFileName;    // File name
   int nLine;                  // Line number
   size_t nDataSize;      // Size of user block
   int nBlockUse;         // Type of block
   long lRequest;          // Allocation number
// Buffer just before (lower than) the user's memory:
   unsigned char gap[nNoMansLandSize];

} _CrtMemBlockHeader;

   
通过代码我们可以看出CRT堆对分配的每一块内存都通过链表进行了管理,在释放内存时,要根据头结构中的类型进行检查,同时还检查4字节大小的_CrtMemBlockHeader.gap,其被分配后会以0xFD填充,如果我们在使用过程中发生了堆下溢导致该部分内存被修改,在释放时CRT检查到该部分字节被修改就会断言失败报错。在我们申请的内存尾部,也有4字节的内存,被分配后同样以0xFD填充,如果我们在使用过程中发生了堆上溢导致该部分内存被修改,在释放时CRT检查到该部分字节被修改也会断言失败报错,由此我们可以看出debug版进行了很多比较严格的检查,可以帮助我们及早发现和定位问题。检查完内存和类型,释放内存之前,当然需要将该内存对应的链表位置进行更新,最后才是调用真正的释放操作_free_base。请注意传递给_free_base的指针并不是我们要释放的内存的地址,而是该地址向前32个字节,正好是头结构所在的地址,也就是说,在我们程序申请内存时,debug版的CRT会额外多申请36个字节的内存,其中32字节位于内存开始的地方,用于存放头结构,另外4字节位于内存尾部,用于检测内存上溢。

 

下面看release版的释放过程,由于没有源代码,我们就直接看汇编吧,流程比较简单。

004011A8              PUSH EBP

004011A9              MOV EBP,ESP

004011AB              PUSHECX

004011AC              PUSH ESI

004011AD             MOVESI,DWORD PTR SS:[EBP+8]

004011B0              TESTESI,ESI

004011B2              JESHORT 0040120E

004011B4       MOVEAX,DWORD PTR DS:[409CE8]  ;判断当前选择堆是否是V6版本堆

004011B9              CMPEAX,3

004011BC              JNZSHORT 004011D4

004011BE              PUSHESI

004011BF              CALL00402A8C

004011C4              POP ECX

004011C5              TEST EAX,EAX

004011C7              PUSH ESI

004011C8              JE SHORT 00401200

004011CA              PUSHEAX

004011CB              CALL00402AB7

004011D0              POPECX

004011D1              POPECX

004011D2              JMPSHORT 0040120E

004011D4       CMPEAX,2                         ;判断当前选择堆是否是V5版本堆

004011D7              JNZSHORT 004011FF

004011D9              LEAEAX,DWORD PTR SS:[EBP+8]

004011DC              PUSHEAX

004011DD             LEAEAX,DWORD PTR SS:[EBP-4]

004011E0              PUSHEAX

004011E1              PUSHESI

004011E2              CALL004034F1

004011E7              ADDESP,0C

004011EA              TESTEAX,EAX

004011EC              JESHORT 004011FF

004011EE              PUSHEAX

004011EF              PUSHDWORD PTR SS:[EBP+8]

004011F2               PUSH DWORD PTR SS:[EBP-4]

004011F5               CALL 00403548

004011FA              ADDESP,0C

004011FD              JMPSHORT 0040120E

004011FF              PUSHESI                    ;系统堆,释放的地址就是一开始申请的地址

00401200               PUSH0                                                   ; Flags = 0

00401202               PUSHDWORD PTR DS:[409CE4]                       ; hHeap = NULL

00401208               CALLDWORD PTR DS:[<&KERNEL32.HeapFree>]           ;直接释放了

0040120E              POPESI

0040120F              LEAVE

00401210               RETN

      可以看出,debug版和release版中CRT堆的操作是不一样的,debug版在申请内存时会额外申请36字节的内存以帮助管理和调试,也因此就造成debug版和release版模块申请和释放内存不能混用;同时也再次提醒我们,在编写代码时一定要在debug模式下进行调试,编译器可以帮助我们发现和定位很多问题,否则在release版本下问题暴露时已经错过了有问题代码所在的位置,徒增了调试的难度。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值