变量覆盖
正常的程序,函数的局部变量都是压入栈中,函数退出时,栈中数据便丢弃了,正常情况下没有问题,唯一要担心的是栈空间够不够,会不会溢出。
但51芯片不一样,51编译的程序,栈一般放在内部ram,但51芯片内部ram很小,只有256字节,还要去掉通用寄存器组和特殊寄存器。所以栈一般放在高128字节空间。
128字节能做啥,对稍微复杂点的程序来说,几个局部变量就撑爆它了。为了适用性,keil c51对编译进行魔改,编译过程中会分析函数的调用关系,形成函数调用树。
当两个函数位于不同的调用树上时,编译器认为这两个函数没有调用关系,于是将它们的局部变量都指定到ram同一地址空间。实现ram的复用,这样使用很少的ram就能运行很大的程序。
理想是很美好的,但有个前提,编译器需要明确知道所有的函数调用关系。
这里就有一个深坑,估计很多人都踩过。为了程序的灵活,一般都会在程序中使用函数指针,比如回调函数,比如状态机列表。问题就在于。对于函数指针,编译器不知道它最终调用了哪个函数,于是这样分析出来的调用树是不完整的。
但编译器不管,它按它的理解,复用这不同调用树的函数局部变量空间。于是就是发生这样的问题。执行函数指针调用的函数也许就把调用者的局部变量覆盖了,因为编译器认为这两个函数之间没有调用关系。所以局部变量都安排在一个空间。但实际它们有调用关系,后面执行的函数会覆盖掉调用者的局部变量。
这种问题表现都很诡异,有些甚至不可思议。比如一个工程运行的好好的,似乎很正常,然后在全局变量的结构体中,增加一个字段。立马一运行就死机。一脸懵。比如调试程序,加个打印函数看看,程序行为立马变了。抓狂。
太久没有搞51这种低端芯片了,都快20年了,刚毕业那几年,在厦门一家安防公司,就是使用的51,刚开始还用汇编来写程序。记得调试一个很小的功能,一直不行,搞了好几周,最后只好请老大来帮忙看看。老大一眼就看出中间一个调试打印函数修改了寄存器,导致结果一直不对。一脸黑线。从此对汇编一点都不感冒。开始自学使用C语言开发51程序。后来老大还跑过来请教怎么用c来开发51程序。
后来就再也没有使用过51了,基本都是arm级别的。越来越复杂,跑linux,使用c++,使用rust,20年过去了,以为这辈子应该没机会再碰51了。没想到啊,没想到,世事轮回,苍天绕过谁。
刚开始也是百思不得其解,程序逻辑没问题啊,为啥动一下就死。这不科学啊。一直以为是程序问题,到处找bug。什么也没找到,直到有一天,突然想着,我不优化程序有没有问题呢,于是将优化等级从9级改为0级,世界瞬间安静了,什么异常都消失了,唯一的问题是ram和rom都快撑爆了,想加新功能是不可能的了。于是问了下ai,优化和不优化为啥表现不一样,ai说51程序优化的话,局部变量会覆盖。我天,瞬间通了。程序里面有个巨大的状态机,使用的是函数列表的方式,这不是函数指针是啥。程序里面有几个下层调用上层的接口,使用的是回调函数,这不是函数指针是啥。程序里面有个定时器使用的是注册函数,超时执行,这不是函数指针是啥。
真是坑,坑死我了,于是花了两天时间,重构了程序,将函数指针全部改成直接调用。所有的问题都消失了,测试插拔几十次,没有出现异常。
可是我就纳闷了,为啥提供这个demo的原厂,芯片都出货了,程序这个样子,能用?还是说他运气好,都没碰到过异常。