[讨论] “安全第一”的C语言编程规范

HOHO   2009-12-16 09:37 楼主
在我的旧程序中遇到了这样的问题,
unsigned int fun(int x, int y)
{
    ...
}
if (fun(x,y) > 6000) 当fun返回值为0xffff时,这个条件总是不成立,这个问题隐藏了好久到今天才发现,真是危险呀。
现在认为fun的返回值在与常数6000对比的时候,是作为int型数据-1的,所以总是比6000小。
为解决这个问题我找了很多网页,找的过程中发现了下面这个文章,“针对汽车工业软件安全性的C语言编程规范”我也是第一次见到,
根据这个思想确实可以避免我遇到的这个问题,在这里和大家分享一下。

“安全第一”的C语言编程规范
来源: 单片机与嵌入式系统应用   作者:清华大学 陈萌萌 邵贝贝
编者按: C语言是开发嵌入式应用的主要工具,然而C语言并非是专门为嵌入式系统设计,相当多的嵌入式系统较一般计算机系统对软件安全性有更苛刻的要求。1998年,MISRA指出,一些在C看来可以接受,却存在安全隐患的地方有127处之多。2004年,MISRA对C的限制增加到141条。
  嵌入式系统应用工程师借用计算机专家创建的C语言,使嵌入式系统应用得以飞速发展,而MISRAC是嵌入式系统应用工程师对C语言嵌入式应用做出的贡献。如今MISRA C已经被越来越多的企业接受,成为用于嵌入式系统的C语言标准,特别是对安全性要求极高的嵌入式系统,软件应符合MISRA标准。
  从本期开始,本刊将分6期,与读者共同学习MISRAC。
  第一讲:“‘安全第一’的C语言编程规范”,简述MISRAC的概况。
  第二讲:“跨越数据类型的重重陷阱”,介绍规范的数据定义和操作方式,重点在隐式数据类型转换中的问题。
  第三讲:“指针、结构体、联合体的安全规范”,解析如何安全而高效地应用指针、结构体和联合体。
  第四讲:“防范表达式的失控”,剖析MISRAC中关于表达式、函数声明和定义等的不良使用习惯,最大限度地减小各类潜在错误。
  第五讲:“准确的程序流控制”,表述C语言中控制表达式和程序流控制的规范做法。
  第六讲:“构建安全的编译环境”,讲解与编译器相关的规范编写方式,避免来自编译器的隐患。
  C/C++语言无疑是当今嵌入式开发中最为常见的语言。早期的嵌入式程序大都是用汇编语言开发的,但人们很快就意识到汇编语言所带来的问题——难移植、难复用、难维护和可读性极差。很多程序会因为当初开发人员的离开而必须重新编写,许多程序员甚至连他们自己几个月前写成的代码都看不懂。C/C++语言恰恰可以解决这些问题。作为一种相对“低级”的高级语言,C/C++语言能够让嵌入式程序员更自由地控制底层硬件,同时享受高级语言带来的便利。对于C语言和C++语言,很多的程序员会选择C语言,而避开庞大复杂的C++语言。这是很容易理解的——C语言写成的代码量比C++语言的更小些,执行效率也更高。
  对于程序员来说,能工作的代码并不等于“好”的代码。“好”代码的指标很多,包括易读、易维护、易移植和可靠等。其中,可靠性对嵌入式系统非常重要,尤其是在那些对安全性要求很高的系统中,如飞行器、汽车和工业控制中。这些系统的特点是:只要工作稍有偏差,就有可能造成重大损失或者人员伤亡。一个不容易出错的系统,除了要有很好的硬件设计(如电磁兼容性),还要有很健壮或者说“安全”的程序。
  然而,很少有程序员知道什么样的程序是安全的程序。很多程序只是表面上可以干活,还存在着大量的隐患。当然,这其中也有C语言自身的原因。因为C语言是一门难以掌握的语言,其灵活的编程方式和语法规则对于一个新手来说很可能会成为机关重重的陷阱。同时,C语言的定义还并不完全,即使是国际通用的C语言标准,也还存在着很多未完全定义的地方。要求所有的嵌入式程序员都成为C语言专家,避开所有可能带来危险的编程方式,是不现实的。最好的方法是有一个针对安全性的C语言编程规范,告诉程序员该如何做。
