首页
安全关键代码十大编写规则
Gerard J. Holzmann
NASA/JPL Laboratory for Reliable Software
 
      大多数关键软件开发项目都在使用编码准则,这些准则的目的是定义一些编写软件的基本规则:该如何设计软件的架构,哪些语言特征可以使用,哪些语言特征不能使用。奇怪的是,人们对于良好的编码标准几乎没有任何共识。

      面对众多面世的编码准则,除了新的准则比旧的更长之外,几乎不存在能令人刮目相看的典范。结果,大多数现存的准则都包含100多条规则,甚至有些规则还值得我们怀疑。其中的一些规则,特别是那些定义了程序中空格用法的规则,都可以解释为个人的偏好;其他的规则可以理解为,为了避免同一组织在编码初期产生非常特别、并且不太可能产生的错误。

      不足为奇的是,现有的编码准则在开发人员实际编码的时候往往没有多大用处。这些规则最失败的地方在于,他们很少考虑到大多数基于工具的一致性检查。基于工具的检查是很重要的,因为手动审查几十万行的代码往往是不可行的。

      因此,即使对于关键应用程序,现有的编码准则不能给我们带来多大的益处。然而,可核查的精选编码规则的集合能够分析关键软件的组件属性,这些属性在遵守这些规则的集合之外能很好的运行。可是,为了达到效果,这个规则的集合必须很小,并且非常清楚,使得使用者能够方便的理解和记住它。此外,这个集合必须非常特别,使得使用者能够全面地、机械地检测他们。

      为了给规则的数量加一个上限,我认为集合中规则的数量最多不超过10条会提供一个有效的方针。尽管这样的小集合不能完全包含所有的规则,但是它能够对软件的可靠性和可证实性达到测量的效果。

      为了支持强大的检查功能,我建议这些规则应该稍微严格些,有一些规则甚至可能说要严峻一些。然而,将这些规则折衷应该更清晰。当折衷真正出现的时候,尤其是在安全代码的开发阶段,严格限制的工作需要更加努力。它能更方便的展示关键代码将按照预期效果运行。

