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

| 为什么下面的代码在没有运行时崩溃的情况下运行? 而且大小完全取决于机器/平台/编译器!我什至在一台64位计算机上最多可以放弃200个。如何在OS中检测到主要功能的分段错误?
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 []引用了我们没有写权限的其他页面上的地址时,虚拟内存hw才检测到此错误并触发了段错误。 整数存储在4个字节中,表示此页面包含808(3236/4)个其他伪元素,表示从q [200],q [201]一直访问到元素199仍然完全合法。 + 808 = 1007(q [1007])而不触发段故障。访问q [1008]时,您将进入一个权限不同的新页面。     
        由于您在数组边界之外进行编写,因此代码的行为处于不确定状态。 未定义行为的本质是任何事情都可能发生,包括缺少段错误(编译器没有义务执行边界检查)。 您正在写入尚未分配的内存,但该内存恰好在那儿,并且可能没有被用于其他任何事情。如果您更改了看似无关的代码部分,操作系统,编译器,优化标志等,则代码的行为可能会有所不同。 换句话说,一旦您进入该领土,所有赌注都将关闭。     
关于何时/何处局部变量缓冲区溢出崩溃崩溃取决于几个因素: 调用函数时堆栈上已包含的数据量,其中包含溢出变量访问 总共写入溢出变量/数组的数据量 请记住,堆栈向下生长。即进程执行从靠近要用作堆栈的内存末端的堆栈指针开始。它不是从最后一个映射的单词开始,那是因为系统的初始化代码可能决定在创建时将某种“启动信息”传递给流程,并且通常这样做在堆栈上。 这是通常的故障模式-从包含溢出代码的函数返回时崩溃。 如果写入堆栈上的缓冲区的数据总量大于以前使用的堆栈空间总量(通过调用者/初始化代码/其他变量),那么无论首先执行的内存访问超出顶部,都会导致崩溃(开始)堆栈。崩溃的地址将刚好超出页面边界-
SIGSEGV
,这是因为访问的内存超出了栈顶,没有映射任何内容。 如果此时的总数小于堆栈已使用部分的大小,那么它将正常工作并稍后崩溃-实际上,在将返回地址存储在堆栈上的平台上(对于x86 /从您的函数返回时)。那是因为CPU指令“ 8”实际上是从堆栈(返回地址)中取出一个字并在那里重定向执行。如果该地址包含任何垃圾,而不是预期的代码位置,则会发生异常,并且程序将死亡。 为了说明这一点:调用ѭ9时,堆栈如下所示(在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
当“ 9”开始时,它将为各种目的在堆栈上分配空间,以托管您要溢出的数组。这将使其看起来像:
[ 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
argc
放入堆栈中,并且代码覆盖了它在循环中)。     
        通过使用C ++继承自C的数组类型,您隐式地要求不进行范围检查。 如果您尝试这样做
void main(int argc, char* argv[])
{     
    std::vector<int> arr(3);

    arr.at(4) = 99;
} 
您会抛出异常。 因此,C ++提供了检查和未检查的接口。由您自己选择要使用的那个。     
        这是不确定的行为-您根本不会发现任何问题。最可能的原因是您覆盖了程序行为不依赖于较早版本的内存区域-该内存在技术上是可写的(在大多数情况下,堆栈大小约为1兆字节),并且看不到错误指示。你不应该依赖这个。     
        为了回答为什么它“未被检测到”的问题:大多数C编译器在编译时不会分析您使用指针和内存所做的事情,因此没有人注意到在编译时您编写了危险的东西。在运行时,也没有可托管的托管环境来托管您的内存引用,因此没有人阻止您读取您无权使用的内存。那时恰好为您分配了内存(因为它只是堆栈的一部分,距离您的功能不远),因此OS也不存在问题。 如果要在访问内存时进行手动操作,则需要一个受管理的环境(如Java或CLI),在该环境中,您的整个程序将由另一个程序运行,该程序将监视那些违规情况。     
您的代码具有未定义的行为。这意味着它什么也做不了。根据您的编译器和OS等,它可能会崩溃。 就是说,即使不是大多数编译器,您的代码也无法编译。 那是因为您有
void main
,而C标准和C ++标准都需要
int main
。 关于唯一对ѭ19满意的编译器是Microsoft的Visual C ++。 这是一个编译器缺陷,但是由于Microsoft有很多示例文档,甚至生成19英镑的代码生成工具,他们可能永远也无法修复它。但是,请考虑键入Microsoft特定的“ 19”比标准的“ 20”多键入一个字符。那么为什么不遵循标准呢? 干杯,……     
        当进程试图覆盖它不拥有的内存中的页面时,就会发生分段错误;除非您在缓冲结束时走了很长一段路,否则不会触发段错误。 堆栈位于应用程序拥有的内存块之一中的某处。在这种情况下,如果您没有覆盖重要的内容,那么您就是幸运的。您可能已经覆盖了一些未使用的内存。如果您不太幸运,则可能已经覆盖了堆栈上另一个函数的堆栈框架。     
        因此,显然,当您向计算机询问要在内存中分配的一定数量的字节时,请说:     字符数组[10] 它给我们提供了一些额外的字节,以免碰到段错误,但是使用这些字节仍然不安全,并且尝试到达更多的内存最终将导致程序崩溃。     

要回复问题请先登录注册