1  MISRAC规范
  1994年,在英国成立了一个叫做汽车工业软件可靠性联合会(The Motor Industry Software Reliability Association,以下简称MISRA)的组织。它是致力于协助汽车厂商开发安全可靠的软件的跨国协会,其成员包括:AB汽车电子、罗孚汽车、宾利汽车、福特汽车、捷豹汽车、路虎公司、Lotus公司、MIRA公司、Ricardo公司、TRW汽车电子、利兹大学和福特VISTEON汽车系统公司。
  经过了四年的研究和准备,MISRA于1998年发布了一个针对汽车工业软件安全性的C语言编程规范——《汽车专用软件的C语言编程指南》(Guidelines for the Use of the C Language in Vehicle Based Software),共有127条规则,称为MISRAC:1998。
  C语言并不乏国际标准。国际标准化组织(International Organization of Standardization,简称ISO)的“标准C语言”经历了从C90、C96到C99的变动。但是,嵌入式程序员很难将ISO标准当作编写安全代码的规范。一是因为标准C语言并不是针对代码安全的,也并不是专门为嵌入式应用设计的;二是因为“标准C语言”太庞大了,很难操作。MISRAC:1998规范的产生恰恰弥补了这方面的空白。
  随着很多汽车厂商开始接受MISRAC编程规范,MISRAC:1998也成为汽车工业中最为著名的有关安全性的C语言规范。2004年,MISRA出版了该规范的新版本——MISRAC:2004。在新版本中,还将面向的对象由汽车工业扩大到所有的高安全性要求(Critical)系统。在MISRAC:2004中,共有强制规则121条,推荐规则20条,并删除了15条旧规则。任何符合MISRAC:2004编程规范的代码都应该严格的遵循121条强制规则的要求,并应该在条件允许的情况下尽可能符合20条推荐规则。
  MISRAC:2004将其141条规则分为21个类别,每一条规则对应一条编程准则。详细情况如表1所列。
表1  MISRAC:2004规则分类

  最初,MISRAC:1998编程规范的建立是为了增强汽车工业软件的安全性。可能造成汽车事故的原因有很多,如图1所示,设计和制造时埋下的隐患约占总数的15%,其中也包括软件的设计和制造。MISRAC:1998就是为了减小这部分隐患而制定的。
  MISRAC编程规范的推出迎合了很多汽车厂商的需要,因为一旦厂商在程序设计上出现了问题,用来补救的费用将相当可观。1999年7月22日,通用汽车公司(General Motors)就曾经因为其软件设计上的一个问题,被迫召回350万辆已经出厂的汽车,损失之大可想而知。
  MISRAC规范不仅在汽车工业开始普及,也同时影响到了嵌入式开发的其他方向。嵌入式实时操作系统μC/OSII的2.52版本虽然已经于2000年通过了美国航空管理局(FAA)的安全认证,但2003年作者就根据MISRAC:1998规范又对源码做了相应的修改,如将
  if ((pevent->OSEventTbl[y] &= ~bitx) == 0) {
    /*… */
  }
的写法,改写成
  pevent->OSEventTbl[y] &= ~bitx;
  if (pevent->OSEventTbl[y] == 0) {
  /*… */
  }
发布了2.62的新版本,并宣称其源代码99%符合MISRAC:1998规范。
  一个程序能够符合MISRAC编程规范,不仅需要程序员按照规范编程,编译器也需要对所编译的代码进行规则检查。现在,很多编译器开发商都对MISRAC规范有了支持,比如IAR的编译器就提供了对MISRAC:1998规范127条规则的检查功能。
2  MISRAC对安全性的理解
  MISRAC:2004的专家们大都来自于软件工业或者汽车工业的知名公司,规范的制定不仅仅像过去一样局限于汽车工业的C语言编程,同时还涵盖了其他高安全性系统。

