本
文
摘
要
一般来说,大多数严肃的软件开发项目都会遵循一定的编写规范,这些规范旨在说明编写软件的基本规则:软件的结构以及语言特性的使用范围。但另一方面,关于什么是好的代码准则却几乎没有共识,现有的一些代码准则往往缺乏重点,结果就是新的准则文档只会越编越长。大多数现有准则都包含了不下一百多条内容,有些内容甚至令人怀疑文档编写的原则,例如一些只是来源于个人喜好的条目:试图规定程序中空格的使用规则;还有的条目则关注于一些早期编码工作中遇到的十分特殊或不必要关心的错误。基于以上现状,现有的这些代码准则对一般开发人员其实并无帮助。许多代码准则中最大地缺陷还在于很少允许进行基于工具的全面合规性检查,而基于工具的检查却又十分重要,因为现实中往往会遇到需要手动检查成千上万行的大型应用程序代码的情况。
对于重要的程序而言,现有代码准则都无法提供有效的建议。而一套 *** 的代码准则必须能够对关键软件组件进行更彻底的分析,甚至超越准则本身规定的内容。因此,一套有效的代码准则必须足够简洁,以便可以被理解和记忆,又必须足够具体,可以用作直接检查。如果要为这样一套代码准则定一个数量上限的话,我认为不超过十条是一个比较适合的数量,这个数量虽然不够全面,但能提供一个软件可靠性和可验证性检查的有效原则。为了保证高标准的检查,这套规则可以说有点严格——甚至可以说严厉。但是权衡利弊地来考虑,在开发安全的关键代码时,遵守这些严格准则付出的努力应当是值得的。基于此,我们将能够有说服力地证明编写的关键性代码会按照预期执行。
编写安全关键代码的十条准则
安全关键代码的语言选择本身是一个重要的考虑因素,但这里不对此进行太多讨论。在许多机构(包括JPL)中,关键代码一般都使用C语言编写。由于C语言拥有悠久的历史,因此该语言也有着广泛的工具支持,包括强大的代码分析器,逻辑模型提取器,度量工具,调试器,测试工具,以及各种成熟稳定的编译器。因此,C也是现有大多数代码准则的描述对象。从务实的角度出发,我们的代码准则也主要针对C,并尝试在C代码编写中进行更彻底的可靠性检查。
以下准则应当都能为编码带来好处,特别是编号靠前的前几条更加应该重视。每条准则都已附上提出的理由。
准则1:使用简洁的控制流程。
请勿使用goto语句,setjmp或longjmp结构以及直接或间接的递归。
解释:简洁的控制流程可读性更好,因此可验证性也更好。本条规则中递归的禁用也许是最大的疑问:原因在于没有递归,我们可以保证获得非循环的函数调用图,可以更好地被代码分析器解析,并且更容易证明那些应该限制的执行确实都有边界。(注意,该规则并不是要求所有函数都只具有单一的返回点——尽管这样能简化控制流程。但很多时候在函数前部进行检查并返回错误是更简单的解决方案。)
准则2:所有循环次数必须具有确定的上限。
对于检查工具必须能使其静态地证明预设的迭代次数上限不会被超越。如果无法静态地证明循环上限,则该视为违反了本条准则。
解释:避免递归和预设循环界限可以防止代码失控。当然,本条准则不适用于那些本身不终止运行的程序(如进程调度程序)。这种情况下,应当使用逆向的解释:代码应该能被静态地证明迭代不能终止。一种适配本条准则的方法是为所有可变的迭代次数显式地设置上限(例如用于遍历链表的代码中)。当迭代次数超出限制,将触发断言(assert)失败,函数将返回错误。(有关assert的使用,请参阅准则5。)
准则3:初始化后不再使用动态内存分配。
解释:这条准则对于安全关键代码是普遍适用的,而且在现有的代码规范里面一般也很常见。原因很简单:内存分配器,例如malloc和内存回收程序往往会产生不可预测的行为,并严重影响性能。一些值得引起注意的典型编码错误也都来源于对内存的分配和释放过程:比如忘记释放内存或继续使用已释放的内存、尝试分配超出可用物理地址的内存、访问未分配的内存地址等。因此强制所有应用程序位于固定的,预先分配完成的内存区域中,可以避免许多此类内存问题,并且使程序易于排查内存使用情况。注意,未分配堆内存的情况下动态声明内存的唯一方法是使用栈内存。在没有递归的情况下(基于准则1),栈内存的使用上限可以被静态推导,因此可以证明应用程序将始终存在于预分配的内存中。
准则4:避免单个函数过于冗长。
单个函数功能描述不得超过一张纸(基于标准的格式,每行代码对应一行描述)。这就意味着每个函数不超过约60行代码。
解释:每个函数应当是是代码中可以理解的一个逻辑单元,并且可以作为一个整体进行验证。一个长度跨越几个屏幕的逻辑单元将会非常难以理解和阅读。过长的函数通常意味着代码结构不够优化。
准则5:保证断言(assert)使用数量。
应当保证平均每个函数至少使用了2次以上断言。断言一般用于检查一些正常运行中绝不允许出现的异常情况,且必须是无副作用的,应定义为布尔测试。当断言失败时,必须有相应的显式恢复措施,例如将错误信息返回给调用者。同时,应当避免出现可被静态证明永远为真或假的断言条件(例如不要添加无意义的“ assert(true) ”语句)。
解释:业内统计数据表明,单元测试通在每10~100行代码中发现至少一个缺陷。增加断言的使用也可以提高缺陷被拦截的几率,因此通常也建议多使用断言增强代码的防御性。断言可用于验证函数出入口处的判断条件,参数值,返回值和循环次数。 由于断言没有副作用,因此可以在对性能敏感的代码进行测试后去除。
断言的典型用法如下:
if (!c_assert(p >= 0) == true) { return ERROR; }断言条件c_assert定义如下:
#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语句和文件close语句的返回值也必须进行检查。但是如果程序对错误值的响应与正确值无异,显式的返回值检查也是没有意义的。调用printf和close通常会面临这种情况,此时可以将函数返回值显式转换为(void)——从而提示程序员明确地忽略返回值。在没有把握的情况下,应添加注释解释为什么此处的返回值是无关紧要的。不过在大多数情况下,不应忽略函数的返回值,特别是当错误返回值会在函数调用链中传递时。然而,标准库却带头违反了该规则,并因此可能在使用中造成严重后果。例如,可以预见错误地执行了C标准库字符串函数strlen(0)或strcat(s1,s2,-1)会发生什么,显然后果是不愉快的。通过遵守这样的准则,我们能够确保一定能够分辨出此类异常,并使用检查器标记。通常,遵守规则比解释为什么合理地不遵守规则更容易。
准则8:预处理器仅用于包含头文件和简单的宏定义。
避免使用连接符号,可变参数列表和递归宏调用,所有宏都必须扩展为完整的语句单位。条件编译指令的使用通常也存在风险,但这方面不能完全避免,因为除了惯常的用于避免重复引用,在大型软件开发中,一般也需要多个条件编译指令。每种此类使用都应使用工具进行标记,并且在代码中证明是正确的。
解释:C预处理器具有强大的混淆能力,可以破坏代码清晰度和混淆许多基于文本的检查器。即使有正式的语言定义,预处理阶段的结构由于不受限制也可能会变得极难翻译。在新版C预处理程序中,开发人员经常不得不使用较早的版本来处理早期C标准中的复杂定义。此外,防范编译条件同样重要,试想只有存在十个条件编译指令,就可以产生210种代码版本的可能性,若进行版本测试将增加大量的测试工作量。
准则9:限制指针的使用。
其中,只允许进行不超过1层的解引用,但禁止将指针的解引用操作隐藏在宏定义种或typedef声明的内部,也禁止使用函数指针。
解释:即使是经验丰富的程序员也容易存在滥用指针的习惯。指针能使得程序中的数据流很难跟踪或分析,尤其是对于静态分析工具来说。同样,函数指针也会严重限制静态分析工具的检查,除非有充分的使用理由,并提供理想的替代方法来协助检查工具确认控制流和函数调用层次结构。例如,当使用函数指针时,检查工具将无法证明递归调用不存在,因此必须提供额外的保证以弥补这方面检查的缺失。
准则10:所有代码必须进行编译.
从开始开发的第一天起,就启用编译器最严格的编译检查。所有代码必须在这种检查设置下进行编译,并解决编译器提出的所有警告。所有代码必须每天编译检查,并使用多种最新版的静态源代码分析器分析,并以零警告通过分析。
解释:当今市场上有一些非常有效的静态源代码分析器,同时相当一部分是免费软件(参见http://spinroot.com/static/index.html)。所以没有借口在软件开发中逃避使用这些有用的工具,即使是非关键代码的开发,也应该将其作为惯常的开发手段。零警告标准也适用于编译器或静态分析工具发出警告提示:编译器或静态分析工具出现混淆时,应将引起混淆的代码重写,使其变得更加简单有效。许多开发人员认为警告信息可以直接忽略,然而后来在某些不明显的错误位置才意识到警告信息的重要性。早期静态分析工具由于效果较差而名声不佳,如lint之类的工具通常产生了很多无用信息。但这种情况并没有持续很长时间,如今最好的静态分析工具速度已经很快并且可以准确地进行判断,因此在任何关键的软件开发上都不应该质疑使用它们的必要性。
小结
前两条准则用于确保创建清晰透明的控制流结构,这样更易于构建,测试和分析。第三条准则规定避免使用动态内存分配,消除了与内存分配释放和野指针有关的一类问题。接下来的几条准则(4~7)则被广泛认为是良好编码风格的标准。其他一些好的代码风格也已用于改进安全关键系统,例如“契约式设计”可以在准则5~7中找到一部分。
JPL在编写关键软件任务时正在实验性地使用这十条严格的准则,效果十分令人鼓舞。在克服初期正常的一些不情愿之后,开发人员发现遵守这些准则确实有助于提高代码的可读性,可分析性和安全性。这些准则同时也减轻了开发人员和测试人员在提升程序关键性能上的负担(例如可终止或有界性,安全使用内存和栈等)。虽然这些准则起初看起来比较严厉,请意识到这是为了检查那些关系民生的代码:用于控制您所乘坐的飞机,离您住处几英里的核电站,或者将宇航员送入轨道的航天器。这些规则就像您汽车上的安全带一样:最初它们可能有点不舒服,但是使用一段时间后,这些习惯将成为后天习得的本能,不使用反而可能会觉得不舒服。
版权声明:本文为迪捷软件原创,转载请联系授权并注明来源。
联系电话:010-56131268,0575-88361699;13501153049、13260299730 (微信同号)
联系邮箱:contact@digiproto.com
工作地址:北京市海淀区中关村软件园
浙江省绍兴市越城区中关村•水木湾区科学园3#802
公司简介:浙江迪捷软件科技有限公司2013年成立于北京,专注于安全关键领域数字化转型,提供军工领域的MBSE产品和解决方案,遵循中立开放的商业理念,为我国防务等安全关键领域提供MBSE和数字装备等解决方案。我司软件产品全部为自主研发,具有核心知识产权,涉及了高端装备的设计、研发和测试等环节。公司注册资金为1000万,总部位于浙江省绍兴市,在北京、上海等地设有分公司。