数组越界

有如下代码:
int main() { int i = 0; int a[3] = {0}; for (; i <= 3; i++) { a[i] = 0; printf("Hello world\n"); } return 0; }
这是一段 C语言代码,可以很容易就发现for循环出现了数组越界i<=3
数组size是3, a[0], a[1], a[2],当 i = 3
时,数组 a[3]
访问越界。 这个问题其实很容易发现。 那么运行这段代码会发生什么呢? ... 按照 x86架构编译运行结果并非是打印三行"hello world",而是会无限循环。 这是因为C语言中,只要不是访问受限的内存,所有内存空间都是可以自由访问的。a[3]会被定位到某块不属于数组的内存地址上,而a[3]这个地址正好是存储变量i的内存地址,那么 a[3]=0
就相当于 i=0
,下次循环 i++
就把 i 从0变成了1,这样 i<=3
成立继续循环,所以就会导致代码无限循环。
这里涉及到栈内存分配需要遵循的规则:
局部变量默认在栈(Stack)分配空间,栈是一种后进先出(LIFO)的数据结构
栈内内存分配通常从高地址向地址增长,因此先定义的变量
i
会获得较高地址,后定义的数组a
获得较低地址变量
i
(4 bytes) 和数组a
(12 bytes) 在栈上连续存储 由此得出典型内存布局(x86架构):
低地址 -> [arr[0]] (4字节) [arr[1]] (4字节)
[arr[2]] (4字节)
高地址 -> [i] (4字节)
数组元素地址随下标增长由低到高排列
注意这只是理论上的结果,实际上用Ubuntu gcc编译后,打印四行"hello world"就会报错然后退出,就像这样:
Hello world Hello world Hello world Hello world *** stack smashing detected ***: terminated Aborted
可以尝试把变量 i
和数组 a
的内存地址打印出来:
&i: 0x7ffca6f4e2d8 &a[0]: 0x7ffca6f4e2dc &a[1]: 0x7ffca6f4e2e0 &a[2]: 0x7ffca6f4e2e4 &a[3]: 0x7ffca6f4e2e8 *** stack smashing detected ***: terminated Aborted
可以看到变量 i
的地址是 0x***d8,而数组 a
的起始地址恰好是 &i + 4
: 0x***d8+4=0x***dc 相比之下变量 i
是低地址,数组 a
是高地址,这和内存地址分配从高地址低地址不相符。 不过数组内的地址倒是符合元素地址随下标增长由低到高排列
造成这种现象的原因是编译器优化策略:
GCC默认会根据目标架构特性调整栈帧布局, x86架构中可能采用向下增长栈,但变量从低到高填充的策略以提高缓存命中率
现代GCC会启用栈保护技术,将数组等易溢出目标放在高地址,防止覆盖关键控制数据,在变量间插入Canary值检测溢出
x86架构允许编译器自由选择局部变量排列顺序
如需保持传统的高地址优先分配的布局方式,可采用编译选项: gcc -fno-stack-protector -O0 program.c
这会禁用栈保护并关闭优化。看来编译器为了防止不靠谱程序猿煞费苦心啊😂
看似简单的一段代码包含如此多知识点,都是非常基础的知识,很适合作为面试题目。 有感而发写一段题外话: 记得工作第一年做的一款嵌入式产品,开机屏幕显示时候总是会Crash,那时我在外地出差和模块负责人不在一起办公,为了解决问题经常打电话沟通,盯着代码看了几天没有找出来问题所在。 就在Manager决定通宵攻关(某司喜好攻关)时,我发现了root cause -- 数组越界。数组size比较大我是一点一点数数找出来的,太TM累了。搞定后有点小Happy,当天给自己放了个小假 -- 没加班6点下班就撤了。
C语言开发最好还是用容器,再小心也可能会犯低级错误。
For the best experience view this post on Liketu
看不懂 嘎嘎嘎