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

C 堆栈上局部变量分配的顺序,c,memory,gcc,x86,stack,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; } 如果我在gdb中的function1()处中断,并打印变量的地址,我会得到以下结果: (gdb) p &x $1 = (int *)

看看这两个功能:

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

void function2() {
    char buffer1[4];
    char buffer2[4];
    char buffer3[4];
    int *ret;
}
如果我在
gdb
中的
function1()
处中断,并打印变量的地址,我会得到以下结果:

(gdb) p &x  
$1 = (int *) 0xbffff380
(gdb) p &y
$2 = (int *) 0xbffff384
(gdb) p &z
$3 = (int *) 0xbffff388
(gdb) p &ret
$4 = (int **) 0xbffff38c
(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
如果我在
function2()
中执行相同的操作,我会得到以下结果:

(gdb) p &x  
$1 = (int *) 0xbffff380
(gdb) p &y
$2 = (int *) 0xbffff384
(gdb) p &z
$3 = (int *) 0xbffff388
(gdb) p &ret
$4 = (int **) 0xbffff38c
(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
,最后是
x
。在
function2()
中,
ret
后跟
buffer1
,然后是
buffer2
buffer3
。为什么要更改存储顺序?我们在这两种情况下使用相同的内存量(4字节
int
s和4字节
char
array),所以这不可能是一个填充问题。这种重新排序的原因是什么?此外,通过查看C代码是否可以提前确定局部变量的排序方式

现在我知道ANSI C规范没有说明局部变量的存储顺序,也没有说明编译器可以选择自己的顺序,但我可以想象编译器有规则来处理这些问题,并解释为什么这些规则是这样的


作为参考,我在MacOS10.5.7上使用GCC4.0.1,它通常与对齐问题有关

大多数处理器在获取未与处理器字对齐的数据时速度较慢。他们必须把它撕成碎片并拼接在一起

可能发生的情况是,它将所有大于或等于处理器最佳对齐的对象放在一起,然后将可能不对齐的对象更紧密地打包。碰巧在您的示例中,所有的
char
数组都是4个字节,但我打赌如果将它们设为3个字节,它们仍然会在相同的位置结束

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


这一切都是关于处理器最容易(翻译为“最快”)获取的内容。

我的猜测是,这与数据如何加载到寄存器有关。也许,对于char数组,编译器可以发挥一些魔力来并行处理,这与内存中的位置有关,以便轻松地将数据加载到寄存器中。尝试使用不同级别的优化进行编译,并尝试改用
int buffer1[1]

这也可能是一个安全问题

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

对于(i=0;i有趣的是,如果在函数1中添加一个额外的int*ret2,那么在我的系统中,顺序是正确的,而对于3个局部变量,顺序是不正确的。我猜它是按这种方式排序的,因为它反映了将要使用的寄存器分配策略。要么是这样,要么是任意的。

我不知道(虽然我想你可以打开它的源代码或找到答案),但我可以告诉你,如果出于某种原因需要,如何保证特定堆栈变量的顺序。只需将它们放在结构中:

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

如果我没有记错的话,spec保证
&ret>&z>&y>&x
。我把我的K&R放在工作中,所以我不能引用章节。

ISO C不仅没有说明堆栈上局部变量的顺序,甚至不能保证堆栈的存在。该标准只是讨论了范围和寿命块中变量的输入法。

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

因此,我做了更多的实验,下面是我的发现。这似乎是基于每个变量是否是数组。Gi即使此输入:

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和指针,最后在堆栈顶部声明,然后在更接近底部的位置声明。然后以相反的方向处理数组,声明越早,堆栈上最高的位置。我确信这是有充分的理由的。我想知道这是什么。

C标准没有t为其他自动变量指定任何布局。但是,为了避免疑问,它特别指出

[…]未指定[功能]参数的存储器布局

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

C标准中没有提到一个单词“stack”;例如,很有可能实现一个无堆栈的C实现,从堆中分配每个激活记录(尽管这些可能会被理解为形成一个堆栈)


给编译器一些回旋余地的原因之一是效率。然而,当前的编译器也会利用这一点来提高安全性,使用地址空间布局随机化等技巧,并试图使未定义行为的利用更加困难。对缓冲区进行重新排序是为了使金丝雀的使用更加有效。

我认为这是一个安全问题,或者至少是为保护堆栈而采取的措施的副作用

#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");
    }
}
#包括
int main(){
int secret=0xdeadbeef;
字符名[100]={0};
读取(0,名称,0x100);
if(secret==0x1337){
放置(“哇!这里有个秘密。”);
}否则{
puts(“我想你还没冷静到能看到我的秘密”);
}
}
当我使用默认值编译它时,甚至使用
-O0
secret
被放置在
name
开头的4个字节之前,这使得轻松利用漏洞的行为不会被忽略