为什么stackoverflow错误混乱?

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

#include  #include  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,或……)

将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结尾的地址。

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

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

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

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

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

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

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

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

上面的代码可能会导致两个问题:

  • 堆栈溢出。
  • 整数溢出。

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

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

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

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

在此处输入图像描述

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

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

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

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