最近新开始的项目遇到一个小问题,记录一下。
背景是这样的,我们之前做过一款SSD,有多种容量。以前开发这款产品的时候,我们采用编译期配置的方式,在开始编译每种容量的fw之前,需要先跑一个配置脚本将config.c文件中的#define都修改成对应容量的config。所以,假如现在有7种容量的SSD,我们需要编译7次,生成7个不同的bin file。这样既浪费编译时间,也不利于客户后续upgrade。现在,我们需要base在原项目的基础上,用同款SOC搭配新的nand flash开发新款SSD。在此背景之下,公司决定采用运行期读取配置的方式,这样只需要编译一份fw即可(虽然会牺牲一部分runtime的性能,还需要额外的动态空间分配,不过不在讨论范围之内)。
因为比较简单,新任务被直接分配到刚毕业的童鞋手中。这个同学也很快完成了初步的代码,兴高采烈地上电启动,但是很不幸,SOC上电跑了还没有半秒钟就挂掉了。从uart上看,SOC上电之后也就只打印了寥寥几句log。这位同学一开始还觉得没什么太大不了,既然卡住了,那就再加log呗,代码我不熟悉,二分法也总能找出来吧!于是继续加更多的log,可是问题逐渐变得有点儿离谱了,因为他发现,每次上电卡住的位置都是不一样的,有时候甚至只是移动了一句打印,上电卡住的地方就会差很远了。这个同学经验不足无法解决,问题拖了大约有一周的时间。在过年前几天我刚好闲下来了,于是就帮忙看了一下这个问题。
从他反映的上电现象来看,这个问题感觉很像是cpu跑死了。跑死了是什么意思呢?32位arm架构的内核有7种工作模式,SSD fw相对简单,不会使用到这么多工作模式,一般只需要两种就可以了,比如sys mode + irq mode ,而其他模式下的代码在基于SOC厂商sdk的基础上一般不会再做改动。比如当cpu fetch到了不合法的指令,就会进und mode,这时cpu就会切换到该模式下的堆栈并且跳转到对应的handler。这个旧项目有个特别坑爹的地方就是,它除了irq mode(正常跑使用sys mode),其他异常模式下的代码都是一个简单的死循环,所以即便发生了exception也无从得知。因此,要做的第一件事情就是先为其他几种模式加上合适的handler,至少需要在里面保存cpu register的值以及sys mode下的sp、lr等以便debug。
我们在handler里加了register dump以及stack dump的代码。不出所料,上电之后马上就进入了und mode的exception handler,而且stack dump也非常有特点,sys mode下sp指针附近的堆栈都被填充成了0xB0B0B0B0,甚至好几个register连带lr都是0xB0B0B0B0,这显然是不合理的。而且,我们这款SOC是双核的,两个cpu都进入了exception。不过这下问题也还是好办多了,在项目代码中搜索“0xB0B0B0B0”,它出现在以下代码片段中。
这里看起来应该是将cpu1 svc mode下的stack全部init成0xB0B0B0B0,方便之后检查stack的使用量。根据之前的stack dump,这个地方很有可能发生了问题,导致了越界的初始化。查看了一下链接之后生成的map文件,发现Image$$SVC_Stack_1$$ZI$$Length这个变量竟然变成0了。我们现在来看一下initstack这段汇编在Image$$SVC_Stack_1$$ZI$$Length为0的时候是如何导致越界的。
这下答案应该相当明显了,initstack这个过程会先递减r4,再将r4与0做比较,假如r4是0,那么递减一次之后r4会溢出,最终结果就是导致stack的初始化越界了。这里的svc stack是cpu1的stack,但是由于溢出,也把cpu0位于dtcm上的所有stack都盖住了,所以两个cpu都进入了exception mode。至于为何Image$$SVC_Stack_1$$ZI$$Length会变成0,后来对比新旧两份代码,才发现是这个同学为了节省一些dram的空间,把一些放置在dram上的debug用的uart cmd给注释掉了。有趣的是,其中有一道uart cmd正是用来检查cpu1的stack状态的,它引用到了SVC_Stack_1这个全局变量。当把这个cmd注释后,linker认为没有人引用到这个SVC_Stack_1就把它的length定为0,最终导致了以上的现象。把这道uart cmd重新打开之后,问题就解决了。