《借鉴MISRA规范减少C语言程序隐患》,转载自《单片机与嵌入式系统应用》2004年第3期,略有删节。作者:
清华大学陈文刚
MISRA(Motor Industry Software Reliability Association,汽车工业软件可靠性协会)曾经发布了一套嵌入式C语言的编程规范。这套规范包含127条规则,鼓励程序员按照这些规则来编写C语言代码,从而减少程序潜在的隐患。这套规则本来是为汽车工业中的嵌入式软件编程而制定的,但是由于这套规范的代表性,很多其他行业,例如通信、航天等也逐渐开始借鉴其中的一些编程规范。本文就其规范的一些内容予以讨论。
1、背景
没有任何一种编程语言可以确保它的最终执行过程会和程序员的预期完全相同。在程序执行的过程中,会引起各种意想不到的问题,这些问题包括:
(1)程序员的失误。比如敲错了字母或理解错了算法;很多初学者会把逻辑比较“==”写成赋值“=”,这种错误简单的编译器是无法察觉的。
(2)程序员对编程语言的错误理解。C语言的编译预处理过程其实非常复杂,但是很多程序员并没有意识到这一点。
(3)编译器没有按照程序员的意图工作。不同的编译器的工作方式是不同的,这一点常常被经验不足的程序员忽视。
(4)编译器的错误。编译器是一种软件工具,同样也会有Bug;另外C语言的复杂性使得有些编译器的编写者对其理解错误。
(5)操作平台的差异。嵌入式操作系统有上千种,这些平台之间的差异肯定是存在的。
(6)运行错误。程序运行种出现的错误更是数不胜数,一些诸如溢出、指针等错误只有在运行过程中才能被发现。
总而言之,由于涉及的平台众多,以及其本身安全性、可移植性的要求,在嵌入式软件编程过程中,如何规范的使用C语言已成为一个重要的课题。
2、MISRA C编程规范
MISRA的127条规则看上去有些过于苛刻,很多程序员恐怕不能完全遵循。下面是其中最有价值的部分规则。
※注释不要嵌套使用,C语言不支持“/*”的嵌套使用。
※char、int、short、long、float、double这样的变量类型不应该直接使用,因为不同的编译器对它们的比特长度认为可能是不同的,所以推荐使用下面的方法来替代:
typedef signed short SI_16; /*在头文件中定义*/
这样可以定义SI_16为16位长的有符号整型,如果在有些平台上的int为16位整型,而short为8位整型,在头文件中将上面的语句修改为:
typedef signed int SI_16;
显然这样做有利于代码移植。
※char类型一定要定义为unsigned char或者signed char型,因为char类型到底是有符号类型还是无符号类型取决于编译器。所以在定义char类型的时候要直接制定其类型,避免将来移植过程中的错误。
※不要使用八进制的数字,因为它极易造成混淆。
※局部变量名不要同全局变量名相同;变量的作用范围最好是在函数内,除非有必要才定义全局变量。
※作用于文件范围内的变量要定义为static型,这样避免与其他文件中同名的全局变量冲突。
※不要定义寄存器类型的变量,好的编译器知道如何处理这些变量。
※局部变量在使用前一定要赋值,因为只有静态变量的缺省值为0,局部变量则不一定。
※在定义非全零数组、结构的时候要使用括号。
例如:
SI_16 y[3][2] = {1,2,3,4,5,6}; /*不正确*/
SI_16 y[3][2] = {{1,2},{3,4},{5,6}} /*正确*/
※移位操作符不应该在有符号变量上使用。
※以为操作不应该超过变量本身的位长度,例如一个16位长的整型,移位操作的范围应该在0~15。避免犯这种错误的最好办法是以为操作的时候使用常量。
※在计算取余操作之前应该明确相应编译器的工作方式。例如有的编译器计算-5/3 =-1,余数是-2;还有的编译器计算-5/3 = -2,余数是+1。
※隐式类型转换可能会丢失信息,这种转换应当避免。例如,应当避免无符号类型和有符号类型在同一个表达式中出现。
※混合精度的计算表达式中,应当采用显示强制类型转换来保证期望的结果。
例如:
UI_16 i = 1;
UI_16 k = 3;
F_64 d0 = i/j; /*结果不正确= 0.0*/
F_64 d1 = (F_64)(i/j); /*结果不正确= 0.0*/
F_64 d1 = (F_64)i/j; /*结果正确= 0.333*/
F_64 d2 = (F_64)i/(F_64)j; /*结果正确= 0.333*/
UI_16 i = 65535;
UI_16 j = 10;
UI_32 e0 = i+j; /*结果不正确= 9*/
UI_32 e1 = (UI_32)(i+j); /*结果不正确= 9*/
UI_32 e2 = (UI_32)i+j; /*结果正确 = 65545*/
UI_32 e3 = (UI_32)i + (UI_32)j; /*结果正确 = 65545*/
※浮点类型变量不应该做逻辑相等比较。
例如下面的程序是不推荐的:
F_32 x,y;
/*一些计算过程*/
if(x == y)
{/*...*/}
※不要使用goto、continue语句,它们会给程序的结构性带来问题。break语句只能在switch语句中使用,其他情况也不要使用。
※if、else if、else、while、do...while、for语句都有用括弧来完成相应的语句,即使只有一行语句。
if(test){
x = 1; /*即使一句也要加括号*/
}
else
x = 3; /*没有加括号是错误的方式,不利于以后的维护和修改*/
y = 2; /*这行语句是后来增加的,但是由于前面没有括号,使得程序员的本意被歪曲*/
※所有的if、else if结构语句都要在最后包含else语句;switch语句也一定要有default语句。
if(x<0){
log_error(3);
x = 0;
}/*这种情况不需要else语句*/
if(x<0){
log_error(3);
x = 0;
}
else if(y<0){
x = 3;
}
else{/*这个else是必须的,即使程序员认为代码永远不会走到这里*/
errorflag = 1;
}
※浮点型变量不能用作循环计数,用作计数的变量在循环体内不能修改其内容。
flag=1;
for(i=0;(i<5)&&(flag == 1);i++){
/*...*/
flag = 0;/*正确,这样可以提前退出循环*/
i = i +3;/*不正确,修改了计数器变量*/
}
※函数不应该直接或间接地调用自己。因为递归算法对于一个安全稳定的系统来说是可怕的。
※无参数的函数应该用void声明。
void myfunc(void);
※一个函数应该只有一个出口,即一个return语句。
※如果一个函数可能返回错误信息,那么调用它的时候一定要检查返回值。
※如果一个函数可以用宏来实现,那么最好使用宏。因为宏的速度更快,例如,
#define abs(x) (((x)>=0)?(x):-(x))
※用宏来定义的函数的参数要用括号来表示。
#define abs(x) (((x)>=0)?(x):-(x)) /*正确*/
#define abs(x) x>=0?x:-x /*不正确*/
※两重指针以上的指针类型不应该使用。
※如果一个函数可能返回空指针NULL,那么调用这个函数后一定要检查返回值是否为空。
※引用标准函数库的时候,参数的有效性一定要检验,否则无效的参数可能会带来无法预知的后果。例如sqre和log函数的参数不能为负数等等。
※禁止使用setjmp、longjmp和signal机制。
※禁止使用stdlib.h库中的atoi、atof、atol等函数。因为,当出现不能正常进行字符串转换时会带来不可预知的错误,而且嵌入式系统中这些函数也不太需要。
※禁止使用stlib.h库中的abort、exit、setenv、system函数。因为这些函数要和环境打交道,会带来不可预知的错误。
※不要使用time.h库中的时间函数。因为不同的系统的时间格式等内容差异很大,除非你对这些内容都非常清楚。
※不要使用stdio.h库中的I/O库函数。如fgetpos、fopen、ftell、gets、perror、remove、rename、ungetc等。其实这些函数在嵌入式系统中也没有什么作用。
※在 switch 中case返回值的时候,要考虑各种情况的返回值;
3、总结
遵循这些规则可以减少程序的隐患,同时也增加了程序员编程的难度。这些规则也许在使用过程中一不小心就会被忽视,所以在实际应用过程中,建议使用一些辅助工具。例如静态分析工具可帮助程序员分析自己的程序是否符合规范,避免将隐患带到产品中
*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。