堆内存损坏

时间:2015-04-07 07:16:15

标签: c++ windows memory-management heap

int main()
{
    char *p = new char[100];
    strcpy(p, "Test");
    cout << "Before heap corruption: " << p << endl;
    p[150] = '\0';

    cout << "after heap corruption: " << p;

    delete p;

    getchar();
    return 0;
}

在上面提到的代码中,我在一个不是我的内存位置写'\ 0',即使这样也没有抛出异常。 如果使用注释delete p运行上面的代码,则不会抛出任何异常。但是如果它被取消注释,则抛出附加的异常。因此,验证内存所有权的是 delete 。所以,我可以知道eaxctly删除是如何工作的以及为什么在写出内存块时有很多验证

enter image description here

5 个答案:

答案 0 :(得分:6)

这是未定义的行为。如果您不应该访问内存,那么任何事情都可能发生。没有必要验证你没有这样做;你可以自己编写你的程序,而不是。

如果你想要运行时验证,你需要更高级别的抽象而不是原始数组和指针:

std::vector<char> p(100);
p.at(150) = 0;              // out of bounds, throws exception

答案 1 :(得分:4)

instant 你使用未定义的行为,所有的赌注都关闭。

两周后,它可以完美地工作,立即失败或以某种模糊的方式失败。它甚至可以,如果它愿意,可以格式化你的硬盘并通过声卡嘲笑你: - )

在分配或取消分配内存之前它不会失败的可能原因是因为这是进行检查的理想时间,因为它是内存分配函数将分割或合并块以给你内存或取回它的时间。

您经常会看到Memory arena corrupted之类的消息,因为在尝试操作竞技场时,内存分配代码已经注意到结构已损坏(校验和和标记值可能不是预期的那样)。

抓住类似的东西:

p[150] = '\0';

通常需要相当多的运行时间开销,如果遵循规则则完全没有必要。

如果使用C ++集合(如vector)而不是直接数组,则可以运行时保护,但代价是原始性能。

答案 2 :(得分:1)

C ++使用&#34; Undefined behavior&#34;的概念。让编译器/操作系统决定他们想做什么。 C ++不会抛出异常(或很好地退出程序),因为执行所有这些操作需要在代码中进行额外的检查。在C ++中,你不会得到你不会付出的代价(即C ++对性能的关注很多)。

未定义的行为就是:任何事情都可能发生,当然有一定的可能性。访问不属于您的内存就是这样。

答案 3 :(得分:1)

随机将零写入内存位置不会产生可重复的行为,可能会或可能不会触发一种旨在让您免于麻烦的内存保护方法。

  

因此,删除可验证内存所有权。所以,我可以知道eaxctly删除是如何工作的

这取决于编译器的标准库实现。例如,在调试模式下,MSVC会在之前和之后填充具有已知模式的堆块,以便它可以检测到溢出。您将无法在发布模式下找到它,因为它会降低性能。

  

以及为什么在写出内存块

时存在任何验证

在每次内存访问周围插入边界检查会非常昂贵。执行此操作的代码检测工具确实存在。我记得在90年代期间使用 Bounds Checker ,我的天意很慢,但有时候找到一个困难的bug是最方便的。

也就是说,CPU通过其集成的MMU执行了一定量的access checks。如果您访问尚未分配给您的进程的内存,那么它将被捕获。同样,如果您尝试使用与其应该不匹配的模式access a page,那么它将被捕获。

答案 4 :(得分:0)

检查非常数组访问将需要至少两个额外的指令与每个索引操作 - 如果你想捕获s++ = 0;更多,其中s是指向某个数组的指针(不仅我们需要跟踪大小,以及指针区域开始的位置),如果数据也是动态分配的话,甚至更多(因为现在我们需要跟踪原始分配的位置,以及它的大小)。我的Pascal编译器中有数组边界检查,它增加了大约15%的开销 - 在某些情况下增加了300%,有些情况下增加了5-10%。但它仅适用于固定大小的阵列,而不是由于上述问题而动态分配的内存。对于大多数代码来说,5-15%并不是一个大问题。 300%的情况是一个问题 - 如果它也支持动态分配的内存,那就更糟了!

以上是我们知道记忆来自何处的简单情况&#34;。如果你有一个指针指向某个东西的函数怎么办呢?它需要为每个指针安排额外的存储来安排存储指针内存大小的某个地方 - 并且每个内存访问都必须读取内存,比较一下并且必须添加分支指令。通常,指针访问只是一条指令,所以我们现在添加了至少三条指令(以及一个永远不是好事的分支)。当然,这些数据必须在使用之前填写 - 理想情况下,这种数据不会破坏人们对内存中数据布局的想法......

这就是为什么使用valgrind和类似工具运行代码比运行&#34;全速&#34;慢大约10倍。

添加一些&#34;填充&#34; (又名&#34; crumble-zone&#34;)到内存分配,并在delete检查 - &#34;填充&#34;仍然完好无损是侵入性较小的,因此在大多数情况下是首选方法 - 它本身只有几个百分点慢,并且捕捉到了#34;你的代码没有按照你的预期行事,即使它没有&#34; 39;立刻抓住它。