编码规范——NASA篇

瞻仰一下,全是干货。原文件链接:<The Power of Ten - Rules for Developing Safety Critical Code>


                                      编写安全严谨代码的十条规则的力量

                                                    NASA/JPL 软件可靠实验室  Gerard J. Holzmann

大部分严格的软件开发项目都遵循着一套编码标准。这些编码标准指的是编写软件的基本准则:这些准则应该怎样组织,哪种语言该用,哪种语言不该用。好奇的是对于什么才是一套好的编码标准目前几乎没有共识。在之前写的很多编码文档中除了一个比一个规模更大之外,并没有找到一个很好的模型可供参考。所以就导致了现存的编码规则有上百条,并且有时带有一些可疑的理由。有一些规则,尤其是那些去规定程序中的空格使用的规则,他们通常带有个人风格。其他的则是在同一组织早期编码工作中为了防止非常具体且不太可能发生的错误而定的规则。不足为奇的是,在开发者实际编写代码时那些现存的编码规范往往对他们没什么作用。令人沮丧的是很多编码规则都不允许基于工具的全面合规性检查。工具校验真的很重要,因为给大型工程中的成百上千的代码进行手工检查时不太可行的。

因此现存的编码规则作用很小,即使关键的应用。然而一些可验证的精选的编码规则能使重要的部分软件能够更彻底的分析,这就是遵循本身编码规则的超越属性。然而为了有效,这些设定的规则必须精简而且清晰以便于理解和记住。这些编码规则必须具体到能够机械的被检查到。为了推出一个在规则数量上有上限的编码规范,我敢说限制不超过十条规则将给我们带来巨大的益处。当然,这样的一个小规则并不能面面俱到,但是它最起码可以给我们一个立足点去获得在软件的可靠性和可验证性上可衡量的作用。为了支持强有力的检查,这些规则有点严格,甚至有人会说太苛刻。不过这些取舍应该是明了的。在紧要关头,特别是在关键安全代码的开发中,可能值得去付出更多额外的努力而且要限定在比理想更加严格的范围内。回过头来,我们应该能够更令人信服的证明关键的软件将按照预期运行。

安全严格编码的十条规则

一个安全严格代码的语言选择本身就是一个关键因素,但是我们这里不讨论太多。在很多组织,包括JPL,严格代码都是用C写的。因为C的已经有段历史了,先进有很多支持这中语言的辅助工具,包括强大的源代码分析,逻辑模型提取,指标工具,仿真器,测试工具和成熟稳定的编译器。因此,大多数已制定的编码规范都是围绕C来写的。然而为了实用性,我们的编码规则侧重于C并且尝试优化我们彻底检查用C的关键代码可靠性的能力。

下面这些规则可能会有些用处,尤其是在少数开发者已经习惯遵守这些规则的情况下。下面每条规则的遵守都带有一个简短的理由,即他们的具体内容。

1.限制所有代码为非常精简的控制流结构--不要用goto语句,setjmp或者longjmp(汇编)结构,和一些直接间接的递归。

更简单的控制流能够转化为更强大的的验证代码的能力而且往往能提高代码的清晰度。在这里递归的滥用或许是最大的惊吓。虽然没有递归,但是我们能保证有一个非循环的函数调用图,他可以被代码分析工具利用,而且直接有助于我们证明所有应该有限制的执行程序实际运行时有上界。(注意:这条规则不要求所有的函数都有一个单一的结束点---即使这些东西经常可以简化控制流。不过有足够多的案例说明,一个早期的错误值返回是一个更简单的解决方法)

2.所有循环必须有有一个确定的上限值。必须可以被一个检查工具静态证明存在一个预置的循环执行的上界次数。如果这个循环界限不能被静态证实,那么可以认为违背该原则。

递归的去除和循环边界的出现预防了代码失控。当然,这条规则不允许无止境的迭代。(例如,在一个进程调度中)在那些特殊的案例中,规则运行被推翻:应该是可以被静态证实的无止境迭代。支持这条规则的一个方法就是去给所有有可变迭代次数的循环添加一个明确的上界(例如,遍历链表的代码)。当上界被超过,一个失败断言被触发,而且函数里的失败迭代返回一个错误(请看规则五,关于断言的使用)

