1.引言
最近在学习极客时间课程过程中碰到个挺有意思的关于数组和Linux 栈中增长方向的问题,特来与大家分享下。
话不多说,先上代码
int main(int argc, char* argv[]){
int i = 0;
int arr[3] = {0};
for(; i<=3; i++){
arr[i] = 0;
printf("hello world\n");
}
return 0;
}
代码很简单。当使用gcc 搭配上-fno-stack-protector(禁用堆栈保护)选项进行编译、运行。小伙伴认为结果应该是什么?不加上-fno-stack-protector 选项结果又是什么?
2.现象
当加上-fno-stack-protector 编译选项后,运行时程序一直循环打印 hello world。
然而不加上-fno-stack-protector 编译选项,程序只打印四次 hello world 就退出。
3.解析
当加上-fno-stack-protector进行编译时,编译器是按照C语言以及Linux中规定的方式对变量进行处理。
首先简单了解下此问题涉及到的相关知识:
1)了解Linux 进程内存分配的小伙伴肯定熟悉下图,不熟悉的可以先看下,图中箭头表示堆栈的增长方向。
程序中的局部变量i和数组arr均存储在栈空间,而Linux中栈增长方向是递减的,因此先定义的变量i地址肯定大于数组arr地址。
假设i的地址为0xb200_0020,那么数组arr的起始地址就应该是0xb200_0014(arr[0] = 0xb200_0014、rr[1] = 0xb200_0018、arr[2] = 0xb200_001c)。可能会有小伙伴想为啥数组变量的内存分布一定是这样,而非下图左侧所示呢?
2)其实数组在Linux 栈内存中这样分配的原因在于C语言中数组的某个元素的地址由来相关。
C语言中数组某个元素地址计算公式如下所示:
a[i]_address = base_address + i * data_type_size
所有的元素都是在数组基地址的基础上 + 对应的偏移量,这就使数组中索引号越大的元素,其地址越大的原因。
结合上Linux 栈中的内存分配情况,共同就导致了变量i后紧跟的是a[2]而非a[0]了。
通过以上两点知识便知道这些变量在栈内存中的分布情况,相信聪明的小伙伴们能明白为什么程序会一直打印hello world。当i = 3时,&a[3]其实指向的就是变量i地址,此时执行a[3] = 0,意味着i = 0,导致无限循环。这就是一直打印hello world根本原因。
至于为啥不加上-fno-stack-protector程序打印四次hello world就退出,了解到gcc编译时默认就会打开堆栈保护选项。
简单的打印了下变量的地址:
上图中可看到默认情况下:变量i紧随在数组array[0]后面,意味着入栈顺序为:array[2]、array[1]、array[0]、i
加上-fno-stack-protector选项后:变量i后面紧随数组array[2],意味着入栈顺序为:i、array[2]、array[1]、array[0]
编译时禁不禁用堆栈为何会导致该问题,原因后续博文追踪。
4.总结
由于对数组的越界访问未加限制以及指针访问越界,此乃C语言设计本身的一大缺陷,同时也是C语言的魅力之处。
因此在日常使用过程,数组乃至指针等直接涉及内存相关的操作,一定要注意是否存在越界的情况。很多时候未加思考,导致后期出现的bug可能极难解决。C语言涉及内存使用时一定要多思考,尽可能减少这种隐藏bug。