【问题标题】:Why does a stack overflow occur at varying stack usage each run instead of a fixed amount?为什么每次运行时会发生堆栈溢出而不是固定数量?
【发布时间】:2017-08-17 00:44:42
【问题描述】:

我正在 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

运行 2:

738438,738437,[1]    7125 segmentation fault

【问题讨论】:

  • 只有在堆栈页面错误时才会发生溢出。那是当堆栈指针运行到未加载/无主页面时。堆栈开始的位置不必在确切的页面边界上,但可以取决于程序加载的位置,因此溢出条件(页面错误)触发器会有所不同。
  • this 看起来相似吗?
  • @RichardCritten 那么分配的堆栈大小之外的任何页面都必须是无主页面,对吗?如果我错了,请纠正我。
  • 这取决于堆栈的增长方式。它可能会增长到您的程序静态数据或堆中。这将取决于架构和实现细节。例如,实现者可以在堆栈帧的末尾放置一个保护页。我们非常接近 Undefined Behavior is Undefined Behavior,任何解释都需要目标硬件和实现细节。
  • 对于你的 Rust 实现,你不需要声明一个 static 可变的。局部变量也可以正常工作,同时也无需使用 unsafe 代码。

标签: c linux rust out-of-memory stack-overflow


【解决方案1】:

这很可能是由于ASLR

堆栈的基地址在每次运行时都是随机的,以使某些类型的漏洞利用更加困难;在 Linux 上这个 has a granularity of 16 bytes(这是 x86 和我所知道的几乎所有其他平台上最大的对齐要求)。

另一方面,the page size is (normally) 4 KB on x86,当你触摸第一个禁止页面时,系统会检测到堆栈溢出;这意味着您将始终首先获得部分页面(偏移量取决于 ASLR),然后在系统检测到堆栈溢出之前获得两个完整页面。因此,总可用堆栈大小至少是您请求的 8192 字节,加上第一个部分页面,其可用大小在每次运行时都不同。1


  1. 所有这些都在偏移量非零的“常规”情况下;如果你很幸运并且随机偏移量为零,那么你可能正好得到两页。

【讨论】:

  • 它只需要针对最严格的数据类型进行对齐。
  • 正如我所说,16 字节对齐就足够了(因此最挑剔的 SSE 类型可以正确对齐);页面边界对于快速堆栈访问并不是特别有趣 - 堆栈实际上是在执行期间在每个偏移量(对目标类型敏感)处访问的,只要它是对齐的,它开始并不重要。
  • 通过在/proc/sys/kernel/randomize_va_space中禁用ASLR进行验证,现在结果一致。再次感谢。
  • @nohup 以防万一您在测试后没有重新启用 ASLR,您绝对应该这样做。 ASLR 是一项关键的安全保护,不应禁用。
  • 谢谢@Shepmaster。我已经启用了它。我正在学习一点系统编程,只是好奇。
猜你喜欢
  • 1970-01-01
  • 2014-09-19
  • 2015-08-13
  • 2018-07-01
  • 2011-10-22
  • 1970-01-01
  • 1970-01-01
  • 2010-11-09
  • 2012-12-19
相关资源
最近更新 更多