当我写一个数组的末尾时,为什么我的程序没有崩溃?

时间:2011-06-23 10:59:40

标签: c stack callstack

为什么下面的代码没有任何崩溃@ runtime?

而且尺寸完全取决于机器/平台/编译器!!我甚至可以在64位机器上放弃200。如何在OS中检测到主函数中的分段错误?

int main(int argc, char* argv[])
{
    int arr[3];
    arr[4] = 99;
}

这个缓冲空间来自哪里?这是分配给进程的堆栈吗?

9 个答案:

答案 0 :(得分:73)

我之前写的一些用于教育目的的东西......

考虑以下c程序:

int q[200];

main(void) {
    int i;
    for(i=0;i<2000;i++) {
        q[i]=i;
    }
}

编译并执行后,会生成核心转储:

$ gcc -ggdb3 segfault.c
$ ulimit -c unlimited
$ ./a.out
Segmentation fault (core dumped)

现在使用gdb执行事后分析:

$ gdb -q ./a.out core
Program terminated with signal 11, Segmentation fault.
[New process 7221]
#0  0x080483b4 in main () at s.c:8
8       q[i]=i;
(gdb) p i
$1 = 1008
(gdb)
嗯,当一个人在分配的200个项目之外写作时,程序没有发生段错误,而是当i = 1008时它崩溃了,为什么?

输入网页。

可以在UNIX / Linux上以多种方式确定页面大小,一种方法是使用系统函数sysconf(),如下所示:

#include <stdio.h>
#include <unistd.h> // sysconf(3)

int main(void) {
    printf("The page size for this system is %ld bytes.\n",
            sysconf(_SC_PAGESIZE));

    return 0;
}

给出输出:

  

此系统的页面大小为4096字节。

或者可以像这样使用命令行实用程序getconf:

$ getconf PAGESIZE
4096

验尸后

事实证明,segfault不是出现在i = 200但是在i = 1008时,让我们找出原因。启动gdb进行一些事后分析:

$gdb -q ./a.out core

