【问题标题】:Why does not stack overflow/underflow trigger an run-time error?为什么堆栈上溢/下溢不会触发运行时错误?
【发布时间】:2015-08-13 21:43:46
【问题描述】:

我用这个代码sn-p:

// stackoverflow.c 
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main(int argc, char** argv)
{
    int i;
    int a[10];
    // init
    a[-1] = -1;
    a[11] = 11;
    printf(" a[-1]= = %d, a[11] = %d\n", a[-1], a[11]);
    printf("I am finished.\n");
    return a[-1];
}

编译器是 GCC for linux x86。它运行良好,没有任何运行时错误。我还在 Valgrind 中测试了这段代码,它也不会触发任何内存错误。

$ gcc -O0 -g -o stack_overflow stack_overflow.c
$ ./stack_overflow
   a[-1]= = -1, a[11] = 11
   I am finished.

$ valgrind ./stack_overflow
==3705== Memcheck, a memory error detector
==3705== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==3705== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==3705== Command: ./stack_overflow
==3705==
   a[-1]= = -1, a[11] = 11
   I am finished.
==3705==
==3705== HEAP SUMMARY:
==3705==     in use at exit: 0 bytes in 0 blocks
==3705==   total heap usage: 0 allocs, 0 frees, 0 bytes allocated
==3705==
==3705== All heap blocks were freed -- no leaks are possible
==3705==
==3705== For counts of detected and suppressed errors, rerun with: -v
==3705== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

据我了解,堆和栈是同一种内存。唯一的区别是它们的生长方向相反。

所以我的问题是:

为什么堆上溢/下溢会触发 rum-time 错误,而堆栈上溢/下溢不会?

为什么 C 语言设计者没有像堆一样考虑到这一点,而是将其保留为 Undefined Behaviour

【问题讨论】:

  • 这段代码不会触发堆栈溢出。尝试制作一个无限递归函数。
  • Valgrind 应该已经捕获了超出数组末尾的写入。
  • 因为很难知道 -1 的索引是堆栈不足/溢出!它未分配,但仍然存在,并且在极少数情况下进行这种访问是有效的(C++ 分配器可能会进行这种访问)。
  • 必须 .. 查找 .. 重复 .. 'heap' 和 'stack' 是 physical 构造 - 将它们弄乱会导致 CPU 故障。这与您在这里所做的事情无关(嗯,很少)。您最后一个问题的最常见答案是,“因为由程序员确保不会发生这种情况”。 C 被设计为一种精益语言。

标签: c memory stack stackunderflow


【解决方案1】:

valgrind 不检测堆栈缓冲区溢出。使用AddressSanitizer。至少需要 gcc 4.8 并且必须安装 libasan。

gcc -g -fsanitize=address stackbufferoverflow.c

==1955==ERROR: AddressSanitizer: stack-buffer-underflow on address 0x7fffff438d4c at pc 0x000000400a1d bp 0x7fffff438d10 sp 0x7fffff438d00
WRITE of size 4 at 0x7fffff438d4c thread T0
    #0 0x400a1c in main /home/m/stackbufferoverflow.c:9
    #1 0x7fe7e24e178f in __libc_start_main (/lib64/libc.so.6+0x2078f)
    #2 0x400888 in _start (/home/m/a.out+0x400888)

Address 0x7fffff438d4c is located in stack of thread T0 at offset 28 in frame
    #0 0x400965 in main /home/m/stackbufferoverflow.c:5

  This frame has 1 object(s):
    [32, 72) 'a' <== Memory access at offset 28 underflows this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-buffer-underflow /home/m/stackbufferoverflow.c:9 main

