本人工作中维护着一个基于嵌入式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指针时,就会出现神秘的总线错误。
段错误或段违规(segmentation violation)--在Sun的硬件中,段错误是由于内存管理单元(负责支持虚拟内存的硬件)的异常所致,而该异常通常是由于引用一个为初始化或非法值的指针引起的。
[问]pc-lint与linux中的splint有何区别?因为没用过pc-lint,能不能用splint来代替?
[答]PC-lint是一个由Gimpel Software提供的支持C/C++的商用程序。
Splint (原来的 LCLint) 是一个GNU免费授权的 Lint程序,但是只支持C不支持C++. Splint http://www.splint.org/
在软件开发过程中,我们可以利用源码检查工具来找出常见的编程错误以及安全漏洞。gcc选项"-Wall -Werror"编译给出的警告并不够,并且编译器版本不同,警告的范围也不相同!
这些工具用起来并不复杂,下面介绍splint和flawfinder这两款源码检查工具的使用方法。但需要注意的是,虽然这些工具能够分担一部分工作,但却无法完全替代人类。因为工具在发现漏洞的同时,也可能遗漏安全漏洞。
Splint跳过所有系统头文件,还发生解析错误,主要有以下原因:
(1)语法不符合C89,例如变量pAlarm定义不在语句块的开头:
if (g_oMDAlarmStatus == EVENT_ALERT){
g_oMDAlarmStatus = EVENT_ALARM;
Alarm* pAlarm=(Alarm*)malloc(sizeof(Alarm));
pAlarm->intSignal=SIG_SIGUSR2;
EvtNotify(pAlarm);
printf("sigusr2(): Called EvtNotifyMain/n");
}
(2)数据类型或函数是编译器扩展的,例如__sigset_t应当用sigset_t代替:
sigemptyset((__sigset_t *)&sa.sa_mask);
还有GCC编译器内建的数据类型__built_va_list被GCC作为基本类型了...所以,Splint手册指出系统头文件一般是不可解析的!要处理GCC所有内建的预处理宏、数据结构、函数很难,Splint邮件列表有人尝试以这种方式lint linux内核模块成功了。
可能自编C文件依赖于include的系统头文件中某些扩展宏、数据结构、函数声明,可以自己编写与之同名的头文件。这个很容易的:不需要修改工程中任何文件,并且一般写两三个很短的头文件就行了。
(3)Splint的unixlib包括posixlib包括isolib。但都不包含
定义的UINT_MAX等宏
Splint的unixlib比posixlib多了(如果用posixlib,自己写的这几个同名头文件似乎就不会被splint include进去?checking macros阶段看出来用上了?):
[1]定义的socklen_t,in_addr_t,sa_family_t等类型
[2]定义的fd_set类型和select函数(遵循POSIX 1003.1-2001)。
[3]定义的pthread_t,pthread_mutex_t等类型。但却有pthread_create等函数声明。
怎样查看splint的posixlib库包含了哪些头文件?
Splint知道的POSIX规格是IEEE 1003.1-1990,所以不支持之后规格(如IEEE Std 1003.1b-1993)导入的特性,如siginfo_t结构体等。有两个办法解决:[1]干净的办法是,更新posix.h后重新生成posix.lcd和posixstric.lcd。Splint手册第14.2节讲述了这方面内容:splint源码包的lib目录下就有standard/posix/unix三个库的头文件和lcd文件。按照指示生成一个lcd文件后,在之后运行splint检查时以"-load"选项加载一个自己创建的lcd(注意:最多只能加载一个自己创建的lcd)。[2]As a quick-and-dirty solution, you could provide a dummy definition for siginfo_t.
(4)自己工程定义的宏没有设置好导致语法错误...BUG:)
Splint解析错误发生时,如果一眼看不出原因,可以给splint命令加“+keep”选项使得其保留预处理之后的C文件。GCC的"-E"选项输出预处理之后的C文件。用以下办法获得C预处理器所有预定义的宏:
touch foo.h
cpp -dM foo.h