3.在初始化后不要使用动态内存分配。

这条规则在安全严格软件中很普通,而且出现在很多编码规范中。一个简单的理由:内存申请,比如malloc和内存回收经常不可预测的严重影响性能的行为。一个显著的编码错误类型也源自与内存申请和释放的错误利用:忘记去释放内存或者是在释放内村之后继续用内存,尝试申请超过硬件限制的更多内存,超过申请内存的边界等等。强制所有的应用程序在一个固定的,预分配的内存区域内可以消除很多问题而且使代码中内存的使用更容易被验证。注意:在堆中不能申请动态内存情况下只有一个方法,那就是用栈内存。在递归不能使用时,一个堆栈内存使用的上界可以静态导出,因此才有可能去证明一个应用程序将始终在他的预置内存之内。

4.如果一个语句一行、一个声明一行的标准格式来参考,那么没有一个函数的长度应该超过一张A4纸。通常这意味着每个函数的代码行不能超过 60行。

每一个函数应该只能是一个逻辑单元,一个可理解可验证的逻辑单元。横跨多个电脑屏幕或者打印出来好几页的逻辑单元很难被理解。函数过长通常是代码结构很烂的标志。

5.代码中断言的密度应该至少平均每个函数 2 个断言。断言被用于检测那些在实际执行中不可能发生的情况。断言必须没有副作用,并应该定义为布尔测试。当一个断言失败时(程序执行异常),应该执行一个明确的恢复动作,例如,把错误情况返回给执行该断言失败的函数调用者。对于静态工具来说,任何能被静态工具证实其永远不会失败或永远不能触发的断言违反了该规则(例如,通过增加无用的 assert(true) 语句是不可能满足这个规则的)。

工业编码工作的统计数据指出单元测试经常在写好的代码中至少每10到100行代码中发现一个缺陷。拦截缺陷的概率随着断言密度增加而增加。断言的利用也经常被推荐为强防御性编码策略的一部分。断言可以被用来验证函数运行前后的情况,参数值、函数返回值、和循环稳定性。因为断言是无副作用的,在关键代码测试之后可以选择关闭他们。

下面是断言的典型应用:

