为什么堆栈溢出在每次运行时的堆栈使用量不同而不是固定量?

11

我在Debian操作系统上运行一个带有递归调用的程序。我的堆栈大小为

-s: stack size (kbytes)             8192

据我所知,堆栈大小必须是固定的,并且在每次运行程序时应该分配相同的大小,除非使用ulimit明确更改它。

递归函数将给定数字递减,直到达到0。这是用Rust编写的。

fn print_till_zero(x: &mut i32) {
    *x -= 1;
    println!("Variable is {}", *x);
    while *x != 0 {
        print_till_zero(x);
    }
}

并将该值作为参数传递

static mut Y: i32 = 999999999;
unsafe {
    print_till_zero(&mut Y);
}

由于程序分配的堆栈是固定的,并且理论上不应该改变,我预期每次都会在相同的值处发生堆栈溢出,但事实并非如此,这意味着堆栈分配是可变的。

运行1:

====snip====
Variable is 999895412
Variable is 999895411

thread 'main' has overflowed its stack
fatal runtime error: stack overflow

跑酷2:

====snip====
Variable is 999895352
Variable is 999895351

thread 'main' has overflowed its stack
fatal runtime error: stack overflow

虽然差异微妙,但最理想的情况不是应该在同一变量处引起堆栈溢出吗?为什么它会在不同的时间发生,意味着每次运行的堆栈大小不同?这不是 Rust 特有的;在 C 中也观察到类似的行为:

#pragma GCC push_options
#pragma GCC optimize ("O0")
#include<stdio.h>
void rec(int i){
    printf("%d,",i);
    rec(i-1);
    fflush(stdout);
}
int main(){
setbuf(stdout,NULL);
rec(1000000);
}
#pragma GCC pop_options

输出:

运行 1:

738551,738550,[1]    7052 segmentation fault

第二轮:

738438,738437,[1]    7125 segmentation fault

6
只有当栈页错误时,才会发生溢出。也就是说,当栈指针遇到未加载或未拥有的页面时。栈的起始位置不一定在一个精确的页面边界上,而可能取决于程序的加载位置,因此溢出条件(页错误)触发将会有所不同。 - Richard Critten
1
这个看起来相似吗?链接 - Art
@RichardCritten 所以任何超出分配的堆栈大小的页面都必须是未拥有的页面,对吗?如果我错了请纠正我。 - nohup
1
这将取决于堆栈增长的方式。它可能会增长到程序的静态数据或堆中。这将取决于架构和实现细节。例如,实现者可以在堆栈帧末尾放置一个警戒页。我们非常接近未定义行为,未定义行为需要目标硬件和实现细节的任何说明。 - Richard Critten
2
对于您的Rust实现,您不需要声明一个静态可变量。一个本地变量同样可以工作,避免了需要使用unsafe代码的必要性。 - Shepmaster
1个回答

16

很可能是由于 ASLR 导致的。

堆栈的基地址在每次运行时都会随机化,以使某些类型的攻击更加困难;在 Linux 上,其 对齐粒度为 16 字节(这是 x86 和我所知道的几乎任何其他平台上最大的对齐要求)。

另一方面,在 x86 上,页面大小通常为 4 KB,当您触及第一个禁止页面时,系统便会检测到堆栈溢出;这意味着您总是可以先使用部分页面(其偏移量取决于 ASLR),然后再使用两个完整页面,直到系统检测到堆栈溢出。因此,可用的堆栈大小至少为您请求的 8192 字节,再加上每次运行时可用大小不同的第一个部分页面。1


在“常规”情况下,偏移量不为零;如果你非常幸运,随机偏移量为零,则可能恰好获得两个页面。

1
它只需要对最严格的数据类型进行对齐。 - Richard Critten
1
就像我说的那样,只要它是16字节对齐的(这样最挑剔的SSE类型就可以正确地推送),就足够了;页面边界对于快速堆栈访问并不特别重要-在执行期间,堆栈在每个偏移量(对于目标类型合理)处被虚拟访问,它开始的位置并不重要,只要它对齐即可。 - Matteo Italia
4
通过在/proc/sys/kernel/randomize_va_space中禁用ASLR,已经验证并且结果是一致的。再次感谢。 - nohup
3
如果你测试后没有重新启用ASLR,我建议你一定要重新启用它。ASLR是一个重要的安全保护措施,不应该被禁用。 - Shepmaster
1
谢谢@Shepmaster。我已经重新启用了它。我正在学习一些系统编程,只是出于好奇。 - nohup
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接