C语言的栈溢出问题
例如:针对学习过程中遇到的栈溢出问题
- C语言的栈溢出问题
- 前言
- 栈溢出(Stack overflow)
- 导致栈溢出的原因
- ①函数递归层次太深
- 1.修改栈区空间大小
- 2.尾部递归优化
- (附一)设置优化选项(O1/O2)
- (附二)解决“/O1”和“/RTC1”命令行选项不兼容
- ②局部变量体积太大
- 解决问题
- ③动态申请空间使用之后没有释放
- ④数组访问越界
- ⑤指针非法访问
- 总结
前言
溢出,常见的解释是:程序外部的数据大小,超出其规定数据类型所能表达的范围,从而造成正常程序预期外的错误表达。 溢出一般被当做黑客攻击操作系统的途径。具体的溢出类型有以下三种:缓冲区溢出(Buffer overflow);内存溢出(memory overflow);数据溢出(data overflow)。 本文只谈及缓冲区溢出的——栈溢出
栈溢出(Stack overflow)
在谈栈溢出之前,先提一下一个程序所占用计算机内存的分布:
导致栈溢出的原因
一般导致栈溢出的原因有五点原因:
①函数递归层次太深。 递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈(栈)溢出。
②局部变量体积太大。 (因为局部变量是存储在栈中的)
③动态申请空间使用之后没有释放。 虽然程序中动态申请空间没有释放,在程序结束之后,系统也会将进程中申请的内存全部释放,但是程序运行中时间过长,导致内存占用过大,进程耗尽所有内存。申请后不手动释放就会内存泄露(C语言中是用malloc和free来分配空间和释放空间(内存)的)
④数组访问越界。 C语言中是没有提供数组下标越界检查,如果在程序中出现数组下标访问超出数组范围,就会出现内存访问错误 (例如进行字符串拷贝或处理用户输入等)
⑤指针非法访问。 指针保存了一个非法地址(其实也可以看做是指针越界),再通过指针访问所指向地址的时候就会出现内存错误。
此处我想提及学习过程中遇到的递归循环发生的系统栈溢出。
①函数递归层次太深
①函数递归层次太深
为了避开数据溢出的干扰,这里我选用了一个比较简单的加法运算来举例
int Fun(int n){if (n == 1)return 1;elsereturn n + Fun(n - 1);}int main(){printf("%d\n", Fun(10000));return 0;}
在运行到第5211次(当然每一次都不确定,大约都是在5000次左右)的时候,直接就爆出Stack overflow(栈溢出)
那么这段程序在栈区干了些啥呢?就把人家弄爆了。(入栈,此程序还未出栈,以下只是补充画的出栈图)
递归层次太深,其实也是函数调用过多。函数调用的本质是:入栈(push)和出栈(pop),递归中每一次调用函数时,都会入栈保存一个栈帧,里面保存着该函数的调用信息、内部变量(后面简称这两玩意为“调用记录”),这样栈空间总有被耗干的时候。 此处的Fun函数就是如此,”被调用10000次,一直调用到5211次时还没到可以返回的时候,我们写的这个程序还没到出栈,栈空间就满了。并且我们发现,即使是栈没满的情况,程序正常运行,函数又往反方向一个一个的出栈,其实效率是很慢的。
1.修改栈区空间大小
解决问题①(函数递归层次太深)的方法还是挺多的,直接一点的就修改栈堆保留大小 ,它的默认是1MB,改大了就可以了。(此处针对VS修改)
具体步骤:项目->属性->链接器->堆栈保留大小
现在就不会溢出了。
2.尾部递归优化
然后再就是尾部递归优化,递归:函数自己调用自己;尾调用:函数最后一步是调用另一个函数。那么尾递归就是函数最后一步调用自己(两个结合起来)。 那么就永远只有一个栈帧,调用函数会在栈区产生栈帧,最后一步return …把当前函数的结果当作参数传递给了另外一个自身调用,由于尾调用是函数的最后一步操作,所以它不需要外层函数的调用记录了,这样外层函数的栈帧空间就被释放了(因为它已经完成了return任务,已经没用了)。
之前的普通递归之所以不会释放,是它每次还需要保留一个外部变量n,因为执行Fun(n-1)调用Fun()时还要用到n。而尾部递归本来返回的就是此次调用的结果(作为下一次调用的函数参数),也没有额外的变量需要被保存等着算,那么上一层的Fun_iter()栈帧空间释放了也不影响return的下一次调用。
int Fun_iter(int num, int sum){if (num == 1)return sum;elsereturn Fun_iter(num - 1, num + sum);} int main(){printf("%d\n", Fun_iter(10000, 1));return 0;}
上面讲的是尾部递归,那么传值的时候,谁会去传两个值,用户输入秉持的是用户的体验,用户说要算10000以内数字的和,他肯定只会输入10000啊,不会帮你再传一个参数1的。所以可以改良一下:
int Fun(int n){return Fun_iter(n, 1);}int Fun_iter(int num, int sum){if (num == 1)return sum;elsereturn Fun_iter(num - 1, num + sum);} int main(){printf("%d\n", Fun(10000));return 0;}
现在栈也不会溢出了,因为尾递归永远只保留一次调用记录。
当然我想提醒一下,尾部递归优化的实际效果是来源于编译器的优化!!!意思就是是编译器决定要不要按照你的逻辑,在尾部递归的时候每次释放上一次空间,这是由编译器的心情决定的。如果你的编译器不支持,那么即使使用尾部递归也会栈溢出。对于gcc编译器3.2.2(别的博主的旧版本)-O2或者-O3就可以完成尾部递归优化。VS目前看到的是从2010版本使用O1及以上的优化选项,一般就会尾递归优化。我是VS2019,用的O1就可以尾部递归优化了。
(附一)设置优化选项(O1/O2)
右键你的项目文件->属性->C/C+±>优化->优化->最大优化(优选大小)(/O1)
这里优化默认的是已禁用(/Od),改成O1就可以了
(附二)解决“/O1”和“/RTC1”命令行选项不兼容
当然如果你出现以下报错
解决:项目->属性->代码生成->基本运行时检查->默认值
将原本的两者(/RTC1,等同于 /RTCsu) (/RTC1)改为默认值
现在你的vs编译器可以尾部递归优化了。
对于gcc编译器,想改就加个 #pragma GCC optimize(2) ,就可以改成-O2
我这个是在vscode下配置的gcc,我的gcc是 8.1.0版本的,没有更改优化选项,也可以实现尾部递归优化。
②局部变量体积太大
②局部变量体积太大。
函数里定义的很大的局部变量,(比如很大的数组)也会导致栈溢出,因为局部变量是存在栈区的。这种情况,你要么直接修改栈区大小,上面已讲。要么你就把局部变量改为全局变量(静态变量),存在静态区或者存在堆上。
比如举个例子:
int main(){int a[1000000] = {0};return 0;}
在没有修改栈区空间的情况下会栈溢出,如图:
解决问题
那么我们可以把它设为全局变量,或者在堆区分配 动态内存分配空间
记得malloc与free要一起搭配使用哦!malloc()就是在堆区分配一块指定大小的内存空间,用来存放数据。
③动态申请空间使用之后没有释放
③动态申请空间使用之后没有释放。
堆一般由程序员手动释放,如果不释放,程序结束可能有OS收回,也有可能不会,全凭OS心情。比如malloc()申请完了,没释放,就噶了。②中写了就不写了。
解决问题:没释放那就去释放呗!
④数组访问越界
④数组访问越界。
先来看看数组越界访问后,发生些什么吧。发现访问下标<0的元素,会发生下限越界 ;访问下标>数组长度,会发生上限越界。(此处注意此时访问越界,没有报错,只有警告)还没完呢,咱继续。
之前介绍说得也很清楚了,数组访问越界后,栈区还是会给越界部分分配空间,越界后会按照数组的内存继续写(你看地址是连续的),如果此时数组越界部分的内存有数据,那么越界部分数据会“被”它覆盖掉。 所以你想如果你想要存入的数据很重要,数组越界后,又碰巧遇到越界那部分内存原本放有别的数据,那么你要存入的数据就被改写了(被覆盖),那问题就严重咯~
我们来验证一下,是不是会被覆盖。
int main(){int i = 0;int arr[] = { 1,2,3,4,5 };arr[6] = 8;//printf("arr[%d] = %-10d %p\n", 6, arr[6], &arr[6]);for (i = 0; i <= 7; i++){printf("arr[%d] = %-10d", i, arr[i]);printf("%p, i的地址%p\n", &arr[i], &i);}arr[5] = 6;//printf("arr[%d] = %-10d %p", 5, arr[5], &arr[5]);return 0;}
可以看到,经过越界访问得到的随机值,已经被刚开始我们写的arr[6]给覆盖了。并且通过arr[5]的例子,我们发现即便arr[5]已存有了随机值-858993460,我们也可以对越界访问到的内存进行改写,和正常修改数组的值一样。 不过会报:Stack around the variable ‘arr’ was corrupted 的错,这个error的意思是变量arr周围的栈被破坏了。arr只开辟了00FDFE24~00FDFE34中的这段内存,但是我们修改的是这段空间之外的内存进行修改,这就是这个error的意思。之前还看到有的博主说数组下限越界不会报错,无法察觉,辟个谣,会报错哈!无论上限下限越界都是arr周围栈被破坏。
这里还有个点就是arr[7] = 7,以及arr[7]的地址和 i 的地址是一样的。
也就是说本来40里面保存的是变量 i 的值,而访问中 i 是下标,一直在做++运算,到arr[7]时,40里保存的是 i = 7。而当访问到arr[7]时,编译器发现这块黄色的内存里已经放有数据7了,就直接覆盖掉了arr[7]越界访问会产生的随机值。所以arr[7]得到的是7。
想解决这类问题,那就只能靠程序员自己注意了。因为本来出现数组访问越界的问题,其根本还是程序员的自己的问题。
⑤指针非法访问
⑤指针非法访问。
此时指针已经不叫指针了,它叫野指针,野指针:指针指向的位置不可知,或者是非法的,无效的。
int main(){int arr[] = { 1,2,3,4,5 };int* pa = &arr;int i = 0;for (i = 0; i <= 10; i++){//printf("arr[%d]的值%-12d ", i, arr[i]);*pa = i;//printf("Pa访问arr[%d]的地址%p ",i, &*pa);//printf("arr[%d]的值%-12d\n", i, arr[i]);pa++;}return 0;}
看到这个error是不是很熟悉,刚刚才遇到过,仔细看代码,在for循环中,咱除了访问了合理合法的arr中的5个元素,4个下标,从下标值为5开始,就是在非法访问了(越界访问),得到的值就如同数组越界访问所得的随机值,接着我觉得还不够,还干了一件把访问到的值改写*pa = i,改写数组范围内的没有任何问题,但是超出数组范围,也就是从5开始,你又开始自己开创空间,还往里面改写数据(赋值覆盖),也就是变量arr周围的栈被破坏了。
总结
以上就是今天记录的内容,本文介绍了五种导致栈溢出的问题以及解决办法,原计划是还准备了整型溢出和算术溢出的问题,那就安排到其他的子文章中记录吧。(求生欲:小白专属)