【讨论】:

    【解决方案2】:

    为什么 C 语言设计者没有像堆一样考虑到这一点,而是将其保留为 Undefined Behaviour

    最初的 C 语言设计者为自己编写了一种更舒适、更便携的汇编程序。原始语言的设计并不是为了防止程序员的错误。

    如果您对相反的示例感兴趣,请查看 Ada (http://en.wikipedia.org/wiki/Ada_%28programming_language%29)。

    【讨论】:

      【解决方案3】:

      C 不检查越界数组索引之类的东西。它只是按照您的指示进行操作,在这种情况下,更改包含 10 个元素的数组中的第 11 个元素。通常,这意味着您的程序将写入内存中应该存储该项目的位置(如果它存在的话)。这可能会或可能不会导致某种可见的错误,例如崩溃。它可能没有效果,或者它可能会让你的程序做一些奇怪的事情。这取决于内存中的那个地方碰巧存储了什么(如果有的话),以及它是如何使用的。

      其他一些编程语言会执行这些检查,并保证会报告错误。 C 标准没有提供这样的保证,只是说它会导致“未定义的行为”。这样做的一个原因是应该可以用 C 编写非常高效的程序,其中检查会导致一个小的但在某些情况下可能是不可接受的延迟。此外,在设计 C 的时候,计算机速度较慢,延迟会是一个更严重的问题。

      C 中也不保证会检测或报告堆错误。 Valgrind 不是 C 语言的一部分,而是一个不同的工具,它尽最大努力使用其他比 C 更有效的机制来查找错误,但不能保证它会找到所有错误.

      【讨论】:

        【解决方案4】:

        编辑

        这是一个有趣的教程:

        http://gribblelab.org/CBootcamp/7_Memory_Stack_vs_Heap.html

        顺便说一句 Clang (OSX) 检测到它,但它只是一个额外的功能,好的旧 gcc 会让你这样做。

        ctest.c:6:5: warning: array index 42 is past the end of the array (which contains 1 element) [-Warray-bounds]
            a[42] = 42;
            ^ ~~
        cpp.cpp:4:5: note: array 'a' declared here
            int a[1];
            ^
        1 warning generated.
        

        a[11] = 11;
        

        会触发分段错误(但这里只有一个字节,它很可能只是覆盖另一个变量的值),如果您想要堆栈溢出,请尝试执行无限递归。

        此外,如果您想证明您的代码段错误(仅适用于 malloc),我建议您使用电子围栏编译它以进行测试。它将阻止您的程序超出其分配的内存(从第一个字节开始)

        http://linux.die.net/man/3/efence

        正如 cmets 中所建议的,Valgrind 也是一个有用的工具。

        http://valgrind.org/

        【讨论】:

        • 它不会触发段错误(参见 OP)。无论如何,为什么会这样?
        • 这是因为它只比它的大小高一个字节,你可能只是覆盖了另一个变量的值。因此 -lefence 的用处。
        • 只是祝你好运。试试 int main() { int a[1];整数 b = 12999; a[b] = 42; }
        • 当然,如果你经常越界,你会碰到一个不是堆栈保护页面的页面
        • 你有什么有趣的文章吗?因为我的印象是只有堆是由操作系统管理的
        【解决方案5】:

        为什么堆栈上溢/下溢不触发运行时错误?

        C 不限于“堆”和“栈”实现。示例:main() 中的变量不必在“堆栈”中。甚至 GCC 也可能以难以理解的方式进行优化。 许多内存架构是可能的。由于 C 没有指定底层内存架构,因此以下只是未定义的行为。 @Karoly Horvath

        // Undefined behavior: accessing memory outside array's range.
        int a[10];
        a[-1] = -1;
        a[11] = 11;
        

        任何分析都可能在一周中的某一天对给定的内存模型有意义,但这种行为只是众多可能性之一。

        【讨论】:

          【解决方案6】:

          分配堆存储总是包括内存不足的测试;对于堆栈空间,由于堆栈空间一次又一次地重复使用的方式,这不太重要。如果它们共享同一个存储块,那么它们可能会发生冲突。

          GCC 不会这样做,因为堆空间和堆栈空间是分开的;我不知道 Valgrind。

          在至少一种旧语言(Turbo C)中,如果在堆顶和栈底之间剩余的存储空间少于 256 字节,则 alloc() 将失败。假设 256 字节足以容纳堆栈增长。如果不是,您会收到一些非常奇怪的运行时错误。

          Turbo C 有一个编译时选项 -N,可以更彻底地检查堆栈溢出。其他语言可能有类似的选项。

          【讨论】:

            猜你喜欢
            • 2013-09-11
            • 1970-01-01
            • 2021-04-26
            • 2010-12-22
            • 1970-01-01
            • 1970-01-01
            • 2019-01-09
            • 2011-09-10
            相关资源
            最近更新 更多