NASA 编写安全关键程序的 10 条编码规则
大型复杂的软件项目使用各种编码标准和指南。这些指南建立了编写软件时必须遵循的基本规则。通常,他们确定:
a) 代码应该如何组织?
b) 应该或不应该使用哪种语言功能?
为了有效,规则集必须很小,并且必须足够具体,以便于理解和记住。
在 NASA 工作的世界顶级程序员遵循一套开发安全关键代码的准则。事实上,包括 NASA 喷气推进实验室 (JPL) 在内的许多机构都专注于用 C 编程语言编写的代码。这是因为该语言有广泛的工具支持,例如逻辑模型提取器、调试器、稳定的编译器、强大的源代码分析器和度量工具。
在危急情况下,有必要应用这些规则,尤其是在人的生命可能取决于其正确性和效率的情况下。例如,用于控制飞机、航天器或核电站的软件程序。
但是你知道航天机构使用什么标准来操作他们的机器吗?下面,我们列出了 JPL 首席科学家 Gerard J. Holzmann 制定的 NASA 10 条编码规则。它们都主要关注安全参数,您也可以将它们应用于其他编程语言。
规则 1 – 简单的控制流程
用非常简单的控制流结构编写程序——不要使用 setjmp 或 longjmp 构造,转到 语句,以及直接或间接递归 .
原因: 简单的控制流可以提高代码的清晰度和更强的验证能力。没有递归,就不会有循环函数调用图。因此,所有应该有界的执行实际上仍然是有界的。
规则 2 – 固定循环的上限
所有循环都必须有一个固定的上限。验证工具应该可以静态证明循环迭代次数不能超过预设上限。
如果不能静态证明循环边界,则认为违反了规则。
原因: 循环边界的存在和递归的不存在可以防止代码失控。但是,该规则不适用于旨在非终止的迭代(例如,流程调度程序)。在这种情况下,应用反向规则——迭代不能终止必须是静态可证明的。
规则 3 – 无动态内存分配
初始化后不要使用动态内存分配。
原因: 内存分配器,如 malloc 和垃圾收集器通常具有不可预测的行为,可能会对性能产生异常影响。此外,由于程序员的错误,也可能发生内存错误,其中包括
- 尝试分配比物理可用内存更多的内存
- 忘记释放内存
- 释放内存后继续使用
- 超出分配内存的界限
强制所有模块都位于一个固定的、预先分配的存储区域内可以消除这些问题,并且更容易验证内存使用情况。
在没有从堆分配内存的情况下动态声明内存的一种方法是使用堆栈内存。
规则 4 – 没有大函数
任何函数的长度都不应超过以标准参考格式打印在一张纸上的长度,每个声明一行,每个语句一行。这意味着函数不应超过 60 行代码。
原因: 过长的函数通常是结构不良的标志。每个功能都应该是一个可理解和可验证的逻辑单元。理解跨越计算机显示器上多个屏幕的逻辑单元相当困难。
第 5 条规则——低断言密度
程序的断言密度应该平均为每个函数至少两个断言。断言用于检查在现实生活中永远不会发生的异常情况。它们应该被定义为布尔测试。当断言失败时,应采取显式恢复操作。
如果静态检查工具证明断言永远不会失败或永远不会成立,则视为违反了规则。
原因: 根据行业编码工作量统计,单元测试每 10 到 100 行代码至少捕获一个缺陷。拦截缺陷的机会随着断言密度的增加而增加。
断言的使用也很重要,因为它们是强大的防御性编码策略的一部分。它们用于验证函数的前后条件、参数、函数的返回值和循环不变量。测试性能关键代码后,可以有选择地禁用断言。
规则 6 – 在最小范围级别声明数据对象
这个支持数据隐藏的基本原理。所有数据对象都必须在尽可能小的范围级别上声明。
原因: 如果对象不在范围内,则无法引用或损坏其值。此规则不鼓励将变量重用于多个不兼容的目的,这可能会使故障诊断复杂化。
阅读:有史以来最伟大的 20 位计算机程序员
规则 7 – 检查参数和返回值
每个调用函数都要检查非空函数的返回值,每个函数内部都要检查参数的有效性。
在最严格的形式中,此规则甚至意味着 printf 的返回值 声明和文件关闭 应该检查语句。
原因: 如果对错误的响应与对成功的响应完全相同,则应明确检查返回值。这通常是调用 close 的情况 和printf .将函数返回值显式转换为 void – 是可以接受的 表明编码器明确(并非偶然)决定忽略返回值。
规则 8 – 预处理器的限制使用
预处理器的使用应限于包含头文件和宏定义。不允许递归宏调用、标记粘贴和变量参数列表。
即使在大型应用程序开发工作中,也应该有理由使用一两个以上的条件编译指令,超出标准样板,避免多次包含同一头文件。每次此类使用都必须由基于工具的检查器标记并在代码中证明其合理性。
原因: C 预处理器是一种功能强大且模棱两可的工具,它会破坏代码的清晰度并混淆许多基于文本的检查器。即使手头有正式的语言定义,无界预处理器代码中构造的影响也可能异常难以解读。
对条件编译的警告同样重要——只有 10 个条件编译指令,代码可能有 1024 个可能的版本 (2^10),这会增加所需的测试工作。
阅读:今年要学习的 9 种新编程语言
规则 9 – 限制使用指针
必须限制指针的使用。不允许超过一个级别的取消引用。指针取消引用操作不应隐藏在 typedef 中 声明或宏定义。
也不允许使用函数指针。
原因: 指针很容易被误用,即使是专家也是如此。它们使得跟踪或分析程序中的数据流变得困难,尤其是通过基于工具的静态分析器。
函数指针还限制了静态分析器执行的检查类型。因此,只有在有充分理由实施它们时才应使用它们。如果使用函数指针,工具几乎不可能证明不存在递归,因此应该提供替代方法来弥补这种分析能力的损失。
阅读:14 种编写代码的最佳编程软件
规则 10 – 编译所有代码
所有代码必须从开发的第一天开始编译。编译器警告必须在编译器最细致的设置下启用。代码必须在没有任何警告的情况下使用这些设置进行编译。
所有代码都应该每天使用至少一个(最好多于一个)最先进的静态源代码分析器进行检查,并且应该以零警告通过分析过程。
原因 :市场上有很多有效的源代码分析器;其中一些是免费软件工具。任何编码人员都绝对没有理由不使用这种现成的技术。如果编译器或静态分析器混淆,则应重写导致混淆/错误的代码,使其变得更加简单有效。
阅读:我们在日常生活中使用的 30 项令人惊叹的 NASA 发明
美国宇航局对这些规则有何看法?
工业技术