为什么下面的代码没有任何崩溃@ runtime?
而且尺寸完全取决于机器/平台/编译器!!我甚至可以在64位机器上放弃200。如何在OS中检测到主函数中的分段错误?
int main(int argc, char* argv[])
{
int arr[3];
arr[4] = 99;
}
这个缓冲空间来自哪里?这是分配给进程的堆栈吗?
答案 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)
关于局部变量缓冲区溢出崩溃的确切时间/位置取决于几个因素:
请记住,堆栈向下向下。即进程执行从靠近要用作堆栈的内存的 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] 它给我们提供了一些额外的字节,以免碰到段错误,但是使用它们仍然不安全,尝试到达更多的内存最终将导致程序崩溃。