参数有效性检验 包含函数打印等等
原文地址:https://onlycalm.cn/docs/编程思想/参数有效性检验/
这里做一个复制
前言:
2018-11-26天气凉,耗时三个周末完成这篇原创文章,记录下自己关于程序安全性方面的一些微薄见解。愿自己程序员之路越走越顺利,保持激情初心,不忘理想前行。
1 问题:
- 为什么要检验?
- 哪些情况判为参数失效?
- 有哪些参数需要检验?
- 怎么检测?
- 在哪里检验?
- 怎么处理?
2 为什么要检验?
保护程序免糟非法输入数据的破坏,尽可能将异常数据对程序造成的影响控制在有限的范围内。
防御式编程主要思想:子程序应该不因传入错误数据而被破坏,哪怕是由其他子程序产生的错误数据。更一般地说,其核心思想是承认程序都会有问题,都需要被修改,聪明的程序员应该根据这一点来编程。
不管进来什么,好的程序都不会生成垃圾,而是做到“垃圾进,什么都不出”、“进来垃圾,出去是错误提示”或“不许垃圾进来”。
——《代码大全2》第8章 防御式编程
3 哪些情况判为参数失效?
- 参数越界失效:参数值不在预期范围内。比如参数值超过上下限,数组下标越界。
- 符号异常失效:指正负号异常,应该尽可能使用无符号类型。
- 空指针失效:传递空指针。
- 数据过期失效:原本实时更新的数据长期未变化。
- 跳变失效:数据波动异常的大或小。
- 关联数据异常失效:指两个以上的数据值之间有依赖和关联,当值不符合依赖关联性时认为失效。
4 有哪些参数需要检验?
最佳的形式是在一开始就不引入错误。
- 从数据来源讲:
- 检查来源于外部的数据的值。
- 检查子程序的输入参数的值。
应重点检查外部输入数据(网络通讯传入、上层或下层传入、硬件设备采集数据)外部传入的数据对本模块的程序员来说往往是模糊和不可操控的,内部数据也较多依赖于外部输入数据,因此重点检查外部数据。内部数据可以只检查较为复杂的算式或算法结果。
5 怎么检测?
对参数进行失效检验意味着需要存储有一些有关参数的信息,可以定义结构体化的参数。我们对参数进行分类如下:
5.1 功能型参数
用于表示某项功能开关的参数,我们可以定义为如下结构体:
1
|
//Type redefinition------------------------------------------------------------------------
|
note:定义历史值方便检测功能开关状态切换的跳变变化。功能有时需要记录开启或者关闭时间,因此直接将定时器封装进结构体使得参数关系紧密。
对功能型参数使用枚举类型因此不需要检测值越界失效和符号异常失效,重要的是检测关联数据异常失效。比如功能A和B为互斥关系,一次只能有一个功能打开或两个功能都关闭,再比如功能B开启前应该先打开功能A。功能开关较多,关联性强的情况下这样的检测变得重要。
对于功能型参数的关联性检测可以放到预处理时期由编译器检测,不用等到外出调试才发现错误。预处理指令不能检测变量,考虑到功能有效性一旦确当后不在修改,但又要满足调试时可能存在的对功能开关的修改,可以通过以下形式实现:
1
|
//General macro definition-----------------------------------------------------------------
|
5.2 状态型参数
状态型参数用于表示当前运行下的某种状态,是实时的因此不像常量宏可以用预处理指令检验。比如电机的开关波状态,阀门的开关状态,气压状态等。我们可以定义为如下结构体:
1
|
//Status enum definition-------------------------------------------------------------------
|
note:定义历史值方便检测状态转换的跳变变化。状态常常需要记录持续的时间,因此加入一个计数器可用于计时。在一段时间内有时还需要记录状态发生的次数,因此需要一个变量记录状态发生频度。
一种运行状态可能存在两种以上的状态值,且不同的状态值有不同的含义,因此无法用统一的枚举类型。对于一组状态值可以单独使用一组枚举表示,枚举的参数封装性更好,宏定义参数较“散”但省空间,综合多种因素在状态繁多的情况下我选择用封装性更好的枚举。
对于状态的检测,由于状态是随着程序运行实时变化的且不固定,因此无法在编译阶段对状态值的有效性检测。这就需要我们根据实际逻辑需要增加相应的检测代码。由于状态值都通过枚举定义好,因此不需要做参数越界失效和符号异常失效判断,状态值都是预知且固定的所以不需要跳变失效判断,其他失效根据实际情况添加检验代码。
5.3 故障型参数。
故障型参数用于记录当前或历史的故障发生情况,并决定是否要发出报警信号或停机等操作。我们可以定义为如下结构体:
1
|
//Unusual enum definition-------------------------------------------------------------------
|
note:定义历史值方便检测故障的发生和恢复变化。对于故障常常需要检测故障的持续时间和频度,因此定义相关成员变量。根据故障的持续时间和频度有时会引起其他动作,比如报警或停机,因此引入报警标志。
故障的状态只需要两种,即异常或正常,因此可以同意定义枚举类型。
对于故障的检测,由于故障是在程序运行中发生,因此对故障参数有效性的检测需要实时进行。由于故障参数有固定的枚举类型,因此不需要越界失效、符号异常、跳变失效判断,其他失效根据实际情况添加检验代码。
5.4 普通值参数。
普通值参数是较为庞大的一类参数,这类参数的值类型不一,值之间的关联性可以很复杂,每种值各有特点,参数的有效性检验很难做统一的处理。我们可以定义为如下结构体:
1
|
//Sign enum definition-------------------------------------------------------------------
|
6 怎么处理?
6.1 用断言检查永远不应该发生的错误
优点:错误定位,给代码调试带来极大的便利,创建更稳定、质量更好且不易于出错的代码。
缺点:频繁的调用会极大的影响程序的性能,增加额外的开销。
相比于函数,调用函数需要额外的队栈开销,宏函数节省开销但同一段代码存在多个副本。使用宏函数会引起很多副作用需要小心。
断言使用形式如下:
1
|
//#define NDEBUG
|
note:通过定义宏NDEBUG来控制断言的开启或关断。用于对较为严重的错误统一处理。另外,禁止把必须执行的代码放在断言中,断言关闭后功能将失效。
用断言的两种形式:
- 断言一直开启。不论是调试版还是发行版都将断言函数开启。
- 可以在测试时启用断言,而在部署时禁用断言。同样,程序投入运行后,最终用户在遇到问题时可以重新起用断言。它可以快速发现并定位软件问题,同时对系统错误进行自动报警。断言可以对在系统中隐藏很深,用其它手段极难发现的问题可以用断言来进行定位,从而缩短软件问题定位时间,提高系统的可测性。实际应用时,可根据具体情况灵活地设计断言。
- 前置条件断言:代码执行之前必须具备的特性。
- 后置条件断言:代码执行之后必须具备的特性。
- 前后不变断言:代码执行前后不能变化的特性。
6.2 错误处理代码
用错误处理代码来处理预期会发生的状况,用断言来处理绝不应该发生的状况。通常用错误处理来检查有害的输入数据。断言检查代码中的bug。
根据情形的不同,你可以返回中立值、换用下一个正确数据、返回与前次相同的值、换用最接近的有效值、在日志文件中记录警告信息、返回一个错误吗、调用错误处理子程序或对象、显示出错信息或者关闭程序——或把这些技术结合起来使用。
6.2.1 返回中立值
对于一些“危害”并不严重的错误,最佳的做法就是继续执行操作并简单的返回一个没有危害的数值,比如数值0或空指针。
6.2.2 给出一个修正的数据
对于can网络中不断更新但实时性要求并不高的数据,比如电机温度,短时间内数据丢失可是使用历史值。
6.2.3 换用最接近的合法值
有些情况下可以换用最接近的合法值,当值大于上限或者小于下限值时,可以直接去上下限边界值。
6.2.4 把警告信息记录到日志文件中
在检测到错误数据的时候,你可以选择在日志文件中记录一条警告信息,然后继续执行。这种方法可以同其他的错误处理技术结合使用。
6.2.5 返回一个错误码
你可以决定只让系统的某些部分处理错误。其他部分则不在本地(局部)处理错误,而只是简单地报告有错误发生和发生的是何种错误,并信任调用链上游的某个子程序会处理该错误。通知系统其余部分已经发生错误可以采用下列方法:
- 设置一个状态变量的值。
- 用状态值作为函数的返回值。
- 用语言内建的异常机制抛出一个异常。
需要决定系统里哪部分应该直接处理错误,哪部分只是报告所发生的错误。对于安全性很重要的部分请确认调用的子程序总会检查返回的错误码。
6.2.6 调用错误处理子程序或对象
这种方法需要把错误处理都集中在一个全局的错误处理子程序或对象中,使得调试工作更为简单。而代价是整个程序都要知道这个集中点并与之紧密耦合。如果你想在其他系统中重用其中的某些代码,那就得把错误处理代码一并带过去。
6.2.7 错误发生时发出错误信息
在调试模式下,当错误发生时为了帮助调试者尽快定位问题所在位置,可以通过打印显示或者报文将错误信息较为详细的发出。
6.2.8 在局部处理错误
针对一些设计方案可能更适合在局部立即解决所有遇到的问题,具体的错误处理方法需要由该模块的设计者根据问题特点自行决定。这种方法给力模块程序员很大的灵活度,但这样做会导致错误记录和发出错误信息等代码散布到整个系统中,从而使得设计者还得考虑错误记录和错误发送相关的故障问题。能在局部立即解决的问题最好在内部立即解决。
6.2.9 关闭或复位程序
程序发生较为严重的错误时,比如堆栈溢出导致运行环境被破坏,所依附的操作系统严重故障,甚至收到恶意攻击时可能需要关闭和复位程序。
确定一种通用的处理错误参数的方法,是架构层次(或称高层次)的设计决策。如果在高层次处理错误,低层次只是汇报错误,那么就要确保高层次真的处理有错误!千万不要忽略错误信息,在函数的返回值返回故障信息,记得检查函数的返回值,即使你对某个函数有足够的自信。当有错误产生时,请记录并反馈对应的故障码和它描述的信息。
6.3 异常
如果一个子程序在运行过程中遇到了预料之外的情况,但这个情况又必须处理否则会影响功能的使用或程序的运行,这个时候需要子程序将异常抛出,把“问题”交给能解释并处理好它的代码。这属于一种错误处理机制。
能在局部处理的错误优选在局部处理,不能立即处理或局部无法处理的问题可抛出异常。不能用异常来推卸责任。异常处理函数需要了解大量子程序可能抛出何种错误并给出处理办法,弱化了程序的封装性,增加了程序的复杂程度。
每种异常都对应一种特殊情况,因此要确保异常信息中含有为理解异常抛出原因所需的充分信息,这对于读取异常信息的人来说是很有价值的。比如当发生数据越界时,除了抛出对应的故障码标识数据越界外,还应该给出上下界和非法的数据值。
总结:断言、错误处理代码、异常都需要有完备的故障捕获处理,以及故障信息的记录反馈。因此有必要考虑一种方法以确保异常处理的一致性,即创建一个集中的异常报告机制。这个机制需要能对异常信息进行一个集中的格式化和存储。c语言不像c++和java自带异常机制,但c语言可以借助setjmp和longjmp实现异常处理机制。实现代码如下:
1
|
typedef unsigned char Byte; |