Core was generated by `./a.out'.
Program terminated with signal 11, Segmentation fault.
[New process 4605]
#0  0x080483b4 in main () at seg.c:6
6           q[i]=i;
(gdb) p i
$1 = 1008
(gdb) p &q
$2 = (int (*)[200]) 0x804a040
(gdb) p &q[199]
$3 = (int *) 0x804a35c

q在地址0x804a35c处结束,或者更确切地说,q [199]的最后一个字节位于该位置。页面大小与我们之前看到的4096字节一样,机器的32位字大小使虚拟地址分解为20位页码和12位偏移量。

q []以虚拟页码结尾:

0x804a = 32842 偏移量:

0x35c = 860 所以还有:

4096 - 864 = 3232 在分配了q []的内存页面上留下的字节数。这个空间可以容纳:

3232/4 = 808 整数,代码对它进行处理,好像它包含位于200到1008位置的q元素。

我们都知道那些元素不存在且编译器没有抱怨,因为我们对该页面具有写权限,所以hw也没有。只有当i = 1008时,q []引用了我们没有写入权限的不同页面上的地址,虚拟内存hw检测到这一点并触发了段错误。

整数存储在4个字节中,这意味着该页面包含808(3236/4)个额外的伪元素,这意味着从q [200],q [201]一直访问这些元素仍然是完全合法的在没有触发seg故障的情况下,元素199 + 808 = 1007(q [1007])。访问q [1008]时,您输入的权限不同的新页面。

答案 1 :(得分:6)

由于您是在数组边界之外编写的,因此代码的行为未定义。

任何事情都可能发生,这是未定义行为的本质,​​包括缺少段错误(编译器没有义务执行边界检查)。

你写的是你没有分配的内存,但恰好在那里,而且 - 可能 - 没有被用于其他任何东西。如果您对代码中看似无关的部分,操作系统,编译器,优化标记等进行更改,您的代码可能会有不同的行为。

换句话说,一旦你进入那个领域,所有的赌注都会被取消。

答案 2 :(得分:4)

关于局部变量缓冲区溢出崩溃的确切时间/位置取决于几个因素:

  1. 调用函数时堆栈上的数据量,包含溢出变量访问
  2. 总计写入溢出变量/数组的数据量
  3. 请记住,堆栈向下向下。即进程执行从靠近要用作堆栈的内存的 end 的堆栈指针开始。它不会从最后一个映射的单词开始,这是因为系统的初始化代码可能决定在创建时将某种“启动信息”传递给进程,并且通常在堆栈上执行此操作。

    这是通常的失败模式 - 从包含溢出代码的函数返回时崩溃。

    如果写入堆栈缓冲区的数据量大于先前使用的堆栈空间总量(通过调用者/初始化代码/其他变量),那么你将崩溃在任何内存访问首先超出堆栈的顶部(开始)。崩溃地址将超过页面边界 - SIGSEGV,因为访问堆栈顶部之外的内存,没有映射任何内容。

    如果此时总数小于堆栈使用部分的大小,那么它将正常运行并且稍后崩溃 - 事实上,在存储返回地址的平台上从函数返回时,堆栈(对于x86 / x64是正确的)。那是因为CPU指令ret实际上从堆栈中取一个字(返回地址)并重定向执行。如果该地址不包含预期的代码位置,而是包含任何垃圾,则会发生异常并导致程序死亡。

    为了说明这一点:当调用main()时,堆栈看起来像这样(在32位x86 UNIX程序上):

    [ esp          ] <return addr to caller> (which exits/terminates process)
    [ esp + 4      ] argc
    [ esp + 8      ] argv
    [ esp + 12     ] envp <third arg to main() on UNIX - environment variables>
    [ ...          ]
    [ ...          ] <other things - like actual strings in argv[], envp[]
    [ END          ] PAGE_SIZE-aligned stack top - unmapped beyond
    

    main()启动时,它会在堆栈上分配空间用于各种目的,其中包括托管要溢出的数组。这将使它看起来像:

    [ esp          ] <current bottom end of stack>
    [ ...          ] <possibly local vars of main()>
    [ esp + X      ] arr[0]
    [ esp + X + 4  ] arr[1]
    [ esp + X + 8  ] arr[2]
    [ esp + X + 12 ] <possibly other local vars of main()>
    [ ...          ] <possibly other things (saved regs)>
    
    [ old esp      ] <return addr to caller> (which exits/terminates process)
    [ old esp + 4  ] argc
    [ old esp + 8  ] argv
    [ old esp + 12 ] envp <third arg to main() on UNIX - environment variables>
    [ ...          ]
    [ ...          ] <other things - like actual strings in argv[], envp[]
    [ END          ] PAGE_SIZE-aligned stack top - unmapped beyond
    

    这意味着您可以愉快地访问arr[2]以外的方式。

    对于缓冲区溢出导致的不同崩溃的品尝者,请尝试以下方法:

    #include <stdlib.h>
    #include <stdio.h>
    
    int main(int argc, char **argv)
    {
        int i, arr[3];
    
        for (i = 0; i < atoi(argv[1]); i++)
            arr[i] = i;
    
        do {
            printf("argv[%d] = %s\n", argc, argv[argc]);
        } while (--argc);
    
        return 0;
    }
    

    并且看一下当你将缓冲区溢出一点(比如10)位时,崩溃将会是什么不同,而当它溢出超出堆栈末尾时。尝试使用不同的优化级别和不同的编译器。相当具有说明性,因为它显示了错误行为(不会总是正确地打印所有argv[])以及在各个地方崩溃,甚至可能是无限循环(例如,如果编译器放置i或{{ 1}}进入堆栈,代码在循环期间覆盖它。

答案 3 :(得分:3)

通过使用C ++从C继承的数组类型,您隐含地要求不进行范围检查。

如果你试试这个

void main(int argc, char* argv[])
{     
    std::vector<int> arr(3);

    arr.at(4) = 99;
} 

会抛出异常。

因此,C ++提供了已检查和未经检查的界面。您可以选择要使用的那个。

答案 4 :(得分:2)

这是未定义的行为 - 您根本没有发现任何问题。最可能的原因是你覆盖了程序行为不依赖于早期的内存区域 - 内存在技术上是可写的(在大多数情况下,堆栈大小大约是1兆字节)并且你没有看到任何错误指示。你不应该依赖这个。

答案 5 :(得分:1)

回答你的问题为什么它“未被发现”:大多数C编译器在编译时不分析你用指针和内存做什么,所以没有人在编译时注意到你写了一些危险的东西。在运行时,也没有可以管理内存引用的受控托管环境,因此没有人会阻止您阅读您无权阅读的内存。内存恰好在那时分配给你(因为它只是离你的函数不远的堆栈的一部分),所以操作系统也没有问题。

如果您在访问内存时需要手持,则需要一个托管环境,如Java或CLI,其中整个程序由另一个管理程序运行,管理程序以查找这些违规行为。

答案 6 :(得分:0)

您的代码具有未定义的行为。这意味着它可以做任何事情。根据您的编译器和操作系统等,它可能会崩溃。

也就是说,如果不是大多数编译器,你的代码甚至不会编译

那是因为你有void main,而C标准和C ++标准都需要int main

关于唯一对void main感到满意的编译器是Microsoft的Visual C ++。

这是一个编译器缺陷,但由于Microsoft有很多示例文档甚至是生成void main的代码生成工具,因此他们可能永远无法修复它。但是,请考虑编写特定于Microsoft的void main比标准int main更多地输入一个字符。那么为什么不遵循这些标准?

干杯&amp;第h。,

答案 7 :(得分:0)

当进程试图覆盖它不拥有的内存中的页面时,会发生分段错误;除非你在缓冲区结束时跑了很长时间,否则你不会触发seg错误。

堆栈位于应用程序拥有的其中一个内存块中的某个位置。在这种情况下,如果你没有覆盖重要的东西,那么你很幸运。你已经覆盖了一些未使用的内存。如果你有点不走运,你可能会覆盖堆栈中另一个函数的堆栈帧。

答案 8 :(得分:0)

因此,显然当您向计算机询问要在内存中分配的一定数量的字节时,请说:     字符数组[10] 它给我们提供了一些额外的字节,以免碰到段错误,但是使用它们仍然不安全,尝试到达更多的内存最终将导致程序崩溃。