繁体   English   中英

为什么当我写到数组末尾时我的程序不会崩溃?

[英]Why doesn't my program crash when I write past the end of an array?

为什么下面的代码可以在没有任何崩溃@runtime 的情况下工作?

而且大小完全取决于机器/平台/编译器!!。 我什至可以在 64 位机器中最多放弃 200。 如何在操作系统中检测到主函数中的分段错误?

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

这个缓冲空间从何而来? 这是分配给进程的堆栈吗?

我前段时间为了教育目的而写的东西......

考虑以下 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

验尸

事实证明,段错误不是发生在 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 的元素。

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

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

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

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

您正在写入尚未分配的内存,但恰好在那里,并且 - 可能 - 没有被用于其他任何事情。 如果您对代码中看似不相关的部分、操作系统、编译器、优化标志等进行更改,您的代码可能会有不同的行为。

换句话说,一旦你进入那个领域,所有的赌注都没有了。

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

  1. 调用函数时堆栈上的数据量,其中包含溢出的变量访问
  2. 写入溢出变量/数组的数据总量

请记住,堆栈向下增长。 即进程执行从一个堆栈指针开始,该堆栈指针接近要用作堆栈的内存末尾 不过,它不是从最后一个映射字开始,这是因为系统的初始化代码可能决定在创建时将某种“启动信息”传递给进程,并且通常在堆栈上这样做。

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

如果写入堆栈缓冲区的数据总量大于之前使用的堆栈空间总量(由调用方/初始化代码/其他变量),那么无论内存访问首先超出顶部(开始)堆栈。 崩溃地址将刚刚超过页面边界 - 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[] )以及在各个地方崩溃,甚至可能是无限循环(例如,如果编译器将iargc放入堆栈并且代码覆盖它在循环期间)。

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

如果你试试这个

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

    arr.at(4) = 99;
} 

得到一个异常抛出。

所以 C++ 提供了一个检查的和一个未检查的接口。 您可以选择要使用的那个。

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

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

如果您希望在访问内存时进行手动操作,则需要一个像 Java 或 CLI 这样的托管环境,在该环境中,您的整个程序由另一个负责查找这些违规行为的管理程序运行。

显然,当您要求计算机在内存中分配一定数量的字节时,例如:char array[10] 它为我们提供了一些额外的字节,以免遇到段错误,但是使用这些字节仍然不安全,并试图达到更多的内存最终会导致程序崩溃。

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

也就是说,如果不是大多数编译器,您的代码甚至无法编译.

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

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

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

干杯 & hth.,

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM