阴沟翻船,马失前蹄,说明凡事皆有可能。自然,程序设计的再好,也会有crash的时候。开发期还还说,正式交付的系统crash自然更是难以承受的。无论何时,死一次就够了,得有方法查个水落石出。
几年前哥去广州的一家民企呆过些日子。刚到那,就碰上系统毫无线索的crash。咋办?哥想静下心来,花点时间做个工具去定位,但无奈硬件出生的领导天天赶着大家守在机房。唉,无知啊,天天守在机房,面对crash,哥想到的只有我儿子常常念的诗--来如春梦不多时,去如朝霞无觅处。嗯,crash,哥只能数数又crash了几次。
再后来,哥被请去了另外家公司,挂了个闲职,要求就一个,帮忙解决嵌入式系统开发碰到的,解决不了的问题。在那还是蛮幸福,因为哥那些日子确实需要时间,大把大把时间,来照顾家人。
这当中,就碰到三个最典型的问题:memory leak, system crash, system halt。内存泄漏以后再讲,今天只说crash,当然,halt跟crash的解决方法有很大重叠,明天有空就简单的说一说。
首先,简单定义下讨论对象:嵌入式操作系统,使用ucos,vxworks,nucleus等操作系统。Linux自带coredump功能,不在讨论范围。
接下来,普及下CPU的寄存器知识。以ARM为例,处理器存在几个不同的寄存器组,对应不同的模式:用户模式,中断模式,数据访问终止模式,未定义指令模式undef等。系统crash,除去硬件原因(比如电不足等),都是先触发异常:数据异常,取指异常,或者未定义异常。未定义异常一般来说是没有的,除非故意改掉指令的opcode,比如野指针,咱不谈它,那太难了!
有兴趣了解更多,可以读一读ARM的文档:
http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.ddi0344f/Beiibjca.html
当Data aboat或是Prefetch abort出现后,CPU即切换到数据访问终止模式。我们要做的,就是抢在系统开始处理前,把用户寄存器信息保存下来。以及abort模式下LR寄存器内容。接下来,把控制权还给系统的正常流程。当然也不是绝对,哥碰到的系统,总是可以在异常里挂coredump处理函数。万一系统没给你留这个钩子,那就麻烦些,需要修改cpsr到用户模式,调用coredump处理函数;完成后,再返回abort模式,将处理权交还系统。当然,你不想交权也可以,coredump完了,就直接复位CPU。反正你该收集的信息都收集了,系统接下来的异常处理也没什么价值。
Coredump处理函数做的事,也很简单,首先确定当前运行的task和tcb。操作系统一定会提供这个全局变量,或是获取接口。然后,就是把堆栈信息保存起来。很多时候,堆栈会很大,很难保存所有数据。这时,就有几个策略:遍历堆栈,找出所有函数调用;提取栈顶的一段数据。其中原理等下再讲。
系统复位了,刚刚存的数据还在么?当然在,我们用SRAM来保存这些数据。CPU复位但不掉电的情况下,SRAM数据会完整的保留着。不过,系统要带SRAM。当然,没SRAM也可以,只不过做起来有点难度,需要flash驱动做些特别支持。数据需要先保存到内存中,然后在coredump处理函数中写入flash。写flash需要时间,因此要记得喂狗和写flash两不误;还有就是flash一般不能是文件系统,应该是直接的地址映射。flash的驱动必须配这种特俗的写操作。
然后,系统重新启动运行。这时候,我们先赶紧把刚才存在SRAM或是flash的数据复制出来,写入到coredump文件,免得夜长梦多。这个过程就很简单了!
接下来,解释几个技术问题。
怎么遍历堆栈,找到所有函数调用?
很简单。用readelf,objdump一类工具打开编译出来的软件包,你就能发现,函数地址总是在某个区间内。如果是动态加载的系统,则会提供代码段的区间范围。提前找到这个范围就好了,硬编码在你的程序里,且不需要精确,多一点没关系,毕竟相对于32位系统的地址空间来说,这段地址占的比例总是可以忽略不计,误抓取的信息也就非常有限。记得,宁可错杀一千一万!
为什么要保存栈顶数据?
一般来说,最后的一些列操作有更大的嫌疑。从分析角度来说,较远位置的错误,堆栈数据分析的难度非常大。其实,就经验来看,只要能找到函数调用的层次关系,再加上代码走读,基本上都能确定故障原因。堆栈数据分析,也就是不得已才做的,尤其是面对编译使用了优化选项的故障。
如何使用coredump文件?
coredump文件包含三个内容:寄存器信息,函数调用信息,堆栈数据。函数调用信息可以在线翻译,只要利用cshell里的符号表就可以直接查询那些地址,并翻译成可读的函数名。当然,最好的办法是离线处理。这里需要明确,编译时必须带-g参数用以产生addr2line需要的符号表内容。正式发布的软件包可以额外使用strip方法去掉符号表,但需要同时保存带符号表的原始编译软件包和发布软件包。利用addr2line对coredump保存的疑似地址检查,可以梳理出完整的系统调用层次。记得用Perl,或是Python等写个脚本来做这个!
很遗憾,哥找了半天,也没找到以前做的那段搞寄存器的汇编代码段,也没那段异常处理的函数代码,更也没找打coredump分析的perl脚本。早知道就该在给上家做这个的时候,偷偷复制一份供日后参考的。当然,也没什么,在弄清楚具体系统的工作原理后,相关实现估计只要一两个工作日来完成,只是没SRAM的方式,蛮复杂的,需要时间也多点,尤其测试会麻烦很多。