生死疆界-在new与delete之间

作者:土豆

问题源自一段简单的代码:
void main()
{
 char *p = new char;
 cin>>p;
 cout<<p[2];
 delete p;

在以上代码中,如果你输入:abcd,那么如你所望,你会看到"正确"的输出"c"。但是会有错误提示出现: 
Debug Error!
Program: test.exe
DAMAGE: after Normal block(#64) at 0x003429f8

更离奇的是,如果将代码改为如下的代码:

 void()
{
char *p = new char;
cin>>p;
cout<<p;
delete p;
}

        如果只输入一个字符a,那么依然报错。是不是奇怪,分配了一个字符,输入了一个字符,那么错在哪里? 注意,最开始那行Debug Error!说明这是在Debug编译模式下才有的提示,如果你换到release频道,那么此提示不再出现,你成功得到了"c",仿佛程序一切正常。 

一个奇怪的现象是,如果去掉delete p这条语句,这个运行时错误消失了,甚至你在debug模式下也看不到这个提示。 问题何在? 

以前我遇到过这种情况,分析后归结为一个结论:在debug模式下系统有一定的机制侦测到内存的非法访问。然后就放过这个问题。这个结论说了等于没说,关键在于,这种机制的具体运做过程。这次我下了狠心,不入虎穴,焉得虎子。我决定追进源代码里边去。 把编译环境设置成debug模式,很显然,问题出在delete p上,在这条语句设置断点,按F5,程序运行到这条语句前自动暂停,然后按F11。 

Welcome to the Source Code World! 

首先来到DELOP.CPP文件中,这个文件短小精悍,只有一个函数 void __cdecl operator
delete(void *p) _THROW0()
{ // free an allocated object
 free(p);
}

没有任何有用的信息,那就继续追进free(p)里。 不一会,我们追到了DBGHEAP.C中,你从文件名可以看出,这是在debug模式下才能进入的文件。 

最后在_CRTIMP void __cdecl _free_dbg(void * pUserData, int nBlockUse )中的这条语句

 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存在,那么CheckBytes()一定是执行某种检验,如果检验失败,调用_RPT3()函数 在MSDN中,对_RPT函数族有这样的解释: 

Track an application''s progress by generating a debug report (debug version only).

_RPT3的作用就是产生一个错误报告。 
好了,知道了这一点就足够了,它对我们来说没什么意义了。那么只剩下CheckBytes了,深呼吸几口,好了,让我们进去吧。 static int __cdecl CheckBytes(unsigned char *
 pb, unsigned char bCheck, size_t nSize)
{
        int bOkay = TRUE;
        while (nSize--)
        {
            if (*pb++ != bCheck)
            {
               _RPT3(_CRT_WARN, "memory check error at 0x%08X = 0x%02X, should
 be 0x%02X.\n",
                    (BYTE *)(pb-1),*(pb-1), bCheck);
                bOkay = FALSE;
            }
        }
        return bOkay;
}

你看到了,这个函数只调用了_RPT3,再也没有其他的调用,看来,我们到头了。 下面是微软的程序员为这个函数写的注释的一部分:

 *Purpose:
*       verify byte range set to proper value
*Return:
*       TRUE - if all bytes in range equal bcheck
*       FALSE otherwise

再明显不过了,这个函数检验一定范围的位是否设定为了正确的值(就是传进来的那么bCheck),如果正确,返回bOkay=TRUE,否则,返回bOkay=FALSE. 都挖完了,再也没有任何有用的信息,我们仍旧不知道微软是如何进行校验的,眼前依然一片黑暗。如果还有黎明的曙光,那么只能从传入的参数身上发出,呵呵,它们三肩负着我们的厚望啊。看看第一个参数unsigned char* pb。 if (*pb++ != bCheck)这条语句告诉我们要将pb所指内存地址的指与bCheck比较,那么我们还有最后一线希望:直接监视内存。 

 

欲知后事如何,且听下回分解 :)

 

上回《生死疆界(上)》说到: 咱们对着微软在背后做的手脚深感困惑,于是备好车马粮草,一路追杀进去,最后走进了死胡同,剩下的最后希望就是在这里摸摸,那里敲敲,看有没有暗藏机关。

  这正是:探源码身陷绝境,求解脱心系参数。

  还记得我们在delete p;前设的断点吗?好,让我们重新开始调试,按F5,从控制台输入"abcd",然后到这条语句前停止了,查看变量p的值,是0x00342c40,那好,打开vc监视内存的窗口memory,我们查看这个地址的值:

  

  此后的内存情况不再用图片显示。只用红色标志的内存表示发生了变化的内存

  看到了吗?你的宝贝"abcd"乖乖地躺在内存中,其后跟了一个0x00,那表示'''',字符串结束标志。一切都很正常,到底哪里出错了?难道是delete p用错了,而应该用delete[] p?try it,你会发现依然有相同的错误。

  从这段内存中仍然看不出问题,仿佛一切风平浪静,其实是我们来晚了,在delete p前,内存早已经发生了翻天覆地的变化。 再一次重新进入程序,这次我们从一开始就监视内存。

  00342C40 EE FE EE FE EE FE EE 铪铪铪.

  00342C47 FE EE FE EE FE EE FE .

  这是char *p = new char,执行前的内存。下面是执行后的:

  00342C40 CD FD FD FD FD F0 AD 妄.

  00342C47 BA 0D F0 AD BA 0D F0 ..瓠...

  看不出什么问题,再往下执行吧:(cin>>p, 这次我们输入ab)

  00342C40 61 62 00 FD FD F0 AD ab.瓠

  00342C47 BA 0D F0 AD BA 0D F0 ..瓠...

  ab正确地放进了内存中,而且你可以看到cin还体贴地在ab后为你放了一个''''

  还是没有什么问题?再往下走就是cout<<p了,它不会改动内存,再往下就到了delete p,到那时一切都晚了。 没错,就是这一步,静静的内存中早已经翻江倒海。

   还记得static int __cdecl CheckBytes(unsigned char * pb, unsigned char bCheck, size_t nSize)中的bCheck, nSize吗? 如果当初你也监视变量的话,会发现bCheck = 253, nSize = 4。这就是这个内存侦测机制的命门。小时候喜欢看武打片,有一部叫做〈鹰爪铁布衫〉的,当时令我如痴如醉啊,看过的人一定还记得最后杀那老头的时候是先在 他天灵上一拍,接着再在裤裆上捏一把,呵呵,bCheck就是天灵,nSize就是裤裆。

  把253转换为16进制,是什么,没错,是 FD。呵呵,别忙往下看,想一想,你找到真相了吗? 再看一眼char *p = new char执行后的内存,你发现了什么?p指向0x00342c40那个字节的值为CD,这是属于你的内存,看看后边跟的是什么,不多不少,恰恰是4个 FD,恰恰是nSize个bCheck!

  这个侦测内存非法访问的机制现在已经被我们开膛破肚了。微软在你申请的空间后加上四个FD,如 果你访问了你非法访问内存,那么这些内存的内容将被改变(有一个问题我没有解决,我不知道FD代表什么,望知道的兄弟教我),在delete时,将检查由 new产生的''''结束符后是否有连续四个字节都是FD,如果有证明没有发生非法内存访问,如果没有,那就该让_RPT3老兄出马了。

  对于

char *p = new char;
cin>>p;
cout<<p;
delete p;
这段代码,如果只输入一个字符a,cin>>p执行后的内存为

  00342C40 61 00 FD FD FD F0 AD a..

  00342C47 BA 0D F0 AD BA 0D F0 ..瓠...

  虽然你只用了你申请的内存,但是cin为了讨好你给你加那个'''',覆盖了一个FD,这样,delete时照样报错,如果你这样做char *p = new char[2];那么cin>>p后内存为

  00342C40 61 00 FD FD FD FD AD a..

  00342C47 BA 0D F0 AD BA 0D F0 ..瓠...

  那么程序将不会报错。 四个FD就是内存的生死疆界,超过这个疆界,呵呵,听见远方传来的崩溃的声音了吗? 到此,一切真相大白,山高月小,水落石出!

  (尾声:以上所有内容皆来自笔者独立分析,其中难免有错,更甚者,也许我大错特错,压根就不是这样的机制。如果你发现其中有不正确的地方,请指出,谢谢,在下感激不尽)。

 

最后定位到代码

ans = new int[size_a+size_b];        // 动态分配数组空间

delete [] ans;

一把delete [] ans;删掉程序就不弹出错误,这是为什么呢??

错误原因:

因为对内存的操作越界了,超出所分配的内存的边界。

解决:

增大分配的内存!   

e.g.

ans = new int[size_a+size_b +1 ];     

或者加到自己适宜的大小,问题即可解决...

总结:

对内存的操作要细之又细,new完后要delete,操作时不要越界(包括向前越或向后越).......

 

heap corruption detected: CRT detected that the application wrote to memory


错误的现象是这样的:

在程序的开始处我申请了一块内存,中间对其进行了一些操作,在程序结束处,释放内存的时候,引起错误:

HEAP CORRUPTION DETECTED:after Normal block(#***) at 0x****.CRT detected that application wrote memory after end of heap buffer.

错误原因:

以对内在操作的过程中,所写的地址超出了,所分配内在的边界

解决办法:

在可能出错的代码处,使用_CrtCheckMemory进行检测

比如:

int* p = new int[2];
*(p+2) = 1;
_ASSERTE( _CrtCheckMemory( ) );

简单内存调试技术

一 检查内存泄漏

添加以下语句:

#define CRTDBG_MAP_ALLOC

#include <stdlib.h>

#include <crtdbg.h>

以上代码第一行的作用是:输出调息信息的同时输出一些附加信息,例如分配该泄漏内存块的代码所在的文件名、行数。第二、第三行将 malloc 和 free 函数映射到“Debug”版本_malloc_dbg 和_free_dbg,以跟踪内存分配和释放。此映射只发生在调试版本。Release版本使用普通的 malloc 和 free 函数。

程序退出时调用_CrtDumpMemoryLeaks()输出调试信息到VC的调试窗口,也可以设置一个标志,让程序退出时自动输出调试信息,如下所示:

_CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG )|_CRTDBG_LEAK_CHECK_DF)



有时程序泄漏信息没有文件名和行数信息,例如:

Detected memory leaks!

Dumping objects ->

{52} normal block at 0x06AC2C88, 1234 bytes long.

Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD

Object dump complete.

上面的{52}表示该内存块的编号,大多数情况下是固定的。可以将该编号传递给 _CrtSetBreakAlloc以创建一个断点。执行将恰在分配该块以前中断,您可以向上追踪以确定哪个例程执行了错误调用。接着退出程序,观察"输出窗口"的内存泄露信息,验证实际内存分配的编号是不是和预设值相同,如果不相同,说明刚才的断点是无效的,需要尝试其它编号再次跟踪。

二 内存操作越界检查

有时出现内存操作越界,这时可以借助_CrtCheckMemory函数校验所有已分配内存块的有效性(例如内存块两侧边界是否被改写),通过在所有可疑点插入以下代码,定位发生内存越界的代码位置:ASSERT(_CrtCheckMemory());

同时也可以配合使用_CrtSetDbgFlag函数设置_CRTDBG_CHECK_ALWAYS_DF标志,这样每次分配/释放内存时,系统会自动调用_CrtCheckMemory,有助于快速捕获内存错误。

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值