为什么stackoverflow错误是混乱的?

时间:2015-07-02 09:21:16

标签: c recursion operating-system stack-overflow

这个简单的C程序很少在相同的调用深度终止:

#include <stdio.h>
#include <stdlib.h>

void recursive(unsigned int rec);

int main(void)
{
  recursive(1);
  return 0;
}

void recursive(unsigned int rec) {
    printf("%u\n", rec);
    recursive(rec + 1);
}

这种混乱行为背后的原因是什么?

我正在使用fedora(16GiB ram,堆栈大小为8192),并使用cc编译而没有任何选项。

修改

  • 我知道这个程序会抛出一个stackoverflow
  • 我知道启用某些编译器优化会改变行为并且程序将达到整数溢出。
  • 我知道这是未定义的行为,这个问题的目的是理解/获得可能解释我们在那里观察到的实现特定内部行为的概述。

问题更多,因为在Linux上,线程堆栈大小是固定的,由ulimit -s给出,会影响可用的堆栈大小,以便堆栈溢出并不总是出现在相同的调用深度?

编辑2 @BlueMoon总是在他的CentOS上看到相同的输出,而在我的Fedora上,堆栈为8M,我看到不同的输出(最后打印的整数261892或261845,或261826,或......)

7 个答案:

答案 0 :(得分:9)

将printf调用更改为:

printf("%u %p\n", rec, &rec);

这会强制gcc将rec放在堆栈上并给你它的地址,这很好地表明堆栈指针正在发生什么。

运行您的程序几次,并注意最后打印的地址是什么。在我的机器上运行几次显示:

261958 0x7fff82d2878c
261778 0x7fffc85f379c
261816 0x7fff4139c78c
261926 0x7fff192bb79c

首先要注意的是,堆栈地址始终以78c79c结尾。这是为什么?我们应该在跨越页面边界时崩溃,页面长度为0x1000字节,每个函数占用0x20字节的堆栈,因此地址应以00X或01X结尾。但仔细观察,我们崩溃了。因此堆栈溢出发生在libc内部,从中我们可以得出结论,调用printf和它调用的其他所有内容至少需要0x78c = 1932(可能加上X * 4096)字节的堆栈才能工作。

第二个问题是为什么需要不同数量的迭代才能到达堆栈的末尾?一个提示是,我们得到的地址在程序的每次运行中都是不同的。

1 0x7fff8c4c13ac
1 0x7fff0a88f33c
1 0x7fff8d02fc2c
1 0x7fffbc74fd9c

堆栈在内存中的位置是随机的。这样做是为了防止整个系列的缓冲区溢出攻击。但由于内存分配,特别是在此级别,只能在多个页面(4096字节)中完成,所有初始堆栈指针都将在0x1000处对齐。这将减少随机堆栈地址中随机位的数量,因此只需在堆栈顶部浪费一个随机数量的字节就可以增加额外的随机性。

操作系统只能在整个页面中计算您使用的内存量,包括堆栈限制。因此,即使堆栈以随机地址开始,堆栈上的最后一个可访问地址也始终是以0xfff结尾的地址。

简短的回答是:为了增加随机存储器布局中的随机性,堆栈顶部的一堆字节是故意浪费的,但堆栈的末尾必须以页边界结束。

答案 1 :(得分:4)

执行之间不会有相同的行为,因为它取决于当前可用的内存。你可用的内存越多,你在这个递归函数中就越远。

答案 2 :(得分:2)

您的程序无限运行,因为递归函数中没有基本条件。堆栈将在每次函数调用时不断增长,并导致堆栈溢出 如果是尾递归优化的情况(使用选项-O2),那么肯定会发生堆栈溢出。它调用未定义的行为。

  

什么会影响可用的堆栈大小,以便堆栈溢出并不总是出现在相同的调用深度?

当发生堆栈溢出时,它会调用未定义的行为。在这种情况下,结果没有什么可说的。

答案 3 :(得分:1)

堆栈段和堆段之间存在间隙。现在因为堆的大小是可变的(在执行期间保持不断变化),因此堆栈在堆栈溢出发生之前增长的程度也是可变的,这就是为什么程序很少在相同的调用深度终止的原因。

enter image description here

答案 4 :(得分:1)

上述代码可能导致两个问题:

  • Stack Overflow。
  • 整数溢出。

Stack Overflow:当调用递归函数时,其所有变量都被推送到call stack,包括其return地址。由于没有基本条件会终止递归且堆栈内存有限,因此堆栈将耗尽,导致 Stack Overflow 异常。调用堆栈可以包括有限数量的地址空间,通常在程序开始时确定。调用堆栈的大小取决于许多因素,包括编程语言机器架构多线程的数量可用内存。当程序试图使用比调用堆栈上可用空间更多的空间时(也就是说,当它试图访问超出调用堆栈边界的内存时,这本质上是一个缓冲区溢出),堆栈被称为溢出,通常导致程序崩溃。

请注意,每次函数退出/返回时,该函数推送到堆栈的所有变量都将被释放(也就是说,它们被删除)。一旦释放了堆栈变量,该内存区域就可用于其他堆栈变量。但对于递归函数,返回地址仍然在堆栈上,直到递归终止。此外,自动局部变量被分配为单个块,并且堆栈指针的前进足够远以考虑其大小的总和。您可能对Recursive Stack in C感兴趣。

整数溢出recursive()的每次递归调用rec递增1整数溢出可以发生。为此,您的机器必须具有巨大的堆栈内存,因为无符号整数的范围是:0到4,294,967,295。请参阅参考here

答案 5 :(得分:1)

在实践中,由于stackoverflow(但由于整数溢出),您的递归调用不一定会导致未定义的行为。优化编译器可以简单地将编译器转换为无限循环&#34;用跳转指令:

void recursive(int rec) {
   loop:
    printf("%i\n", rec);
    rec++;
   goto loop;
}

请注意,这会导致未定义的行为,因为它会溢出rec(signed int overflow是UB)。例如,如果rec是unsigned int,那么代码是有效的,理论上应该永远运行。

答案 6 :(得分:1)

当进程从可执行文件加载程序时,通常会为代码,堆栈,堆,初始化和未初始化数据分配内存区域。

分配的堆栈空间通常不是那么大(可能是10兆字节),因此您可以想象物理RAM耗尽在现代系统上不会成为问题,并且堆栈溢出总是发生在相同的递归深度

但是,出于安全原因,堆栈并不总是在同一个地方。 Address Space Layout Randomisation确保堆栈位置的基础在程序的调用之间变化。这意味着程序可能能够在堆栈顶部出现像程序代码一样无法访问的内容之前执行更多(或更少)的递归。

无论如何,这是我对正在发生的事情的猜测。