安全关键编码的规则

      对安全关键代码来说,语言的选择本质是一个很关键的考虑。在很多公司里,包括JPL在内,开发人员用C编写大部分代码。C具有很长的历史,很多工具都支持这种语言。这些工具包括:强大的源代码分析器,逻辑模型分离器,metrics tool,调试工具,测试工具和成熟,稳定的编译器。由于这些原因,C语言也是多数现有代码编写目标。由于非常注重效率的原因,随后的10条原则主要针对C并且优化工具的性能,使得能够更全面的检测用C编写的应用程序的可靠性。

      这些规则被证实是有益的,尤其是小数量的规则的情况下,就意味着开发人员确实应该坚持使用这些规则了。
  • 规则1 :把所有的代码限制成非常简单的控制流结构——不要使用goto语句,setjmp或longjmp结构,或者直接或间接的递归调用。
    • 基本原理:简单的控制流转化成更加强大的分析性能,并且改进了代码的清晰度。清除递归也许是一个最大的惊喜。为了避免递归产生一个非循环功能调用图表,译器能够使用这个图表去证明对堆栈的限制使用和对有界性的执行。尽管返回值能够简化控制流,值得注意的是这个规则并不要求所有的功能具有一个简单的返回值。可是在某种情况下,一个早期的错误返回的是更简化的解决方案。
  • 规则2 :所有的循环设置一个固定的最大值。对于检查工具来说,通过静态分析检查循环中迭代次数不会超出预设的循环上限是不可能的。如果一个工具不能静态的检测出循环的界限,那么这条规则就被认为是无效的。
    • 基本原理:没有递归和具有循环边界阻止了代码失控。当然了这条规则并不适用于迭代,迭代的意味着无穷无尽,例如,在进程的调度程序中。在这些特殊的案例中,应用的相反的规则:对于一个检测工具来说应该可能静态的检测到迭代并未终止。遵守这条规则的一种方法就是:给所有具有大量迭代的循环添加一个外在最高界限,例如,具有一个连接列表的代码。当循环超出了最高的界限,必须出发一个断言,并且包含错误迭代的函数应该返回一个错误。
  • 规则3:在初始化之后不要使用动态的内存分配。
    • 基本原理:这条规则在大多数安全关键软件的编码方案中出现。原因非常简单:内存模块分配和碎片收集等内存分配常伴随一个不可预知的行为,这个行为能严重地影响性能。一个编码错误的类型也会阻止内存分配和程序的不正确的运行,这是因为在内存被释放后获得自由内存或者继续使用内存,试图分配的内存空间比实际物理上获得内存空间要多,超出了内存分配的边界。强制所有的应用程序在一个固定的、预先分配好的内存空间里留存,这样可以消除大部分此类问题,而且验证内存使用也更为容易。值得注意的是:对于从堆中没有内存分配动态内存获取的唯一方法就是使用栈的内存。在没有递归的情况下,使用堆栈内存的最高界限能够被静态的继承,因此,这样就很容易证明一个应用程序要永驻其资源范围之中。
  • 规则4:如果函数用一行一条语句和一行一条声明标准格式来写,那么函数长度不用该长于一张纸。这就意味着每个函数不会超过60行代码。
    • 基本原理:每个函数是代码中的一个逻辑单元。代码作为一个单元是可以理解和证实的。理解一个含有多页的单元是很困难的,并且过度冗长的函数往往是代码框架不合理的标志。
  • 规则5:代码断言最密度最低限度应该平均到每个函数两个断言。断言用于检测在实际执行中不可能发生不规则条件。断言没有不良后果并且会被定义成布尔测试。当断言失败,必须采取外在的恢复行为,例如为执行失败断言的函数调用恢复错误的条件。任何断言对于静态检测工具来说都会被证实为断言从来不会失败或者违反这条规则。
    • 基本原理:对于工业编码的效果,统计学表明单元测试中,每10行到100行至少能发现1个缺陷。增长断言密度可以阻止缺陷的个数的明显增长。断言的使用被认为是强壮的编码保护策略的一部分。开发人员能够使用断言去验证函数的条件,参数值,函数的返回值和循环变量。因为事先设计的断言是没有不良后果的,在关键代码的性能测试之后,断言可以选择性的是失效。
  • 规则6:在最小的范围中声明所有的数据对象。
    • 基本原理:这条规则支持数据隐藏的基本原理。如果一个对象并不在其范围内,那么其他的模块不能引用或者销毁这个值。同样的道理,如果测试人员必须检查对象的错误值,那么这个值的赋值语句越少就越容易发现错误。这条规则防止变量的重复使用,因为变量多次使用和使用不一致能够使检查错误变得复杂化。
  • 规则7:每次函数调用必须检查函数的返回值,并且每个被调用的函数必须检查函参传递的有效性。
    • 基本原理:这条规则可能是所有被违反的规则中最常用的规则,因此将此作为一条普遍性的规则值得斟酌。在最严格的检测模式里,这条规则必须检查printf和file close语句的返回值。若果一个错误的返回值与正确的返回值没有任何不同的话,那么就没有必要精确的检查返回值。这里以printf和close为例。类似这样的例子中,函数返回值(甚至void)列表能够被接收,因此表明程序员需要准确并且不是偶然才忽略一个返回值。在更多不确定的程序中,需要写一条注释解释为什么一个返回值能够被认为是不相关的。因此,在大部分程序中不能忽略函数的返回值,尤其是函数将一个错误的返回值带到整个函数调用链中去。
  • 规则8:预处理器的使用应该限制在头文件和简单的宏定义中。不允许使用连接符,可变参数列表(省略号) ,以及递归宏调用。所有宏必须扩展到完整的句法单位。尽量不要使用条件汇编指令。理由:C语言预处理器是一个功能强大的工具,可以破坏代码的清晰度以及迷惑许多基于文本的检查器。
    • 基本原理:C预处理器是非常强大且难懂的工具,它能破坏代码的清晰并且使检查者越来越迷糊。在不被限制的预处理器代码中,构架的效果是代码(甚至是用官方语言定义的代码)很难理解。在C预处理器新的运行模式中,开发者必须用C语言的标准定义去解释复杂的定义语言。这对警告与有条件编译相悖的情况是相当重要的。
  • 规则9:必须限制指针的使用。特殊情况下,只有销毁指针可以使用。指针的销毁操作不可以隐藏在宏定义中或者插在类型声明里。不允许使用函数指针。在10个条件汇编指令中,可能有多达2^10种不同版本的代码,其中的每一种都要进行测试,这就引起了大量的测试需求。条件汇编的使用不总是能够避免的,即使在大型软件开发方面,除了标准样板可以避免多次包含同一个头文件之外,很少有理由使用超过一两个这样的汇编指令。基于工具的检查器应该标记代码中的每一次使用而且每一次使用都应该是合理的。
    • 基本原理:指针很容易被误用,很多有经验的程序员也不例外。他们(尤其是使用基础工具的分析者)很难跟踪或者分析程序中的数据流。同样地,只有在理由很充分的情况下才使用函数指针,因为函数指针能够很严格地限制代码检查人员执行自动检查的类型。例如,如果使用函数指针,对于工具来说也不可能证明缺少递归,这就需要更换人员去整理这次检查中的遗漏。
  • 规则10:从开发过程的第一天起,所有的代码编译必须参照编译器可使用的警告。所有的代码编译后必须没有警告。所有的代码必须每天至少检查一次,当然了多于一次和使用强壮的源代码分析软件就更完美了。所有的代码应该以0警告通过所有的分析。
    • 基本原理:当今的市场上有很多相当有效的源代码分析软件,也有很多免费的工具。对任何软件的开发过程来说,没有理由不去使用这个现有的技术。甚至对于关键软件的开发来说,它被认为是日常的训练。
      当编译器或者静态分析器报出一个错误的警告时,即:如果编译器或者分析器得到混淆,那么代码所产生的混淆应该被重写,此时应该运用0警告的规则。很多开发者陷入到假象中,即:警告确定是无效的,为了减少这个错误,只有实现了后边的更多实际有效信息。静态分析器有一些不好的名声,由于早期的版本产生了很多无效信息,但是现在不在是这样的了。如今,最好的静态分析器速度非常快,并且能够给出精确的信息。不应该把静态分析器选择性的用于严重软件工程中。

