打印
[经验分享]

C 堆栈溢出

[复制链接]
1895|81
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
sanfuzi|  楼主 | 2024-7-24 12:27 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

1。除非你的程序非常大,否则不要考虑堆栈溢出的问题,还是找找其它错误的原因。更不推荐自己手动修改启动文件,来增加堆栈使用空间。

2。现在大部分编译器(就我所知),没有堆栈溢出检测!!自己小心。一般是由于递归调用外加中断引起的。

http://blog.linuxsky.org/463/viewspace-14564.html

Keil C是非常优秀的C51编译器,可能是最好的C51编译器,提供各种优化模式,对变量的优化和地址安排做得非常好。这是用C语言写代码的好处之一,如果用汇编写,得费一大番功夫给各个变量安排内存物理地址,还得时刻记住哪些地址的内存单元是已经分配了,新增加的变量就不能占用那些已经分配了的单元,以免产生内存交叠冲突和溢出。我一直非常信赖Keil C51的编译结果,在我的印象里,它对内存的分配是完美的,只要代码用它编译时没有报告任何warning和error,代码运行时不可能内存冲突或溢出的现象。

但,今天发生的事情证明我错了。

手头上有个产品的代码,代码量很大。程序跑起来的效果不大好,因此打算把代码优化一下。代码量越大,通常可优化的地方也越多。对8051来说,访问芯片内部的data区(0~7FH)内存速度是最快的,直接访问,一条指令就能读写,而idata区(80H~FFH)虽然还是内存区,但由于地址分配上跟特殊寄存器SFR重合,只能间接地址访问,两条指令才能读写,速度稍慢点,而外存xdata区(0~7FFFH)必须使用DPTR指针才能访问,速度是最慢的。很明显,优化的原则就是尽量把频繁读写的变量优先安排在data区,然后是idata区,最后才是xdata区。

当我做完变量手工优化工作后,把编译模式设为SMALL,这样C51编译器会自动把那些我没手工指定存放区的变量优先安排进data区,如果超出有效地址范围,它会报错,因此我大可以放心。按下rebuild all按钮后,编译器提示:

Program Size: data=236.2 xdata=19321 code=43372

"ipphone_main" - 0 Error(s), 0Warning(s).

编译器提示的data区包括了idata在内,按以往的经验来看,data区有256个byte,程序才使用了236.2个,还剩下19个,没有溢出,而xdata有32k,现在才使用了19k,远没有溢出,编译结果一切很正常。

把代码烧录进芯片跑起来后,结果出人意料,从现象来看,上电约1秒后就自动重启,重启后过1秒又重启,非常有规律的重启。

我没有怀疑是编译器的原因,当时第一念头是怀疑是看门狗,代码里上电后就打开了看门狗,可能某些子程序代码执行时间过长,看门狗复位了,于是在有怀疑的地方插入了喂狗代码,重新编译后再测试,依然自动重启。于是干脆就把看门狗的代码注释了,不使用看门狗,以为这回没问题了吧,结果出人意料,还是重启。

我仔细想了一下,能造成8051的重启的原因不多,一是看门狗引起的重启,这点可以排除;二是某些8051支持重启指令,我手头上用的这款虽然支持,但我没用过那指令,这点也可以排除;三是8051被强干扰,把取指寄存器PC的内容改变了,改成0,于是就重启了,这点也可以排除,因为如果现场有强干扰,没优化前也会重启才对。

由于没想出来是什么原因,于是开始折腾,把优化的变量一个个恢复成未恢复优化的状态,每恢复一步就重新测试一次。终于在恢复一个16字节的数组时发现程序正常了,仔细看了一下,那数组定义在xdata区的时候程序就完全正常,而定义在idata区的时候程序就复位了,虽然奇怪的是,定义在idata区时,编译器并没有报告内存溢出。跟踪汇编指令也没发现异常,无论定义在idata还是xdata,编译器为该数组分配的地址证明确实都是有效地址,确实没有溢出,编译器的安排还是正确。

虽然还没找到根源,但问题既然是出现在内存上,我于是决定查看当那个数组指定为idata类型时的内存分配。Keil C51在编译时会输出一个M51文件,该文件包含了大量的内存分配信息,非常详细,包括哪个变量被编译器分配到哪个内存地址,占用多少个字节,哪些变量是局部变量,可以重复利用……这个M51文件里都有详细的列表。

从列表里的变量分配地址一路看下来,都没错,边看还边惊叹编译器对变量的分配安排非常精确,但看到最后一个堆栈指针的安排时,终于发现问题所在了,它是这样安排的:


TYPE BASE LENGTH RELOCATION SEGMENT NAME
----------------------------------------------------------------------------------------------
IDATA 0080H 0034H UNIT _IDATA_GROUP_
IDATA 00B4H 0022H UNIT ?ID?IPPHONE_MAIN
IDATA 00D6H 001FH UNIT ?ID?DNS_NICRCV?IPPHONE_DNS
IDATA 00F5H 0004H UNIT ?ID?DISP
IDATA 00F9H 0001H UNIT ?STACK

这上面标有STACK的段就是堆栈分配,上面的数据表明,SP堆栈指针安排在F9H这个地址,堆栈空间是1个字节!表面看没有溢出,但我的程序里使用了中断服务,进入中断服务时,至少需要8个字节的堆栈空间(保存R0~R7寄存器)来进行保护现场,8051使用的是递增压栈的设计,堆栈指针往往被安排在内存空间的后面可用部分,每压栈一个字节,SP指针往上加1,进中断服务时,至少压栈8个字节,F9H+8,超出了FFH,堆栈指针不能超过FFH,也就是说堆栈溢出了!原来这就是导致程序不断重启的原因,不是变量内存溢出,而是堆栈溢出!

