这是一篇发布时间大于两年的文章,当时的一些内容或笔者曾经的思维可能已不再适用于现在,请谨慎判断文章内容的可靠性


C 语言中变量可能并不是按照代码中声明变量的顺序分配在内存中的。

问题的发生

今天在读 C 陷阱与缺陷 一书时了解到数组边界赋值溢出时会覆盖其他变量的问题,书中例子是这样的,在a[10]的地方,数组a实际上一共只有 10 个元素而第十一个会溢出导致覆盖 i 被覆盖为 0 然后进入死循环:

int i, a[10];
for (i = 1; i <= 10; i++)
    a[i] = 0;

编译运行后并没有发生死循环,于是使用 gdb 调试看了看变量的地址:

memory-allocation-order-on-stack-1

发现变量 i 的地址在数组 a 的前面,想着可能是编译器不同顺序不太一样?于是我交换了顺序:

int a[10], i;
for (i = 1; i <= 10; i++)
    a[i] = 0;

i 的地址依然在 a 前面

memory-allocation-order-on-stack-2

这就让我比较疑惑了,我在代码中声明的顺序两次都不一样,为何编译运行两次结果都是 i 的地址在 a 的前面呢?

再次尝试

于是我又写下这样的代码,多定义几个变量,然后打印它们的地址试试:

int main(){
    int m=0;
    int n=0;
    int a[4]={1,2,3,4};
    int i=0;
    int j=0;
    int b[4]={5,6,7,8};
    printf("m: %p\n",&m);
    printf("n: %p\n",&n);
    printf("a[0]: %p\n",&(a[0]));
    printf("a[3]: %p\n",&(a[3]));
    printf("i: %p\n",&i);
    printf("j: %p\n",&j);
    printf("b[0]: %p\n",&(b[0]));
    printf("b[3]: %p\n",&(b[3]));
    return 0;

结果如下:

memory-allocation-order-on-stack-3

惊奇的发现,尽管我在代码中定义变量的顺序是m->n->a[4]->i->j->[b],实际上它们在内存中按地址由低到高分布的顺序是m->n->i->j->a[4]->b[4]。经过多次尝试,发现这样不管我在代码中变量声明的顺序如何,整型数组的内存地址总是在单个整型变量的后面

也许是在编译过程中做啦某些优化?随后尝试在编译时添加-O0关闭优化、以及输出预处理的代码结果都没有发现这个问题的所在。

问题所在:GCC 的栈溢出保护

在 Google 搜索许久只得到信息:C 的标准中并没有对局部变量的内存分配顺序有任何定义,因此不同的编译器完全有可以有自己的做法。

随后 V 站上朋友有提到 缓冲区溢出攻击 这个概念,突然就意识到了 GCC 可能就是为了安全起见作出的一些调整优化以避免栈溢出攻击。带着这个概念继续在 Google 上检索,GCC 确实有默认开启对栈溢出攻击的保护,并得到了 GCC 的一个编译选项可以强制关闭栈溢出保护:-fno-stack-protector

重新编译运行:

memory-allocation-order-on-stack-4

按照变量在代码中定义的顺序,其在运行时内存分配的地址果然也变成了从高地址到低地址顺序分布了,不过仔细观察还是发现数组最后一个元素与下一个整型变量地址并不是连续的,中间还间隔了几个字节,也许是像结构体中内存对齐吧,不过我也不深究了。至于 GCC 是具体作出怎样的优化调整顺序来保护栈溢出,其文档上应该有所描述,我也不去详细查询了,了解到这里就好。