参加本次软件可靠性与安全性高级技术研讨会学习主要的收获是学习了对软件可靠性与安全性设计与实现的方法,将会在以后在软件设计的工作中提供重要的帮助。现将软件可靠性与安全性设计与实现的知识点进行梳理记录。
1. 防错性设计与实现
防错性设计包括:简化设计、算法与数据管理、慎用易错架构、使用监错技术、多任务管理。
1.1. 简化设计
复杂性是可靠性最凶恶的对手,复杂的软件会导致:代码规模更大、缺陷更多;交互关系更多、缺陷更多;更难测试、不充分的可能性更大;设计、实现、配置、使用的难度更大;用户更难理解。
1.1.1. 控制模块的复杂性
1) 单元的理论最佳长度66到132行
2) 清晰定义模块的所有输入输出并进行范围检测(架构设计)
3) 模块有唯一的入口和出口
4) 模块中的循环有正常的退出条件
5) 保持模块的控制流从顶到底
6) 尽量降低模块的圈复杂度(不大于10)
1.1.2. 控制软件的复杂性
1) 强内聚
内聚性指模块相对功能密度的度量,依赖于一个单元中各种操作之间互相联系的紧密程度。
a) 功能内聚
b) 顺序内聚
c) 通讯内聚
d) 过程内聚
e) 不可取:时间内聚,逻辑内聚
2) 松耦合
耦合性指两个模块之间联系的紧密程度,依赖于模块间接口的复杂性、引用或进入模块的点、通过接口传递的数据。
a) 简单数据耦合
b) 数据结构耦合
c) 不可取:控制耦合,公用耦合,内容耦合
3) 扇入扇出
模块的扇出指模块的直属下层模块的个数,模块的扇入指有多少个上级模块调用本模块。
a) 上层模块高扇出
b) 下层模块高扇入
c) 单元调用嵌套层数不大于7
d) 扇入扇出数不大于7
1.2. 算法与数据管理
1.2.1. 算法选择
1.2.2. 数据管理
1) 参数化
在软件设计中,用统一的符号来表示参数、常量和标志, 以便在不改变源程序逻辑的情况下,对它们进行修改。
2) 寻址模式的选用
尽量不使用间接寻址方式,在确实有必要采用间接寻址方式时,慎重考虑和充分论证,并在执行之前验证地址是否在可接受的范围内。
3) 文件
文件必须唯一且用于单一目的;文件在使用前必须成功地打开,在使用结束必须成功地关闭;文件的属性应与对它的使用相一致。
4) 对关键下标, 在使用前进行范围检查
5) 使用数据前, 应采取措施保证所有所需的数据都是可用的
6) 应采取措施, 对关键数据进行保护
7) 对数据的非法组合进行检查
1.3. 慎用易错架构
典型易错架构
浮点数 |
指针 |
递归 |
中断 |
继承 |
别名 |
无界数组 |
动态内存分配 |
全局变量 |
公用数据和公共变量 |
不检查输入参数长度的库函数 |
1) 浮点数
先天不精确,有可能导致无效的比较;增加浮点协处理器和内嵌算法/库函数确认的工作量。
2) 指针
指针引用错误的内存区域可能导致数据误用。
3) 递归
错误的递归容易导致内存溢出;当使用递归时, 应有明显的判据, 可预测递归的深度。
4) 中断
有可能导致关键操作的终止;使程序难以理解,类似 goto 语句;使用时,应仔细考虑寄存器和共享变量的内容、中断优先级、中断发生的时机、中断发生的最大可能频率、中断处理时间等。
5) 继承
代码非局部化,代码的修改可能导致无法预期行为,产生难以理解的问题。
6) 别名
使用多个变量名来访问相同状态变量,会使程序的理解和修改变得困难。
7) 无界数组
如果不进行任何数组边界检查,可能出现缓冲区溢出失效。
8) 动态内存分配
在有些场景下,软件运行时内存块大小不能在代码编译时确定, 需要根据代码的运行环境来确定;软件执行过程中, 根据需要分配或者回收存储空间;在C/C++程序中,应正确使用malloc、calloc、realloc、new、alloca与free、delete管理动态内存。
不当动态内存分配的后果:内存泄露、内存碎片。
内存泄露的原因:忘记了回收;回收前失去了对内存的追踪,(如:存储指针值的变量被移出了作用域、指针值被重写、没有保存地址指针);库函数存在内存泄露缺陷,对库函数接口的误解。
9) 全局变量
全局变量不好控制;不利于程序的结构化;不用或少用全局变量
10) 公用数据和公共变量
公用数据和公共变量指明由两个或多个模块公用的数据和公共变量。尽量减少对公共变量的改变,以减少模块间的副作用。
11) 慎用不检查输入参数长度的库函数
缓冲区溢出漏洞;这些函数直接把输入参数的内容复制到缓冲区中,只要输入参数长度大于缓冲区长度,就会造成缓冲区溢出,使程序运行出错;如strcpy()、strcat()、sprintf()、vsprintf()、gets()、scanf()等
1.4. 使用监错技术
使用条件判断
在开发和维护阶段,使用监错技术提示:相互矛盾的假设、传入程序的不良数值等。
主要的监错技术:
1) 断言
断言是一个在假设不正确时会预警的函数或宏指令,可使用断言监错。在开发阶段,断言可以提示相互矛盾的假设、传入程序的不良数值等等;在维护阶段,断言可以表明改动是否影响到了程序其它部分。
例子:
assert(y != 0);
int z = x / y;
2) 异常情况处理
异常是在运行时发生的,无法在设计时预料到的“非常”事件,这种事件通常和具体的运行环境和资源分配有关。仔细分析软件运行过程中各种可能的异常情况,预先设计相应的保护措施,或者利用异常处理做一些必须的善后工作。在开发阶段,可以利用异常情况处理,产生一个警告,提示出现了异常情况,使异常情况的出现变得非常明显;在运行阶段, 异常处理措施应该能使出现的异常情况可以得到修复。
1.5. 多任务管理
多任务设计是软件应用的新趋势,但是多任务之间可能存在难以预知的交互,导致同步错误。
多任务设计的原则:
1) 注意函数的可重入性
不为连续的调用持有静态数据;不返回指向静态数据的指针,所有数据都由函数的调用者提供;使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据;如果必须访问全局变量,记住利用互斥信号量来保护全局变量;不调用任何不可重入函数。
2) 避免死锁与活锁
当两个或者更多的进程停下来相互等待对方完成某个动作时,就造成了死锁,通常表现为系统挂起。活锁于死锁类似,只是当前系统仍然能够进行一些计算,但永远无法转到其它状态。死锁与活锁的发生常常是因为很难预期和重现的罕见的条件组合。
3) 避免临界点竞争
2. 健壮性设计
软件健壮性(Robustness)指软件系统在遭遇异常的情况下,仍然能够正常且安全运行的能力。主要关注外部异常。
异常情况举例:相连的软硬件系统发生了故障或性能降级、输入错误、有意的攻击、其他非正常情况。
2.1. 硬件失效
软件设计必须考虑所涉及的硬件潜在失效模式。
典型考虑:
1) 电源失效防护
2) 电磁干扰
3) 系统不稳定
4) 干扰信号
2.2. 接口考虑
2.2.1. 人机接口设计
可选择的常用方法:
1) 接受错误输入,留给系统处理
2) 接受错误输入, 什么也不产生
3) 接受错误输入, 输出错误提示信息
4) 不允许错误输入进入
较完善的设计:
1) 软件能判断出操作员的输入操作正确性(或合理性)
2) 在遇到不正确(或不合理)输入和操作时, 软件拒绝该操作的执行
3) 软件提醒操作员注意错误的输入或操作
4) 软件指出错误的类型和纠正措施
2.2.2. 程序接口设计
对输入参数进行合法性检查,对非法参数进行处理,常用方法:
1) 返回错误代码
2) 返回中间值
3) 使用下一个合法数据代替
4) 使用上一个合法数据代替
5) 使用最接近的合法值
6) 调用异常处理程序进行处理
7) 调用显示错误信息程序并打印出来
8) 关闭程序
2.2.3. 硬件接口
硬件接口的软件设计必须考虑:
1) 使用握手信号保证通信的连通性
2) 预先确定数据传输信息的格式和内容
3) 每次传输都包含一个字或字符串来指明数据类型及信息内容
4) 使用奇偶校验、循环冗余校验(CRC校验)、海明码来验证数据传输的正确性
5) 充分估计接口的各种可能故障,并设计相应的处理措施
6) 对非法的外部中断的处理,软件应能够识别合法的及非法的外部中断
7) 对传感器故障的考虑
反馈回路中的传感器有可能出现故障并导致反馈异常信息, 软件应能预防将异常信息当作正常信息处理而造成反馈系统的失控。
8) 对输入/输出信息的考虑
软件对输入、输出信息进行加工处理前,应检验其是否合理(最简单的方法是极限量程检验)。对不合理的输入进行正确的处理。通过设计,保证输入/输出符合精度要求。
3. 容错性设计
3.1. 容错策略
容错是指在发生故障的情况下,系统不失效,仍然能够正常工作的特性。容错软件:在一定程度上,对自身故障具有屏蔽能力;在一定程度上, 能从错误状态自动恢复到正常状态;因缺陷而发生故障时, 仍然能在一定程度上完成预期的功能。
1) 故障探测
故障(不正确的系统状态)发生时, 系统必须能够探测到
2) 危害诊断
必须弄清楚受故障影响的系统范围
3) 故障恢复
系统必须恢复到已知的安全状态
4) 故障修复
可以对系统进行改进, 防止故障再次发生
3.2. 容错技术
级别 |
说明 |
0级 |
没有任何容错 |
1级 |
自动检测并重新启动 |
2级 |
在1级的基础上, 增加周期性检查点, 记录和恢复内部状态 |
3级 |
在2级的基础上, 增加持续稳固的数据恢复 |
4级 |
无中断的持续运行 |
3.2.1. 监控定时器
提供监控定时器或类似措施,以确保微处理器或计算机具有处理程序超时或死循环故障的能力。
监控定时器的设计原则:
1) 监控定时器应力求采用独立的时钟源, 用独立的硬件实现
2) 采用可编程定时器实现时, 应统筹设计计数时钟频率和定时参数, 力求在外界干扰条件下, 定时器受到干扰后, 定时参数的最小值大于系统重新初始化所需的时间值, 最大值小于系统允许的最长故障处理时间值
3) 硬件状态变化有关的程序设计应考虑状态检测的次数或时间, 无时间依据的情况下可用循环等待次数作为依据, 超过一定次数作为超时处理
3.2.2. 冗余设计
1) 空间冗余
在某一运行空间出现问题时, 可以启用另外的空间来工作。典型的空间冗余:存储空间冗余(RAID,数据库日志等)、处理器空间冗余(多个CPU协同工作)、网络空间冗余、进程冗余(Apache服务器)。
2) 时间冗余
为了获得成功的结果, 多次尝试相同的操作。典型的时间冗余:外部接口数据传输、传感器数据的采集、磁盘数据的读取、重启不稳定的服务。
3) 结构冗余
结构冗余的典型方式为TMR(Triple-Modular Redundancy),TMR能成功地容错基于两个基本假设:组件不能包含相同的设计缺陷;组件的失效是随机的,多个组件同时失效的概率极低。对于相同的软件而言,两个假设都无法满足:简单复制的软件将包含相同的设计缺陷;对于相同的软件,同时失效不可避免。用于容错的软件必须相异。
软件结构冗余技术:
a) N版本程序设计
不同的小组用许多变体实现相同的规格说明,所有变体同时进行计算,利用表决系统选择多数作为输出。假定不同的小组犯相同错误的概率极低,也可能连算法都不一样。N版本程序设计是最常用方法,例如: 在许多型号空中客车商用飞机中。
b) 恢复块技术
相同的规格说明被实现成若干个明确不同的版本 顺序执行。利用验收检测程序选择接受的输出。强制每个版本使用不同的算法, 以降低相同错误的概率。验收检测程序的设计困难,必须独立于所使用的计算。由于冗余版本是依次顺序执行的,用于实时系统时应注意。
c) 一致性恢复块
d) 验收表决
e) 多版本软件开发策略
保证冗余软件版本之间的设计相异性;保证各独立单元具有高可靠性;保证表决算法具有高可靠性;保证验收检测程序具有高精确度和高可靠性
f) 相异性设计
用不同的途径设计和实现不同的软件版本, 使各版本具有不同的失效模式, 降低各版本包含一致性缺陷的概率。
不同的设计方法:面向对象/面向功能的设计、用不同的程序设计语言实现、使用不同的工具和开发环境、使用不同的实现算法。
4. 安全性设计
4.1. 准则
1) 确保最薄弱环节的安全
2) 纵深防御,避免单点失效
3) 失效安全:进入安全状态、阻止信息非授权访问
4) 最小化准则:范围最小化、权限最小化
5) 通过冗余和多样性降低风险
6) 验证所有输入
4.2. 方法
4.2.1. 风险隔离
划分(Partitioning):为功能上独立的软件部件提供隔离的过程。
使用划分的目的:抑制故障的影响和/或隔离故障,防止组件之间特殊的相互作用和交叉耦合干扰;减少软件验证过程的工作量;最小化安全相关组件的规模。
划分等级的确定:对于通过划分提供保护的软件,可使用与每个组件相关的最严重的失效状态类别来确定该组件的重要等级;区别对待不同等级的软件组件。
划分时考虑的因素:
1) 硬件资源,包括处理器、存储器设备、输入/输出设备、中断和定时器
2) 控制耦合性,外部存取脆弱性
3) 数据耦合性,共享或覆盖数据,包括堆栈和处理器寄存器
4) 与保护机制相关的硬件设备的失效模式
运用信息隐蔽技术,使信息仅对有权和需要访问它的程序开放。信息隐蔽可以避错的三个理由:降低了信息意外讹误的概率;可以帮助在程序中建立防火墙,降低信息问题影响的范围;由于信息被局部化,程序员更少地产生错误,验证人员更容易找到缺陷。
常见需要隔离的信息:
1) 安全关键的数据
2) 容易被改动的区域
3) 复杂的数据
4) 复杂的逻辑
5) 在编程语言层次上的操作
信息隐蔽的障碍:
1) 信息过度分散
2) 交叉依赖
3) 误把局部数据当成全局数据
4) 误认为会损失性能
在数据区和指令区建立防火墙:
1) 为了防止程序把数据错当指令来执行,要采用将数据与指令分隔存放的措施
2) 将不用的内存区域初始化成具有确定性的模式,阻止程序意外跳转到未知存储中运行,一旦不用的内存区域被当做指令被执行,应使系统恢复到已知的安全状态
3) 必要时,在数据区和表格的前后加入适当的NOP指令和跳转指令,使NOP指令的总长度等于最长指令的长度, 然后加入一条跳转指令,将控制转向出错处理程序
4.2.2. 异常处理
- 内部异常处理
1) 在运行阶段, 对于预期范围内的异常, 异常处理措施应保证系统处于安全状态, 并持续运行
2) 在运行阶段, 对于超出预期的异常, 异常处理措施最低限度应使系统转入安全状态
3) 在异常发生之后, 采取措施, 保证数据的完整性
4) 在异常发生之后, 采取措施, 保证敏感和关键数据不被泄露
- 外部异常处理
1) 周期性检测外部输入/输出设备的状态, 并在发生失效时转到到某个安全状态
2) 对于非法的外部中断, 软件应能自动切换到安全状态
4.2.3. BIT (Built-In self-Test)
BIT是指系统、设备内部提供的检测故障、隔离故障的自动测试能力,是改善系统、设备安全性、测试性、维护性、可用性的技术。BIT增加了系统复杂性,自身的可靠性也非常重要。BIT向PHM方向发展。
软件设计必须考虑在系统加电时完成系统级的自检测, 验证系统是安全可靠的, 并在正常地起作用。在可能时, 软件应对系统进行周期性自检测, 以监视系统的安全状态。必要时, 软件应提供维护自检测, 提高系统的可维护性和可测试性。加电自检测、周期性自检测、维护自检测应具有不同级别的检测能力。
应注意周期性自检测功能对其它任务的影响。建立一个可能影响软件的硬件失效列表, 必须清楚地知道并文档化可以通过BIT进行检测的故障模式。BIT发现故障时, 应以显式的方式报告给需要知晓的人, 尤其是哪些安全相关的故障。
1) CPU自检测
EMI、放电、电击、宇宙射线等都可能损坏CPU, 通常在引导时进行CPU自检, 以便证实CPU的运算正确, 如果测试失败, 则CPU有缺陷, 软件应进入安全状态
2) ROM检测
ROM(EEPROM或闪存等)上的软件会优先执行, 检验其完整性是重要的, 通常在上电时完成检测, 如果系统具有变更自身程序的能力则检测应该周期性运行
4.2.4. 故障监控
应设计监控功能, 直接检测可能引起的失效状态, 并对危险失效采取安全防护。监控功能应确保想要检测的故障在所有必要的条件下都能得到检测。应确保监控功能和防护措施不会因为导致被监控危害的同一失效状态而不予动作(共因失效)。
4.2.5. 部署设计
软件设计应提供内嵌的对部署的支持, 降低系统管理员或用户配置软件过程中出现错误。
1) 提供对配置的检查和分析的功能
2) 最小化缺省特权
3) 局部化配置设定
4) 提供修补安全漏洞的简单方法