而当我把那个数组指定为xdata类型后,由于该数组不再占用idata区,于是IDATA一下子多了16个字节的可用空间,重新编译后的M51这样安排:

IDATA 0080H 0024H UNIT _IDATA_GROUP_
IDATA 00A4H 0022H UNIT ?ID?IPPHONE_MAIN
IDATA 00C6H 001FH UNIT ?ID?DNS_NICRCV?IPPHONE_DNS
IDATA 00E5H 0004H UNIT ?ID?DISP
IDATA 00E9H 0001H UNIT ?STACK

从这组数据来看,SP指针安排到在E9H这个地址,堆栈空间有FFH-E9H+1=23个字节,对于程序来说已经够用,因此程序运行正常。

多次调整变量类型的编译结果表明,C51对于堆栈空间需求大小不作计算,任何代码都只是按堆栈空间只有1个字节需求来分配(在我眼里看来这明显是胡来,稍复杂点的子程序调用都不可能只要1个字节就能完成现场保护),由于堆栈只能分配在data区和idata区,因此当一个程序为了优化而data区占用太多时,虽然编译器能编译成功,但往往SP堆栈指针被分配在data区的最后面,很容易造成堆栈空间不够而溢出。为保险起见,最好保证编译后的SP值安排在F0H之前,那样至少有16个字节的堆栈空间,才能最大限度保证程序不会跑飞。


使用特权

评论回复
沙发
bestwell| | 2024-8-4 09:13 | 只看该作者
在单片机C语言编程中,堆栈溢出是一个常见的问题,它发生在程序的堆栈空间被耗尽时。

使用特权

评论回复
板凳
mattlincoln| | 2024-8-4 10:42 | 只看该作者
堆栈溢出可能导致程序无法正常运行,表现为程序崩溃或异常终止。

使用特权

评论回复
地板
minzisc| | 2024-8-4 14:13 | 只看该作者
提高程序员对堆栈溢出问题的认识,通过教育和培训来减少这类错误的发生。

使用特权

评论回复
5
benjaminka| | 2024-8-4 15:21 | 只看该作者
在函数内部定义过多或过大的局部变量会占用栈中的大量空间。当栈的空间不足以容纳这些变量时,就会发生堆栈溢出。

使用特权

评论回复
6
pentruman| | 2024-8-4 16:12 | 只看该作者
根据实际情况,调整单片机的堆栈设置,增加堆栈空间大小。但需要注意,堆栈空间的增加会消耗更多的Flash和RAM资源,需要综合考虑。

使用特权

评论回复
7
chenqianqian| | 2024-8-5 08:23 | 只看该作者
调整堆栈大小是很正常的事情

使用特权

评论回复
8
zerorobert| | 2024-8-5 14:23 | 只看该作者
尽量减少函数中局部变量的数量和大小,特别是大型数组。

使用特权

评论回复
9
yeates333| | 2024-8-5 16:24 | 只看该作者
一些编译器或调试工具提供了堆栈检测功能,可以帮助识别潜在的堆栈溢出问题。例如,Keil C51编译器提供了堆栈溢出检测选项。

使用特权

评论回复
10
wengh2016| | 2024-8-6 17:28 | 只看该作者
如果确定堆栈溢出是由于堆栈空间不足引起的,可以考虑增加堆栈的大小。这可以通过修改编译器设置或调整硬件配置来实现。

使用特权

评论回复
11
linfelix| | 2024-8-6 21:40 | 只看该作者
对于大型数据结构,考虑使用静态存储或全局变量,而不是在堆栈上分配。

使用特权

评论回复
12
robertesth| | 2024-8-9 03:42 | 只看该作者
单片机中的C语言堆栈溢出是一个常见的问题,特别是在资源受限的环境中。堆栈溢出发生在程序使用的堆栈空间超过了分配给它的大小时。

使用特权

评论回复
13
zerorobert| | 2024-8-9 03:54 | 只看该作者
在函数内部声明过大的局部数组或复杂的数据结构,会消耗大量的堆栈空间。

使用特权

评论回复
14
51xlf| | 2024-8-9 04:06 | 只看该作者
使用硬件看门狗可以防止程序进入死循环。一旦程序进入死循环,看门狗会复位单片机,重新运行程序。这虽然不是直接解决堆栈溢出的方法,但可以避免程序因堆栈溢出而长时间无法恢复。

使用特权

评论回复
15
updownq| | 2024-8-9 04:20 | 只看该作者
当函数递归调用自身次数过多,且每次调用都需要在堆栈中保存上下文信息,容易导致堆栈空间不足。

使用特权

评论回复
16
jtracy3| | 2024-8-9 04:33 | 只看该作者
当多个函数嵌套调用时,每个函数都会占用栈中的一定空间。如果嵌套次数过多,就会导致栈空间不够用而发生溢出。

使用特权

评论回复
17
kkzz| | 2024-8-9 04:46 | 只看该作者
如果一个函数不断地调用自己而没有适当的退出条件,堆栈空间最终会被耗尽。

使用特权

评论回复
18
lzmm| | 2024-8-9 05:00 | 只看该作者
如果使用递归算法,应限制递归的深度,以避免堆栈溢出。可以通过设置递归深度限制或转换为非递归算法来实现。

使用特权

评论回复
19
10299823| | 2024-8-9 05:12 | 只看该作者
减少不必要的局部变量,特别是大型数据结构。
尽量避免深度递归调用,或者将递归算法改为迭代算法。

使用特权

评论回复
20
alvpeg| | 2024-8-9 05:24 | 只看该作者
在ISR中使用大量局部变量或进行函数调用会快速消耗堆栈空间。

使用特权

评论回复
发新帖 我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

19

主题

2988

帖子

1

粉丝