为什么同一个程序的不同运行中堆栈大小会不同?

4

考虑以下程序。它从命令行获取一个参数,然后展开递归函数直到达到限制。

#include <stdio.h>
#include <stdlib.h>

int rec(int x, int limit) {
  if (x == limit) {
    return limit;
  }
  int r = rec(x + 1, limit);
  return r - 1;
}

int main(int arc, char* argv[]) {
  int result, limit;
  limit = atoi(argv[1]);
  printf("stack: %p\n", &result);
  result = rec(0, limit);
  printf("%d\n", result);
}

如果我将其编译,我期望它在固定的输入参数限制下耗尽栈。但是发生了其他事情。

dejan@raven:~/test/stack$ gcc stack.c
dejan@raven:~/test/stack$ ./a.out 174580
stack: 0x7fff42fd58f0
Segmentation fault (core dumped)
dejan@raven:~/test/stack$ ./a.out 174580
stack: 0x7ffdd2dd8b20
0

在两次不同的运行中,堆栈大小似乎是不同的。这并不像是编译器问题,因为使用clang也会出现相同的情况,并且反汇编也没有涉及任何奇怪的内容。
为什么在不同的运行中堆栈大小会不同呢?

4
地址空间布局随机化? - Barmar
@Barmar 我不关心堆栈的位置,只关心尺寸。随机化如何影响堆栈的大小? - Dejan Jovanović
我来这里说和 @Barmar 一样的话;然而,我甚至不确定你程序中第一个变量声明的地址是否与堆栈位置有太大关系;你的编译器可能甚至不会为该变量分配自己的空间,而是直接将结果传递给 printf 中的 %EAX - Marcus Müller
3
他的程序中没有任何非确定性的因素,它应该表现出一致的行为。它不依赖于系统的任何其他状态。 - Barmar
1
@MarcusMüller 是的,但它应该每次传递相同的 %EAX - Barmar
显示剩余3条评论
2个回答

2

我已经在你的程序中添加了/proc/self/maps解析器(与@AndrewHenle建议的方法相同,但我是在程序启动时执行的,并且不调用pmap):

char* get_stack_bounds() {
    FILE* maps = fopen("/proc/self/maps", "r");
    static char line[256];

    while(!feof(maps)) {
        fgets(line, 255, maps);
        if(strstr(line, "[stack]")) {
            char* space = strchr(line, ' ');
            *space = '\0';
            fclose(maps);
            return line;
        }
    }

    fclose(maps);
    return NULL;
}

unsigned long get_stack_right() {
    char* bounds = get_stack_bounds();
    bounds = strchr(bounds, '-') + 1;
    return strtol(bounds, NULL, 16);
}

main() 开始时候输出一些信息:

printf("&result: %p delta: %ld\n", &result, 
     get_stack_right() - ((unsigned long) &result));

这里是一些结果:
> ./a.out 104747
&result: 0x7fff3347c7f8 delta: 6152
0
> ./a.out 174580
&result: 0x7fffe43c9b38 delta: 5320
0
> ./a.out 174580
&result: 0x7fff26ad2b28 delta: 9432
Segmentation fault (core dumped)
> ./a.out 174580
&result: 0x7fff145aa5a8 delta: 6744
0
> ./a.out 174580
&result: 0x7fff74fff0b8 delta: 12104
Segmentation fault (core dumped)

我认为delta(即result地址和栈基地址之间的差异)与段错误之间的相关性是显而易见的。
你应该注意,main()不是程序中运行的第一个函数,实际的入口点将是来自crt1.o(或其他文件)的_start(),因此初始堆栈大小可能会有所不同。
实际问题是地址空间布局随机化。这里是来自fs/binfmt_elf_fdpic.c的注释,关于它的用法:
/* In some cases (e.g. Hyper-Threading), we want to avoid L1 evictions
 * by the processes running on the same package. One thing we can do is
 * to shuffle the initial stack for them, so we give the architecture
 * an opportunity to do so here.
 */
sp = arch_align_stack(bprm->p);

这里是x86上arch_align_stack()的实现:
unsigned long arch_align_stack(unsigned long sp)
{
    if (!(current->personality & ADDR_NO_RANDOMIZE) && randomize_va_space)
        sp -= get_random_int() % 8192;
    return sp & ~0xf;
}

这很有用!你有什么想法,非确定性的增量可能来自哪里?这是一个错误还是一个特性? - Dejan Jovanović

0
添加一个SIGSEGV处理程序:
void handler( int sig )
{
    char buffer[ 1024 ]
    sprintf( buffer, "/path/to/pmap %d", getpid() );
    system( buffer );
    exit( 0 );
}

int main( int argc, char *argv[] )
{
    signal( SIGSEGV, handler );
       .
       .
       .

这样,你的进程在发生SEGV时,不会生成核心文件,而是会发出其地址空间的映射。

请注意,一般来说,这是一种非常危险的做法。它并不是真正的异步信号安全的。但是,你并没有做任何死锁会造成实际损害的事情。


我并不试图找出处理段错误的方法,问题是关于堆栈大小的。 - Dejan Jovanović
@DejanJovanović ... 这个 pmap 调用将会精确地打印出来。 - glglgl
从 pmap 显示输出将向您展示该进程的整个地址空间。地址空间映射的信息将显示诸如可能的地址空间随机化等内容,这可以为您提供答案。 - Andrew Henle

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