堆栈上的局部变量分配顺序

时间:2009-07-09 05:55:19

标签: c memory gcc x86 stack

看看这两个功能:

void function1() {
    int x;
    int y;
    int z;
    int *ret;
}

void function2() {
    char buffer1[4];
    char buffer2[4];
    char buffer3[4];
    int *ret;
}

如果我在function1()的{​​{1}}处中断,并打印变量的地址,我就明白了:

gdb

如果我在(gdb) p &x $1 = (int *) 0xbffff380 (gdb) p &y $2 = (int *) 0xbffff384 (gdb) p &z $3 = (int *) 0xbffff388 (gdb) p &ret $4 = (int **) 0xbffff38c 做同样的事,我明白了:

function2()

您会注意到,在这两个函数中,(gdb) p &buffer1 $1 = (char (*)[4]) 0xbffff388 (gdb) p &buffer2 $2 = (char (*)[4]) 0xbffff384 (gdb) p &buffer3 $3 = (char (*)[4]) 0xbffff380 (gdb) p &ret $4 = (int **) 0xbffff38c 都存储在最靠近堆栈顶部的位置。在ret中,后跟function1()z,最后是y。在xfunction2()之后是ret,然后是buffer1buffer2。为什么存储顺序发生了变化?我们在两种情况下使用相同数量的内存(4字节buffer3 s vs 4字节int数组),因此它不能成为填充问题。这种重新排序有什么理由,而且,通过查看C代码可以提前确定如何排序局部变量?

现在我知道C的ANSI规范没有说明存储局部变量的顺序,并且允许编译器选择自己的顺序,但我想编译器有关于它如何的规则处理这个,并解释为什么这些规则是这样的。

作为参考,我在Mac OS 10.5.7上使用GCC 4.0.1

10 个答案:

答案 0 :(得分:16)

我不知道why GCC organizes its stack the way it does(虽然我猜你可以破解它的来源或this paper然后找出),但我可以告诉你如何保证特定堆栈变量的顺序你需要的一些原因。只需将它们放在结构中:

void function1() {
    struct {
        int x;
        int y;
        int z;
        int *ret;
    } locals;
}

如果我的记忆正确地为我服务,spec保证&ret > &z > &y > &x。我离开了我的K& R工作,所以我不能引用章节和诗句。

答案 1 :(得分:9)

所以,我做了一些实验,这就是我发现的。它似乎是基于每个变量是否是一个数组。鉴于此输入:

void f5() {
        int w;
        int x[1];
        int *ret;
        int y;
        int z[1];
}

我最终在gdb中得到了这个:

(gdb) p &w
$1 = (int *) 0xbffff4c4
(gdb) p &x
$2 = (int (*)[1]) 0xbffff4c0
(gdb) p &ret 
$3 = (int **) 0xbffff4c8
(gdb) p &y
$4 = (int *) 0xbffff4cc
(gdb) p &z
$5 = (int (*)[1]) 0xbffff4bc

在这种情况下,首先处理int s和指针,最后在堆栈顶部声明并首先声明更接近底部。然后以相反的方向处理数组,声明越早,堆栈中的最高位。我确信这是有充分理由的。我不知道它是什么。

答案 2 :(得分:8)

ISO C不仅没有说明堆栈上局部变量的排序,甚至不能保证堆栈甚至存在。该标准只讨论了块内变量的范围和生命周期。

答案 3 :(得分:5)

通常它与对齐问题有关。

大多数处理器在获取非处理器字对齐的数据时速度较慢。他们必须将它们分块并将它拼接在一起。

可能发生的事情是将所有大于或等于处理器最佳对齐的对象放在一起,然后将可能不对齐的东西包装得更紧密。事实上,在你的例子中,所有char数组都是4个字节,但我敢打赌,如果你将它们设为3个字节,它们仍然会在同一个地方结束。

但是如果你有四个单字节数组,它们可能会在一个4字节范围内结束,或者在四个单独的数组中对齐。

所有关于处理器抓取的最简单(转换为“最快”)的全部内容。

答案 4 :(得分:2)

C标准没有规定其他自动变量的任何布局。然而,它特别指出,为避免疑问,

  

[...]未指定[function]参​​数的存储布局。 (C11 6.9.1p9)

从中可以理解,对于任何其他对象的存储布局同样未指定,除了标准给出的少数要求,包括空指针不能指向任何有效的对象或函数,以及聚合对象中的布局。

C标准不包含对单词“stack”的提及;很有可能做一个无堆栈的C实现,从堆中分配每个激活记录(尽管这些可能被理解为形成堆栈)。

给编译器一些余地的原因之一是效率。但是,当前的编译器也会使用它来实现安全性,使用诸如地址空间布局随机化和stack canaries之类的技巧来尝试利用未定义的行为更加困难。对缓冲区进行重新排序是为了使金丝雀的使用更有效。

答案 5 :(得分:0)

我的猜测是,这与数据如何加载到寄存器有关。也许,对于char数组,编译器可以有效地并行执行某些操作,这与内存中的位置有关,可以轻松地将数据加载到寄存器中。尝试使用不同级别的优化进行编译,然后尝试使用int buffer1[1]

答案 6 :(得分:0)

这也可能是一个安全问题?

int main()
{
    int array[10];
    int i;
    for (i = 0; i <= 10; ++i)
    {
        array[i] = 0;
    }
}

如果数组在堆栈上比i低,则此代码将无限循环(因为它错误地访问并将数组[10]归零,即i)。通过在堆栈上放置更高的数组,尝试访问超出堆栈末尾的内存将更有可能触及未分配的内存并崩溃,而不是导致未定义的行为。

我用gcc一次尝试了这个相同的代码,并且除了使用我现在不记得的标志的特定组合之外无法使其失败。无论如何,它将数组放置在距离i几个字节的位置。

答案 7 :(得分:0)

有趣的是,如果你在function1中添加一个额外的int * ret2,那么在我的系统上,顺序是正确的,而它只是3个局部变量的乱序。我的猜测是,由于反映了将要使用的寄存器分配策略,因此按此顺序排序。无论是那个还是随意的。

答案 8 :(得分:0)

这完全取决于编译器。除此之外,某些过程变量可能永远不会放在堆栈上,因为它们可以将整个生命都放在一个寄存器中。

答案 9 :(得分:0)

我认为这是一个安全问题,或者至少是为保护堆栈所采取的措施产生的副作用。我正在研究https://ctf101.org/binary-exploitation/buffer-overflow/中的示例,该示例具有以下代码:

#include <stdio.h>

int main() {
    int secret = 0xdeadbeef;
    char name[100] = {0};
    read(0, name, 0x100);
    if (secret == 0x1337) {
        puts("Wow! Here's a secret.");
    } else {
        puts("I guess you're not cool enough to see my secret");
    }
}

当我使用默认值进行编译时,即使使用-O0,也将secret放在name开头之前4个字节处,这使得无法轻松利用漏洞。但是,当我添加-fno-stack-protector时,它在secret开始之后将name移到了108个字节,并且可以通过放置所需的序列来修改secret的值。输入的偏移量108处的字节。