嵌入式Linux中文站

嵌入式C软件维护经验


本人工作中维护着一个基于嵌入式Linux的一份SIP协议栈。一年多来,有如下心得:
(1)非常熟悉你维护的代码,并且为它的体系结构、各个模块(含Makefile之类的软件构建配置文件)、重要函数、重要CASE编写文档。使用Source-Navigator之类的工具高效地浏览整个工程,理清体系结构、调用关系。
(2) 嵌入式软件测试技巧:一是要做单元测试、集成测试、系统测试(系统测试是黑盒的,可以用Python脚本自动化一些测试用例)。二是努力让整个软件尽量少 地修改(不需要修改一行代码,只改Makefile中的CC/AR等平台相关的设置)就完整地运行在Linux PC上。在DDD或gdb中以单步调试、断点等手段复现BUG后定位和解决BUG非常容易,尤其是“段错误”BUG:可以利用DDD或gdb打开产生的 Core Dump就能获得段错误发生时的调用堆栈及所有层次上的环境信息。用Valgrind检查软件的各种内存错误(Valgrind在检查内存非法访问的功能 只适用于动态分配的内存,不适用于栈中的内存)。用ltrace/strace查看executable所有的库调用/系统调用。要是以上措施都搞不定 BUG,那就老老实实地用printf或日志吧。
(3)熟悉C语言的常见的陷阱,如:[1]任何两整数加/减/乘都可能溢出,尤其当心两无符号型 减法/乘法以及无符号与有符号的混合运算(不要为了减少编译器的警告就不加思索地做强之类性转换)。[2]strncpy/malloc等函数的规格要清 楚,它们都有一些使用的注意事项。熟悉这些陷阱就能够主动、高效地发现BUG而不是被动、低效地解决BUG。无论是主动还是被动, DDD/Valgrind之类的工具都有用武之地,但仍然高度依赖于维护者的经验(主要就是熟悉C语言的常见的陷阱)。这方面一是要多看看《C编程精 粹》,《C陷阱与缺陷》之类的经典书籍;二是要利用PC-Lint/Splint之类的C代码静态检查工具。
(4)不要轻信别人的代码和文档。仔细测试软件依赖的那些不属于你维护范围的库(通常是黑盒的)。
(5)熟悉本软件与其他软件互通所遵循的标准。SIP之类的开放标准要直接看标准原文。用户定制的互通性要求有时与标准冲突,要提出来。避免把软件改得不伦不类。

在我看来,BUG的起源有3类:
(1)自己掉进了C语言的陷阱
(2)自己的某些设计逻辑有BUG
(3)别人维护的东西有BUG

《Write Clean Code》经典语录
(1)要从程序中删去无定义的特性或者在程序中使用断言来检查出无定义特性的非法使用。
(2)只要用户使用了“<”操作符或其它要用有符号信息的操作符,就迫使编译程序产生不可移植的代码。记住一个原则不要在表达式中使用“简单的”字符。由于位域也有同样的问题,因此也有一个类似的原则:任何时候都不要使用“简单的”位域。
(3)经常反问:“这个变量表达式会上溢或下溢吗?”
(4)有风险的惯用语就是这样一些短语或表达式,它们看上去似乎能够正确地工作,但实际上在某些特殊场合下,它们并不能正确执行。C语言就是具有这样一些惯用语的语言,最好的办法是:无论什么时候,只要有可能就尽量避免使用这些惯用语。在memchr中有风险的惯用语是:
pchEnd = pch + size;
while( pch < pchEnd )

C语言还有许多其它的有风险的惯用语。有个最好的方法来找到自己经常使用的有风险的惯用语,这就是检查以前出现的每一个错误,再问一下自己:“怎样来避免这些错误?”然后建立个人的风险惯用语表从而避免使用这些惯用语。

《C专家编程》经典语录
(1)对无符号类型的建议
尽量不要在你的代码中使用无符号类型,一面增加不必要的复杂性。尤其是,不要仅仅因为无符号数不存在负值(如年龄、国债)而使用它来表示数量。
尽量使用像int那样的有符号类型,这样在涉及升级混合类型的复杂细节时,不必担心边界情况(如-1被翻译成非常大的正数)。
只有在使用位段和二进制掩码时,才可以用无符号数。应该在表达式中使用强制类型转换,使操作数均为有符号数或者无符号数,这样就不必由编译器来选择结果的类型。
(2)语言的细节决定了一种语言到底是可靠的还是容易滋生错误的。
(3)用lint程序彻查程序的价值不仅仅在于去除现存的Bug,而且能防止新的Bug污染source base。我们现在要求对源代码的所有修改或增加必须能通过lint程序检查,这样就能保持程序的lint-clean状态。
(4)警惕Interpositioning
Interpositioning 就是通过编写与库函数同名的函数来取代该库函数的行为。不仅你自己所进行的所有对该库函数的调用将被自己版本的函数调用所取代,而且所有调用该库函数的系 统调用也将用你的函数取而代之。当编译起注意到库函数被另一个定义覆盖时,它通常不会给出错误信息。这也是遵循C语言的设计哲学,即程序员所做的都是对 的。
绝大多数程序员都没有记住C标准库中所有函数的名字,而且像index或mktemp这样常见的名字其重复概率之高令人吃惊。有时候,这方面的Bug会带入到产品代码中去。
准则:不要让程序中的任何符号成为全局的,除非有意把它们作为程序的接口之一。
(5)UNIX编程常见的两个运行时错误:
bus error (core dumped)和segmentation faule (core dumped)
总线错误和段错误的准确原因在不同的操作系统版本上各不相同。以下描述的是运行于SPARC架构的SunOS出现这两类错误以及产生错误的原因。
事 实上,总线错误几乎都是由于为对齐的读或写引起的。它之所以成为总线错误,是因为出现未对齐的内存访问请求时,被堵塞的组件就是地址总线。对齐 (alignment)的意思即使数据项只能存储在数据项大小的整数倍的内存地址上。在现代的计算机架构中,尤其是RISC架构,都需要数据对齐。通过迫 使每个内存访问局限在一个Cache行或一个单独的页面内,可以极大地简化(并加速)Cache控制器和内存管理单元这样的硬件。
union{char a[16];
int i;
}u;
int *p=(int *)&(u.a[1]);
*p=17;/*p中未对齐的地址会引起一个总线错误!*/
一个好的编译器发现未对齐的情况时会发出警告,但它并不能检测到所有未对齐的情况。编译起通过自动分配和填充数据(在内存中)来进行对齐。...当把一个char指针转换为int指针时,就会出现神秘的总线错误。

本文永久更新链接:http://embeddedlinux.org.cn/emb-linux/system-development/200810/15-196.html



分享:

评论