为什么stackoverflow错误混乱?

Ale*_*aux 6 c stack-overflow recursion operating-system

这个简单的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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

编辑

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

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

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

Art*_*Art 9

将printf调用更改为:

printf("%u %p\n", rec, &rec);
Run Code Online (Sandbox Code Playgroud)

这会强制gcc将rec放在堆栈上,并为您提供其地址,这可以很好地指示堆栈指针发生了什么.

运行您的程序几次,并注意最后打印的地址发生了什么.我的机器上的一些运行显示:

261958 0x7fff82d2878c
261778 0x7fffc85f379c
261816 0x7fff4139c78c
261926 0x7fff192bb79c
Run Code Online (Sandbox Code Playgroud)

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

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

1 0x7fff8c4c13ac
1 0x7fff0a88f33c
1 0x7fff8d02fc2c
1 0x7fffbc74fd9c
Run Code Online (Sandbox Code Playgroud)

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

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

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