C 中变量在堆栈上分配内存顺序的一点疑问

2018-07-18 17:14:21 +08:00
 abowloflrf

今天在读 C 陷进与缺陷 一书时了解到数组边界赋值移除会覆盖其他变量的问题,书中例子这样写会导致覆盖i然后死循环:

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

于是自己就尝试了一下,发现并没有发生死循环,调试了发现i的地址在数组a之前,于是又将声明顺序交换,依然是i在前面。

然后自己再尝试这样:

int a[4]={1,2,3,4};
int m=0;
int n=0;
int b[4]={5,6,7,8};
int i=0;
int j=0;

调式发现打印的地址是

m:    0x7ffffe275a90
n:    0x7ffffe275a94
a[0]: 0x7ffffe275aa0
a[3]: 0x7ffffe275aac
i:    0x7ffffe275a98
j:    0x7ffffe275a9c
b[0]: 0x7ffffe275ab0
b[3]: 0x7ffffe275abc

即变量在内存中的顺序是 m->n->i->j->a[]->b[] 似乎是 gcc 先给 int 分配了内存然后再给 int 数组分配,随后修改了几次声明顺序依然是这样的规律。

Google 上查了下有人说并没有任何标准定义 C 语言中变量内存分配的顺序是按照代码的顺序来的。但是它确实是有这样一个规律,我也好像没有找到什么官方的解释变量究竟是按照怎么样的顺序在堆栈上分配的,以及为何要这样做。

在这里请教一下,不知道有没有人了解。

3525 次点击
所在节点    C
19 条回复
pagict
2018-07-18 17:21:10 +08:00
你把优化关了再试试呢

觉得是编译器为了字节对齐,调整了压栈顺序
abowloflrf
2018-07-18 17:22:47 +08:00
@pagict 开 -O0 也是这样
across
2018-07-18 17:29:47 +08:00
不知道的。猜是在词法分析阶段排序过了,数组在后面内置类型后面吧。
abowloflrf
2018-07-18 17:38:23 +08:00
@across 对从结果上看确实是这样,只是没找到解释
zhicheng
2018-07-18 17:46:59 +08:00
任何不符合标准的操作,行为都是不确定的,一万个编译器可以有一万种做法,没有必要去深究这个。
BlackKey
2018-07-18 17:50:41 +08:00
像变量在栈上如何排布和编译器以及环境有关,不属于 C 语言标准要求的内容,包括开了优化以后内存和变量都不一定是一一对应的关系了。
我印象中 C 唯一对内存排列有要求的部分是结构体内的变量必须按顺序排。
也许编译器的文档会有这方面规则的描述,总之这种规则都是编译时定的。
另外,如果你说 x86 架构的话,压栈的顺序是从高地址向低地址的,也就是说地址高的是先被压入栈的。
kljsandjb
2018-07-18 18:02:04 +08:00
我觉得吧,1. 好的习惯是把数组 a 定义在前面 2. 没出问题只是恰好没有缓冲区溢出,从安全角度来讲很容易被攻击,比如把 a 作为参数传递,数组退化成指针,修改内存,这个时候超过一定量,比如你这边是 10,就覆盖调用者的栈了,严重点覆盖返回值,然后你函数不知道返回到哪儿去了 23333
kljsandjb
2018-07-18 18:06:50 +08:00
关于顺序的话,我猜测是优化,不过这方面也不精通…坐等大神来解:)
easylee
2018-07-18 18:09:38 +08:00
请问楼主的编译器版本号是啥?
hx1997
2018-07-18 20:01:29 +08:00
试试加上 -fno-stack-protector 选项再编译看看,可能是 GCC 启用了 canary 栈保护。
hx1997
2018-07-18 20:05:22 +08:00
现代编译器默认会启用一些防止栈溢出攻击的技术,比如 canary 或者 NX,可以通过开关来关闭这些技术。
hx1997
2018-07-18 20:10:44 +08:00
@hx1997 #10 严格来说调整局部变量顺序不算 canary😂,不过 canary 和调整顺序都是 GCC stack-smashing protection 的一部分。
abowloflrf
2018-07-18 20:17:26 +08:00
@kljsandjb
@hx1997

是这样的,谢谢 7 楼的提醒还有缓冲区溢出,没想到这里来,刚刚下班路上查了下确实 gcc 有栈溢出保护的措施,是默认开启的,刚刚尝试加上-fno-stack-protector 编译然后地址也确实是顺序分配了。至于具体内存分配顺序是怎么优化的可能 GCC 文档上有写我也不详细将了解了。
jsrdzhk
2018-07-18 20:21:14 +08:00
你编成汇编看看😁我瞎说的
BlackCat02
2018-07-18 20:44:37 +08:00
这种例子都是只存在于教科书上的例子了,实际编译器的实现中,变量的排布都是不一样的,数组溢出之后不一定就恰好覆盖 i。事实上 i 都不一定在栈上保存,可能直接被优化成了纯寄存器的变量
kljsandjb
2018-07-18 20:45:38 +08:00
@abowloflrf 加了 canary 后,发生栈破坏一般会终止程序,另外从内存利用效率来说的话,结构体中元素一般会建议按照从大到小顺序排列,也就是 6 楼提到的对齐的补充 :)
abowloflrf
2018-07-18 21:00:52 +08:00
@kljsandjb 嗯是的,就是脑子里也默认把这些变量也当作和结构体一样顺序排列的了,今天遇到这个问题调试看地址才知道编译器会调整局部变量的位置而不是按照代码声明的顺序来的哈哈
kljsandjb
2018-07-18 21:09:48 +08:00
@abowloflrf 哈,我也是正好最近在学习到 csapp 第三章,现学现卖了 :)
reus
2018-07-19 10:44:31 +08:00
深究 UB 毫无意义。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/472047

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX