为什么当我写入数组的末尾时,程序不会崩溃?
为什么下面的代码在运行时没有任何崩溃?
而且大小完全取决于机器/平台/编译器!! 我甚至可以在一台64位机器上使用200个。 如何在操作系统中检测到主函数中的段错误?
void 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 sc:8 8 q[i]=i; (gdb) pi $1 = 1008 (gdb)
呵呵,程序没有在分配200个项目时写段错误,而是当我= 1008时崩溃了,为什么?
input页面。
人们可以在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
验尸
事实certificate,段错误发生不在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) pi $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]),而不触发seg故障。 当访问q [1008]时,您将进入一个权限不同的新页面。
由于你正在写数组的边界之外,你的代码的行为是未定义的。
未定义行为的本质是任何事情都可能发生 ,包括缺乏段错误(编译器没有义务执行边界检查)。
你正在写的内存,你没有分配,但恰好在那里,可能 – 不被用于其他任何事情。 如果您对代码中看似不相关的部分,操作系统,编译器,优化标志等进行了更改,则代码的行为可能会有所不同。
换句话说,一旦你在这个领域,所有的赌注都是closures的。
通过使用Cinheritance自C的数组types,您隐式地要求不要进行范围检查。
如果你尝试这个,而不是
void main(int argc, char* argv[]) { std::vector<int> arr(3); arr.at(4) = 99; }
你会得到一个exception抛出。
所以C ++提供了一个checked和unchecked接口。 您可以select您想使用的那个。
关于何时/何地局部variables缓冲区溢出崩溃取决于几个因素:
- 在调用函数时已经在堆栈上的数据量包含溢出variables访问
- 写入溢出variables/数组的数据总量
请记住,堆栈向下增长。 也就是说,进程的执行是从一个堆栈指针开始,靠近待用作堆栈的内存的末尾 。 它不会从最后一个映射的字开始,这是因为系统的初始化代码可能决定在创build时将某种“启动信息”传递给进程,而且经常在堆栈中这样做。
这是通常的失败模式 – 从包含溢出代码的函数返回时崩溃。
如果写入堆栈缓冲区的数据总量大于以前使用的堆栈空间总量(调用者/初始化代码/其他variables),那么无论内存访问超出顶层开始)的堆栈。 崩溃的地址将刚刚超过页面边界 – SIGSEGV
由于访问超出堆栈顶部的内存,没有任何映射。
如果这个总数小于此时使用的堆栈部分的大小,那么它就会正常工作,之后就会崩溃 – 实际上,在堆栈上存储返回地址的平台上(这对于x86 / x64是正确的),从你的函数返回时。 这是因为CPU指令ret
实际上从堆栈中获取一个字(返回地址)并在那里redirect执行。 如果代替预期的代码位置,这个地址包含任何垃圾,发生exception并且程序死亡。
为了说明这一点:调用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()
启动时,它会为了各种目的在堆栈上分配空间,其中包括托pipe你要溢出的数组。 这将使它看起来像:
[ 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
放入堆栈并且代码覆盖它在循环中)。
这是不确定的行为 – 你根本没有观察到任何问题。 最可能的原因是您覆盖了程序行为之前不依赖的内存区域 – 内存在技术上是可写的(在大多数情况下,堆栈大小约为1兆字节),您看不到错误指示。 你不应该依赖这个。
为了回答你的问题,为什么它是“未被发现的”:大多数C编译器在编译时不会分析你正在用指针和内存做什么,所以没有人会在编译时注意到你写了一些危险的东西。 在运行时,也没有受控的托pipe环境来保存您的内存引用,所以没有人会阻止您阅读您无权访问的内存。 内存碰巧在这个时候分配给你(因为它只是你的函数不远处的堆栈的一部分),所以操作系统也没有问题。
如果你想在访问内存的时候手持,你需要一个像Java或者CLI这样的托pipe环境,在这个托pipe环境中,你的整个程序由另外一个运行,pipe理程序寻找那些违规行为。
您的代码有未定义的行为。 这意味着它可以做任何事或什么都不做。 根据你的编译器和操作系统等,它可能会崩溃。
这就是说,即使不是大多数编译器,你的代码甚至不会编译 。
那是因为你有void main
,而C标准和C ++标准都需要int main
。
关于唯一对void main
感到满意的编译器是Microsoft的Visual C ++。
这是一个编译器的缺陷 ,但由于微软有很多的例子文档,甚至生成void main
代码生成工具,他们可能永远不会修复它。 但是,考虑到编写微软特定的void main
比标准int main
更types一个字符。 那么为什么不去标准呢?
干杯&hth。,
当一个进程试图覆盖不属于自己的内存中的页面时,会发生分段错误; 除非你在缓冲区的末端运行很长时间,否则你不会触发seg错误。
堆栈位于您的应用程序拥有的内存块中的某个位置。 在这种情况下,如果你没有重写一些重要的东西,你就很幸运。 你可能覆盖了一些未被使用的内存。 如果你有点不幸,你可能会覆盖堆栈上另一个函数的堆栈。