图1  汽车事故原因分布图
  MISRAC:2004认为C程序设计中存在的风险可能由5个方面造成:程序员的失误、程序员对语言的误解、程序员对编译器的误解、编译器的错误和运行出错(runtime errors)。
  程序员的失误是司空见惯的。程序员是人,难免会犯错误。很多由程序员犯下的错误可以被编译器及时地纠正(如键入错误的变量名等),但也有很多会逃过编译器的检查。相信任何一个程序员都曾经犯过将“= =”误写成“=”的错误,编译器可能不会认为
  if(x=y)
是一个程序员的失误。
  再举个例子,大家都知道++运算符。假如有下面的指令:
  i=3;
  printf(“%d”,++i);
输出应该是多少?如果是:
  printf(“%d”,i++);
呢?如果改成-i++呢?i+++i呢?i+++++i呢?绝大多数程序员恐怕已经糊涂了。在MISRAC:2004中,会明确指出++或--运算符不得和其他运算符混合使用。
  C语言非常灵活,它给了程序员非常大的自由。但事情有好有坏,自由越大,犯错误的机会也就越多。
  如果说有些错误是程序员无心之失的话,那么因为程序员对C语言本身或是编译器特性的误解而造成的错误就是“明”知故犯了。C语言有一些概念很难掌握,非常容易造成误解,如表达式的计算。请看下面这条语句:
  if ( ishigh && (x == i++))
很多程序员认为执行了这条指令后,i变量的值就会自动加1。但真正的情况如何呢?MISRA中有一条规则:逻辑运算符&&或 的右操作数不得带有副作用(side effect)*,就是为了避免这种情况下可能出现的问题。
*所谓带有副作用,就是指执行某条语句时会改变运行环境,如执行x=i++之后,i的值会发生变化。
  另外,不同编译器对同一语句的处理可能是不一样的。例如整型变量的长度,不同编译器的规定就不同。这就要求程序员不仅要清楚C语言本身的特性,还要了解所用的编译器,难度很大。
  还有些错误是由编译器(或者说是编写编译器的程序员)本身造成的。这些错误往往较难发现,有可能会一直存留在最后的程序中。
  运行错误指的是那些在运行时出现的错误,如除数等于零、指针地址无效等问题。运行错误在语法检查时一般无法发现,但一旦发生很可能导致系统崩溃。例如:
#define NULL 0
  ……
  char* p;
  p=NULL;
  printf(“Location of 0 is %d\n”, *p);
语法上没有任何问题,但在某些系统上却可能运行出错。
  C语言可以产生非常紧凑、高效的代码,一个原因就是C语言提供的运行错误检查功能很少,虽然运行效率得以提高,但也降低了系统的安全性。
  有句话说得好,“正确的观念重于一切”。MISRAC规范对于嵌入式程序员来讲,一个很重要的意义就是提供给他们一些建议,让他们逐渐树立一些好的编程习惯和编程思路,慢慢摒弃那些可能存在风险的编程行为,编写出更为安全、健壮的代码。比如,很多嵌入式程序员都会忽略注释的重要性,但这样的做法会降低程序的可读性,也会给将来的维护和移植带来风险。嵌入式程序员经常要接触到各种的编译器,而很多C程序在不同编译器下的处理是不一样的。MISRAC:2004有一条强制规则,要求程序员把所有和编译器特性相关的C语言行为记录下来。这样在程序员做移植工作时,风险就降低了。
3  MISRAC的负面效应
  ...
4  发展中的MISRAC
  ...
5  对MISRAC的思考
  ...
