Почему происходит переполнение стека при разном использовании стека, а не фиксированная сумма?

Я запускаю программу с рекурсивным вызовом на ОС 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

Ответ 1

Скорее всего, это связано с ASLR.

Базовый адрес стека рандомизирован в каждом прогоне, чтобы сделать некоторые типы эксплойтов более трудными; на Linux это имеет размерность 16 байт (что является самым большим требованием к выравниванию на x86 и почти любой другой платформе, которую я знаю).

С другой стороны, размер страницы (обычно) 4 КБ на x86, и система обнаруживает переполнение стека, когда вы касаетесь первой запрещенной страницы; это означает, что вы всегда будете иметь частичную страницу сначала (со смещением в зависимости от ASLR), а затем две полные страницы, прежде чем система обнаружит переполнение стека. Следовательно, общий размер используемого стека составляет, по крайней мере, 8192 байта, которые вы запросили, плюс первая частичная страница, доступный размер которой различен при каждом прогоне. 1


  • Все это в "обычном" случае, когда смещение отличное от нуля; если вам очень повезло, и случайное смещение равно нулю, вы, вероятно, получите ровно две страницы.