后续
      来自这个集合的少数规则保证创建一个清晰的,易于构建,测试,和分析的控制流结构。没有动态内存分配消除了与分配相关和内存释放的一类问题,还有野指针的使用问题,等等。接下来的几条规则被看做为良好编码风格的准则。其他的规则保证了更健壮编码风格的优点,这些风格提升了系统的安全,如按照规则设计原则。

      目前,JPL的开发人员使用规则集合编写重要软件,具有很好的效果。开发者克服了不愿意在如此严格界限内的编写健壮初始化程序情绪后,他们发现按照这些规则进行编码确实能保证代码安全。这些规则减轻了开发人员和测试人员的负担。他们通过其他的方式确定主要代码的性能,例如终止或者有界性,和内存和堆栈的安全使用。如果这些规则看起来很严格的话,那么请记住这些规则的意图是能够检查安全重要代码。

      这些规则就如同汽车的安全带:首先,使用他们可能有一些麻烦,但是过一段时间以后就变成一种习惯,并且不使用这些规则就会觉得不可靠。

      这篇文章所描述的研究是在加利福利亚大学的JET实验室中进行的,并且与NASA签订合同。 

      Gerard J. Holzmann是NASA JET实验室的主要的计算机专家,他负责这个实验室软件的可靠性。邮箱是 此邮件地址受阻挡spam的自动程序保护,需要激活Javascript功能才能查阅。 .
 
< 上一篇   下一篇 >