参考文献
1  MISRAC:2004, Guidelines for the use of the C language in critical systems. The Motor Industry Software Reliability Association, 2004
2  Harbison III. Samuel P, Steele Jr. Guy L. C语言参考手册. 邱仲潘,等译. 第5版. 北京:机械工业出版社,2003
3  Kernighan. Brian W, Ritchie. Dennis M. C程序设计语言. 徐宝文,等译. 第2版. 北京:机械工业出版社,2001
4  Koenig Andrew. C陷阱与缺陷. 高巍译. 北京:人民邮电出版社,2002
5  McCall Gavin. Introduction to MISRAC:2004, Visteon UK, http://www.MISRAC2.com/
6  Hennell Mike. MISRA CIts role in the bigger picture of critical software development, LDRA. http://www.MISRAC2.com/
7  Hatton Les. The MISRA C Compliance Suite—The next step, Oakwood Computing. http://www.MISRAC2.com/
8  Montgomery Steve. The role of MISRA C in developing automotive software, Ricardo Tarragon. http://www.MISRAC2.com/
生活在激情中 ... 希望 哈哈 https://home.eeworld.com.cn/?80086

回复评论 (14)

MISRAC
PC-LINT
强者为尊,弱者,死无葬身之地
点赞  2011-12-10 15:19
长见识了
点赞  2012-9-23 18:17
嗯,数据类型不统一,很要命。

基本默认,常数或者省略数据类型的,都会默认为int。
int型可以在某种程度上认为是C的标准类型。当然,说的是 整型。

它的位长,经常代表了 机器 的数据宽度。(此处,数据 是为了 区分 地址 的那个 数据,而非泛泛意义上的 数据)。说 经常,因为我不知道是否这方面的规定;
强者为尊,弱者,死无葬身之地
点赞  2012-9-23 23:51
正在看 《Linux C编码规范》 和 《华为C编码规范》,两种规范一些细节仍有所不同,纠结于参照哪一种规范呢

看看 MISRAC 是否适合嵌入式开发~~~~~~~~
点赞  2012-9-24 00:29
我觉得楼主第一个说的有点问题,当unsigned int和int比较时,自动将int转化为unsigned int型,因此fun()>6000成立,我运行过了,这个是成立的
#include
unsigned int fun()
{
    return 0xffffffff;
    }
int main()
{
    unsigned int i;
    if((i=fun())>6000)
    printf("1");
    else
    printf("2");
    return 0;
    }
printf出来的是1,而不是2.而且不是字长的原因,改成0xffff输出也一样。
点赞  2012-9-24 08:51

回复 5楼 bill_shi68 的帖子

不适合才怪。
MISRAC本身就是为 汽车电子行业 制定的。
汽车电子必须是嵌入式啊。
强者为尊,弱者,死无葬身之地
点赞  2012-9-24 17:45
大家在用C开发单片机、嵌入式时都参考什么规范呢? 比如变量、函数命名等
点赞  2012-9-24 19:21

回复 7楼 辛昕 的帖子

指点一下6楼的帖子啊
点赞  2012-9-25 08:34

回复 9楼 qq626926200 的帖子

没试,也没想明白,所以没吭声
强者为尊,弱者,死无葬身之地
点赞  2012-9-25 14:25

回复 8楼 bill_shi68 的帖子

你不妨先看看 华为编程规范,相当不错。
虽然还不是非常全面
强者为尊,弱者,死无葬身之地
点赞  2012-9-25 14:25

回复 6楼 qq626926200 的帖子

  1. unsigned int i;
  2.     if((i=fun())>6000)
  3.     printf("1");
这个写法有点怪。
(i = fun())> 6000,
你确定是拿i和6000比较么?
强者为尊,弱者,死无葬身之地
点赞  2012-9-25 14:28

《MISRA-C:2004》编程规范中的解释比较清楚

《MISRA-C:2004》编程规范中的解释比较清楚
  • 123.JPG
点赞  2012-9-26 13:57

回复 12楼 辛昕 的帖子

应该是i跟6000比较,把i=去掉之后,直接fun()>6000 结果也是一样,所以我感觉楼主最开始举的那个例子有问题。
点赞  2012-9-26 15:29

回复 14楼 qq626926200 的帖子

我提个醒。
默认为int。
但是int 有时表现为 unsigned int 有时表现为 signed int

另外,楼主说的 0xffffffff当成-1故而小于6000,这话的确一点道理都没有,。
强者为尊,弱者,死无葬身之地
点赞  2012-9-27 23:37
电子工程世界版权所有 京B2-20211791 京ICP备10001474号-1 京公网安备 11010802033920号
    写回复