if (!c_assert(p >= 0) == true{
    return ERROR;
}

断言的定义如下:

#define c_assert(e) ((e) ? (true) : \
  tst_debugging(”%s,%d: assertion ’%s’ failed\n”, \ 
 __FILE__, __LINE__, #e), false) 
 

在这个宏定义中,__FILE__和__LINE__是宏预处理器预定义的失败断言的文件名和行号。#e是把断言情况e转变成为打印部分错误信息的字符串。在嵌入式处理器的代码中当然没有地方打印错误信息,在那种情况下tst_debugging的调用将会变为空操作,并且断言会变成一个可以从异常行为中恢复错误的纯布尔测试。

6.必须在最小的范围内声明数据对象。

这条规则支持一个数据隐藏的基本原则。很明显如果一个数据对象不在范围内,他的值肯定不会被应用和破坏。类似的,如果一个对象的错误的值必须被检测出,可以赋值的语句数越少,问题越容易被诊断出。这种规则不鼓励为多种不兼容的目的重用变量,这可能导致故障诊断复杂化。

7.非 void 函数的返回值在每次函数调用时都必须检查,且在每个函数检查输入参数的有效性。

这可能是最常被违反的规则,因此一般原则看来更可疑。在他严格级别最高的形式里,这个规则意味着每一个printf语句返回值和文件关闭语句返回值必须被校验。但是人们可以说,如果返回一个错误和成功并没什么不同,那么对返回值的检验就没有一点意义。这就经常在调用Printf和close语句中出现。在类似这样的例子中,显式的返回空值是可以接受的,从而表示码农是确定的不是粗心忽视了返回值。在更多的一些莫能两可的例子中,应该有一个注释来阐述为什么返回值是无所谓的。但是在大多数例子中,函数的返回值不应该被忽视,尤其是如果错误返回值必须在函数调用链上传播时。标准库显然违反了这条规则,存在潜在的严重后果。举个例子,如果你偶尔用标准C字符串库执行了strlen(0)或者 strcat(s1, s2, -1) ,会不好。通过遵循通用规则,我们确保在机器检查员标记出违规行为取得情况下例外也是合乎情理的。通常情况下,遵循规则比解释为什么不遵循更容易接受。

8.预处理器的使用仅限制于头文件的包含和简单的宏定义。连接符、可变参数列表(省略号)和递归宏调用都是不允许的。所有的宏必须能够扩展为完整的语法单元。条件编译指令的使用也是不怎么建议的,但也不总是能够避免。这意味着即使在一个大的软件开发中不应该有理由超过一两个条件编译指令,这超出了避免多次包含头文件的标准做法。每次在代码中这样做的时候必须有基于工具的检查器进行标记,并能在代码中要合理。

C的预处理器是一个很模糊的工具,他可以摧毁代码简洁性和迷惑很多基于文本的检验器。即使手上有正规的语言定义,在不严格的预处理代码下的构建也会非常难理解。在C预处理器的一个新实现中,开发者不得不去寻求用更早期的执行来作为C语言中解释编译的仲裁。反对条件编译的理由也同等重要。注意,仅使用10个条件编译指令,就可能会有2的10次方个代码版本,每个版本都必须要被测试,就会造成所需测试工作的急剧上升。

9.应该限制指针的使用。特别是不应该有超过一级的解除指针引用。解除指针引用操作不可以隐含在宏定义或类型声明中。还有,不允许使用函数指针。

指针很容易被滥用,即使是很有经验的码农。他们会让代码很难去追踪或者分析程序中的数据流,尤其是静态代码分析工具。同样的,函数指针会严重限制被静态分析工具检查的类型,而且应该只能在有很充分发的理由的情况下才能用它,理想的情况下应该想出一个方法帮助基本检查工具确定控制流和函数调用层。举个例子,如果用了函数指针,检验工具就不可能证实递归不存在,因此必须提供备用保证来弥补这种分析能力的缺失。

10.从开发的第一天起,必须在编译器迂腐的设置中开启所有警告选项的条件下对代码进行编译。在此设置之下,代码必须零警告编译通过。代码必须利用源代码静态分析工具每天至少检查一次或更多次,且零警告通过。

现在市场上有一些很高效的静态源代码分析工具,也有小部分免费代码分析工具。任何软件开发工作都没有理由不使用这些现成的技术。他应该被当作常规实践,甚至是非关键代码的开发。即使编译器或者静态分析工具给出了错误的警告的情况下零警告的规则也适用:如果编译器和静态分析工具混乱了,造成混乱的代码也应该重写使其变得更有效。很多开发者想当然警告是无效的,但过了很久才意识到警告信息是有效的,有效的原因不那么明显。静态分析工具名声不太好是因为早期的工具,比如lint,因为他会产生很多无效信息,但是现在情况已经不同了。现在最好的静态分析工具是很快的,而且可以产生可选的精准的信息。他们的使用在一些软件项目中不应该协商。


       前两条规则保证创建一个清晰简洁的控制流结构,该结构更容易去构建,测试,分析。不用动态内存申请,由第三条规则规定,去除了一些关于内存申请,释放和游离指针等等一系列问题。第四条到第七条规则是能被广泛接受的好代码的标准。促进严格安全系统的其他编码风格的好处,比如“契约式设计”可以在第五条到第七条中找到。

       这十条规则在喷气推进实验室的关键任务软件编写中得到了实验应用,并取得令人鼓舞的结果。在克服了在这种严格限制下最初的不情愿之后,开发者经常发现遵循这些规则往往有利于代码的简洁性,可分析性和代码安全性。这些规则通过其他方式减轻了开发者和测试者构建代码关键属性(例如中止或者限制,内存和堆栈的安全使用等等)的负担。如果这些规则第一次看起来很严格,请记住他们的目的是使检查代码成为可能而你的生命就完全依赖于他的合理性:用来控制你做的飞机的代码,离你住的地方只有几公里的核电站,或者搭载宇航员进入轨道的宇航飞船。这些规则就像你车上的安全带:才开始你可能局的一点不舒服。但是之后遵循这些规则会成为你的第二天性并且难以想象不遵循这些规则。

 

骗阅览量链接:

编码规范——华为篇

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值