嵌入式软件可靠性设计三方面的考虑:防错,判错,容错的详细概述

0.前言
设备的可靠性涉及多个方面:稳定的硬件、优秀的软件架构、严格的测试以及市场和时间的检验等等。这里着重谈一下作者自己对嵌入式软件可靠性设计的一些理解,通过一定的技巧和方法提高软件可靠性。这里所说的嵌入式设备,是指使用单片机、arm7、cortex-m0,m3之类为核心的测控或工控系统。
嵌入式软件可靠性设计应该从防错、判错和容错三方面进行考虑. 此外,还需理解自己所使用的编译器特性。
1.防错
良好的软件架构、清晰的代码结构、掌握硬件、深入理解c语言是防错的要点,这里只谈一下c语言。
“人的思维和经验积累对软件可靠性有很大影响。c语言诡异且有种种陷阱和缺陷,需要程序员多年历练才能达到较为完善的地步。“软件的质量是由程序员的质量以及他们相互之间的协作决定的”。因此,作者认为防错的重点是要考虑人的因素。
“深入一门语言编程,不要浮于表面”。软件的可靠性,与你理解的语言深度密切相关,嵌入式c更是如此。除了语言,作者认为嵌入式开发还必须深入理解编译器。
本节将对c语言的陷阱和缺陷做初步探讨。
1.1 处处皆陷阱
最初开始编程时,除了英文标点被误写成中文标点外,可能被大家普遍遇到的是将比较运算符==误写成赋值运算符=,代码如下所示:
if(x=5) { … }
这里本意是比较变量x是否等于常量5,但是误将’==’写成了’=’,if语句恒为真。如果在逻辑判断表达式中出现赋值运算符,现在的大多数编译器会给出警告信息。并非所有程序员都会注意到这类警告,因此有经验的程序员使用下面的代码来避免此类错误:
if(5==x) { … }
将常量放在变量x的左边,即使程序员误将’==’写成了’=’,编译器会产生一个任谁也不能无视的语法错误信息:不可给常量赋值!
+=与=+、-=与=-也是容易写混的。复合赋值运算符(+=、*=等等)虽然可以使表达式更加简洁并有可能产生更高效的机器代码,但某些复合赋值运算符也会给程序带来隐含bug,如下所示代码:
tmp=+1;
该代码本意是想表达tmp=tmp+1,但是将复合赋值运算符+=误写成=+:将正整数常量1赋值给变量tmp。编译器会欣然接受这类代码,连警告都不会产生。
如果你能在调试阶段就发现这个bug,你真应该庆祝一下,否则这很可能会成为一个重大隐含bug,且不易被察觉。
-=与=-也是同样道理。与之类似的还有逻辑与&&和位与&、逻辑或||和位或|、逻辑非!和位取反~。此外字母l和数字1、字母o和数字0也易混淆,这种情况可借助编译器来纠正。
很多的软件bug自于输入错误。在google上搜索的时候,有些结果列表项中带有一条警告,表明google认为它带有恶意代码。如果你在2009年1月31日一大早使用google搜索的话,你就会看到,在那天早晨55分钟的时间内,google的搜索结果标明每个站点对你的pc都是有害的。这涉及到整个internet上的所有站点,包括google自己的所有站点和服务。google的恶意软件检测功能通过在一个已知攻击者的列表上查找站点,从而识别出危险站点。在1月31日早晨,对这个列表的更新意外地包含了一条斜杠(“/”)。所有的url都包含一条斜杠,并且,反恶意软件功能把这条斜杠理解为所有的url都是可疑的,因此,它愉快地对搜索结果中的每个站点都添加一条警告。很少见到如此简单的一个输入错误带来的结果如此奇怪且影响如此广泛,但程序就是这样,容不得一丝疏忽。
数组常常也是引起程序不稳定的重要因素,c语言数组的迷惑性与数组下标从0开始密不可分,你可以定义int a[30],但是你绝不可以使用数组元素a[30],除非你自己明确知道在做什么。
switch…case语句可以很方便的实现多分支结构,但要注意在合适的位置添加break关键字。程序员往往容易漏加break从而引起顺序执行多个case语句,这也许是c的一个缺陷之处。对于switch…case语句,从概率论上说,绝大多数程序一次只需执行一个匹配的case语句,而每一个这样的case语句后都必须跟一个break。去复杂化大概率事件,这多少有些不合常情。
break关键字用于跳出最近的那层循环语句或者switch语句,但程序员往往不够重视这一点。
1990年1月15日,at&t电话网络位于纽约的一台交换机当机并且重启,引起它邻近交换机瘫痪,由此及彼,一个连着一个,很快,114台交换机每六秒当机重启一次,六万人九小时内不能打长途电话。当时的解决方式:工程师重装了以前的软件版本。事后的事故调查发现,这是break关键字误用造成的。《c专家编程》提供了一个简化版的问题源码:
networkcode()
{
switch(line){
casething1:
doit1();
break;
casething2:
if(x==stuff){
do_first_stuff();
if(y==other_stuff)
break;
do_later_stuff();
}/*代码的意图是跳转到这里……*/
initialize_modes_pointer();
break;
default:
processing();
}/*……但事实上跳到了这里。*/
use_modes_pointer();/*致使modes_pointer未初始化*/
}
那个程序员希望从if语句跳出,但他却忘记了break关键字实际上跳出最近的那层循环语句或者switch语句。现在它跳出了switch语句,执行了use_modes_pointer()函数。但必要的初始化工作并未完成,为将来程序的失败埋下了伏笔。
将一个整形常量赋值给变量,代码如下所示:
int a=34, b=034;
变量a和b相等吗?答案是不相等的。我们知道,16进制常量以’0x’为前缀,10进制常量不需要前缀,那么8进制呢?它与10进制和16进制表示方法都不相通,它以数字’0’为前缀,这多少有点奇葩:三种进制的表示方法完全不相通。如果8进制也像16进制那样以数字和字母表示前缀的话,或许更有利于减少软件bug,毕竟你使用8进制的次数可能都不会有误使用的次数多!下面展示一个误用8进制的例子,最后一个数组元素赋值错误:
a[0]=106;/*十进制数106*/
a[1]=112;/*十进制数112*/
a[2]=052;/*实际为十进制数42,本意为十进制52*/
指针的加减运算是特殊的。下面的代码运行在32位arm架构上,执行之后,a和p的值分别是多少?
inta=1;
int*p=(int*)0x00001000;
a=a+1;
p=p+1;
对于a的值很容判断出结果为2,但是p的结果却是0x00001004。指针p加1后,p的值增加了4,这是为什么呢?原因是指针做加减运算时是以指针的数据类型为单位。p+1实际上是p+1*sizeof(int)。不理解这一点,在使用指针直接操作数据时极易犯错。比如下面对连续ram初始化零操作代码:
unsignedint*pramaddr;//定义地址指针变量
for(pramaddr=startaddr;pramaddr {
*pramaddr=0x00000000;//指定ram地址清零
}
由于pramaddr是一个指针变量,所以pramaddr+=4代码其实使pramaddr偏移了4*sizeof(int)=16个字节,所以每执行一次for循环,会使变量pramaddr偏移16个字节空间,但只有4字节空间被初始化为零。其它的12字节数据的内容,在大多数架构处理器中都会是随机数。
对于sizeof(),这里强调两点,第一它是一个关键字,而不是函数,并且它默认返回无符号整形数据(要记住是无符号);第二,使用sizeof获取数组长度时,不要对指针应用sizeof操作符,比如下面的例子:
voidclearram(chararray[])
{
inti;
for(i=0;ib);//这里误加了一个分号
a=b;//这句代码一直被执行
不但如此,编译器还会忽略掉多余的空格符和换行符,就像下面的代码也不会给出足够提示:
if(n0;i--)
{
sensordata[i]=…;

}
这里声明了拥有30个元素的数组,不幸的是for循环代码中误用了本不存在的数组元素sensordata[30],但c语言却默许这么使用,并欣然的按照代码改变了数组元素sensordata[30]所在位置的值, sensordata[30]所在的位置原本是一个lcd显示变量,这正是显示屏上的那个值不正常被改变的原因。真庆幸这么轻而易举的发现了这个bug。
其实很多编译器会对上述代码产生一个警告:赋值超出数组界限。但并非所有程序员都对编译器警告保持足够敏感,况且,编译器也并不能检查出数组越界的所有情况。举一个例子,你在模块a中定义数组:
int sensordata[30];
在模块b中引用该数组,但由于你引用代码并不规范,这里没有显示声明数组大小,但编译器也允许这么做:
extern int sensordata[];
如果在模块b中存在和上面一样的代码:
for(i=30;i>0;i--)
{
sensordata[i]=…;

}
这次,编译器不会给出警告信息,因为编译器压根就不知道数组的元素个数。所以,当一个数组声明为具有外部链接,它的大小应该显式声明。
再举一个编译器检查不出数组越界的例子。函数func()的形参是一个数组形式,函数代码简化如下所示:
char*func(charsensordata[30])
{
unsignedinti;
for(i=30;i>0;i--)
{
sensordata[i]=…;

}
}
这个给sensordata[30]赋初值的语句,编译器也是不给任何警告的。实际上,编译器是将数组名sensor隐含的转化为指向数组第一个元素的指针,函数体是使用指针的形式来访问数组的,它当然也不会知道数组元素的个数了。造成这种局面的原因之一是c编译器的作者们认为指针代替数组可以提高程序效率,而且,还可以简化编译器的复杂度。
指针和数组是容易给程序造成混乱的,我们有必要仔细的区分它们的不同。其实换一个角度想想,它们也是容易区分的:可以将数组名等同于指针的情况有且只有一处,就是上面例子提到的数组作为函数形参时。其它时候,数组名是数组名,指针是指针。
下面的例子编译器同样检查不出数组越界。
我们常常用数组来缓存通讯中的一帧数据。在通讯中断中将接收的数据保存到数组中,直到一帧数据完全接收后再进行处理。即使定义的数组长度足够长,接收数据的过程中也可能发生数组越界,特别是干扰严重时。这是由于外界的干扰破坏了数据帧的某些位,对一帧的数据长度判断错误,接收的数据超出数组范围,多余的数据改写与数组相邻的变量,造成系统崩溃。由于中断事件的异步性,这类数组越界编译器无法检查到。
如果局部数组越界,可能引发arm架构硬件异常。同事的一个设备用于接收无线传感器的数据,一次软件升级后,发现接收设备工作一段时间后会死机。调试表明arm7处理器发生了硬件异常,异常处理代码是一段死循环(死机的直接原因)。接收设备有一个硬件模块用于接收无线传感器的整包数据并存在自己的硬件缓冲区中,当一帧数据接收完成后,使用外部中断通知设备取数据,外部中断服务程序精简后如下所示:
__irqexinthandler(void)
{
unsignedchardatabuf[50];
getdata(databug);//从硬件缓冲区取一帧数据

}
由于存在多个无线传感器近乎同时发送数据的可能加之getdata()函数保护力度不够,数组databuf在取数据过程中发生越界。由于数组databuf为局部变量,被分配在堆栈中,同在此堆栈中的还有中断发生时的运行环境以及中断返回地址。溢出的数据将这些数据破坏掉,中断返回时pc指针可能变成一个不合法值,硬件异常由此产生。
如果我们精心设计溢出部分的数据,化数据为指令,就可以利用数组越界来修改pc指针的值,使之指向我们希望执行的代码。1988年,第一个网络蠕虫在一天之内感染了2000到6000台计算机,这个蠕虫程序利用的正是一个标准输入库函数的数组越界bug。起因是一个标准输入输出库函数gets(),原来设计为从数据流中获取一段文本,遗憾的是,gets()函数没有规定输入文本的长度。gets()函数内部定义了一个500字节的数组,攻击者发送了大于500字节的数据,利用溢出的数据修改了堆栈中的pc指针,从而获取了系统权限。
一个程序模块通常由两个文件组成,源文件和头文件。如果你在源文件定义变量:
unsigned int a;
并在头文件中声明该变量:extern unsigned long a;
编译器会提示一个语法错误:变量’a’声明类型不一致。但如果你在源文件定义变量:
volatile unsigned int a,
在头文件中声明变量:extern unsigned int a; /*缺少volatile限定符*/
编译器却不会给出错误信息(有些编译器仅给出一条警告)。这里volatile属于类型限定符,另一个常见的类型限定符是const关键字。限定符volatile在嵌入式软件中至关重要,用来告诉编译器不要优化它修饰的变量。这里举一个刻意构造出的例子,因为现实中的volatile使用bug大都隐含且难以理解。
在模块a的源文件中,定义变量:
volatile unsigned int timercount=0;
该变量用来在一个定时器服务程序中进行软件计时:
timercount++; //读取io端口1的值
在模块a的头文件中,声明变量:
extern unsigned int timercount; //这里漏掉了类型限定符volatile
在模块b中,要使用timercount变量进行精确的软件延时:
#include“...a.h”//首先包含模块a的头文件

timercount=0;
while(timercount>=timer_value);//延时一段时间

实际上,这是一个死循环。由于模块a头文件中声明变量timercount时漏掉了volatile限定符,在模块b中,变量timercount是被当作unsigned int类型变量。由于寄存器速度远快于ram,编译器在使用非volatile限定变量时是先将变量从ram中拷贝到寄存器中,如果同一个代码块再次用到该变量,就不再从ram中拷贝数据而是直接使用之前寄存器备份值。代码while(timercount>=timer_value)中,变量timercount仅第一次执行时被使用,之后都是使用的寄存器备份值,而这个寄存器值一直为0,所以程序无限循环。下面的流程图说明了程序使用限定符volatile和不使用volatile的执行过程。
arm架构下的编译器会频繁的使用堆栈,堆栈用于存储函数的返回值、aapcs规定的必须保护的寄存器以及局部变量,包括局部数组、结构体、联合体和c++的类。从堆栈中分配的局部变量的初值是不确定的,因此需要运行时显式初始化该变量。一旦离开局部变量的作用域,这个变量立即被释放,其它代码也就可以使用它,因此堆栈中的一个内存位置可能对应整个程序的多个变量。
局部变量必须显式初始化,除非你确定知道你要做什么。下面的代码得到的温度值跟预期会有很大差别,因为在使用局部变量sum时,并不能保证它的初值为0。编译器会在第一次运行时清零堆栈区域,这加重了此类bug的隐蔽性。
unsignedintgettempvalue(void)
{
unsignedintsum;//定义局部变量,保存总值
for(i=0;i<10;i++)
{
sum+=collecttemp();//函数collecttemp可以得到当前的温度值
}
return(sum/10);
}
由于一旦程序离开局部变量的作用域即被释放,所以下面代码返回指向局部变量的指针是没有实际意义的,该指针指向的区域可能会被其它程序使用,其值会被改变。
char*getdata(void)
{
charbuffer[100];//局部数组

returnbuffer;
}
让人欣慰的是,现在越来越多的编译器意识到了语义检查的重要性,编译器的语义检查也越来越强大,比如著名的keil mdk编译器在其 v4.47或以上版本中增加了动态语法检查并加强了语义检查,可以友好的提示更多警告信息。
1.3 不合理的优先级
c语言有32个关键字,却有34个运算符。要记住所有运算符的优先级是困难的。不合理的#define会加重优先级问题,让问题变得更加隐蔽。
#definereadsdaio0pin&(1<<11)//定义宏,读io口p0.11的端口状态
//判断端口p0.11是否为高电平
if(readsda==(1<<11))
{

}
编译器在编译后将宏带入,原if语句变为:
if(io0pin&(1<<11)==(1<<11))
{

}
运算符'=='的优先级是大于'&'的,代码io0pin&(1<<11)==(1<<11))等效为io0pin&0x00000001:判断端口p0.0是否为高电平,这与原意相差甚远。
为了制造更多的软件bug,c语言的运算符当然不会只止步于数目繁多。在此基础上,按照常规方式使用时,可能引起误会的运算符更是比比皆是!如下表所示:
常被误会的
优先级 表达式 常被误认为: 其实是:
取值运算符*与自增运算符++优先级相同,但它们是自右向左结合 *p++ (*p)++ *(p++)
成员选择运算符.高于取值运算符* *p.f (*p).f *(p.f)
数组下标运算符[]优先级高于取值运算符* int *ap[] int (*ap)[]
ap为数组指针 int *(ap[])
ap为指针数组
函数()优先级高于取值运算符* int * fp() int (*fp)()
fp为函数指针 int * (fp())
fp为函数,返回指针
等于==和不等于!=运算符优先级高于位操作运算符&、^ 和 | val & mask != 0 (val & mask)!= 0 val &(mask != 0)
等于==和不等于!=运算符高于赋值运算符= c=getchar()!=eof (c=getchar())!=eof c=(getchar()!=eof)
算数运算符+和-优先级高于移位运算符 msb<<4+lsb (msb<<4)+lsb msb<<(4+lsb)
1.4 隐式转换和强制转换
这又是c语言的一大诡异之处,它造成的危害程度与数组和指针有的一拼。语句或表达式通常应该只使用一种类型的变量和常量。然而,如果你混合使用类型,c使用一个规则集合来自动完成类型转换。这可能很方便,但也很危险。
a.当出现在表达式里时,有符号和无符号的char和short类型都将自动被转换为int类型,在需要的情况下,将自动被转换为unsigned int(在short和int具有相同大小时)。这称为类型提升。提升在算数运算中通常不会有什么大的坏处,但如果位运算符 ~ 和 <>4;
假如我们不了解表达式里的类型提升,认为在运算过程中变量port一直是unsigned char类型的。我们来看一下运算过程:~port结果为0xa5,0xa5>>4结果为0x0a,这是我们期望的值。但实际上,result_8的结果却是0xfa!在arm结构下,int类型为32位。变量port在运算前被提升为int类型:~port结果为0xffffffa5,0xa5>>4结果为0x0ffffffa,赋值给变量result_8,发生类型截断(这也是隐式的!),result_8=0xfa。经过这么诡异的隐式转换,结果跟我们期望的值,已经大相径庭!正确的表达式语句应该为:
result_8=(unsigned char) (~port) >> 4;/*强制转换*/
b.在包含两种数据类型的任何运算里,两个值都会被转换成两种类型里较高的级别。类型级别从高到低的顺序是long double、double、float、unsigned long long、long long、unsigned long、long、unsigned int、int。这种类型提升通常都是件好事,但往往有很多程序员不能真正理解这句话,从而做一些想当然的事情,比如下面的例子,int类型表示16位。
uint16_tu16a=40000;/*16位无符号变量*/
uint16_tu16b=30000;/*16位无符号变量*/
uint32_tu32x;/*32位无符号变量*/
uint32_tu32y;
u32x=u16a+u16b;/*u32x=70000还是4464?*/
u32y=(uint32_t)(u16a+u16b);/*u32y=70000还是4464?*/
u32x和u32y的结果都是4464(70000%65536)!不要认为表达式中有一个高类别uint32_t类型变量,编译器都会帮你把所有其他低类别都提升到uint32_t类型。正确的书写方式:
u32x=(uint32_t)u16a+(uint32_t)u16b;或者:
u32x=(uint32_t)u16a+u16b;
后一种写法在本表达式中是正确的,但是在其它表达式中不一定正确,比如:
uint16_tu16a,u16b,u16c;
uint32_tu32x;
u32x=u16a+u16b+(uint32_t)u16c;/*错误写法,u16a+u16b仍可能溢出*/
c.在赋值语句里,计算的最后结果被转换成将要被赋予值得那个变量的类型。这一过程可能导致类型提升也可能导致类型降级。降级可能会导致问题。比如将运算结果为321的值赋值给8位char类型变量。程序必须对运算时的数据溢出做合理的处理。
很多其他语言,像pascal语言(好笑的是c语言设计者之一曾撰文狠狠批评过pascal语言),都不允许混合使用类型,但c语言不会限制你的自由,即便这经常引起bug。
d.当作为函数的参数被传递时,char和short会被转换为int,float会被转换为double。
e.c语言支持强制类型转换,如果你必须要进行强制类型转换时,要确保你对类型转换有足够了解:
并非所有强制类型转换都是由风险的,把一个整数值转换为一种具有相同符号的更宽类型时,是绝对安全的。
精度高的类型强制转换为精度低的类型时,通过丢弃适当数量的最高有效位来获取结果,也就是说会发生数据截断,并且可能改变数据的符号位。
精度低的类型强制转换为精度高的类型时,如果两种类型具有相同的符号,那么没什么问题;需要注意的是负的有符号精度低类型强制转换为无符号精度高类型时,会不直观的执行符号扩展,例如:
unsignedintbob;
signedcharfred=-1;
bob=(unsignedint)fred;/*发生符号扩展,此时bob为0xffffffff*/
一些编程建议:
深入理解嵌入式c语言以及编译器
细致、谨慎的编程
使用好的风格和合理的设计
不要仓促编写代码,写每一行的代码时都要三思而后行:可能会出现什么样的错误?是否考虑了所有的逻辑分支?
打开编译器所有警告开关
使用静态分析工具分析代码
安全的读写数据(检查所有数组边界…)
检查指针的合法性
检查函数入口参数合法性
检查所有返回值
在声明变量位置初始化所有变量
合理的使用括号
谨慎的进行强制转换
使用好的诊断信息日志和工具
2. 判错
工欲善其事必先利其器。判错的最终目的是用来暴露设计中的bug并加以改正,所以将错误信息提供给编程者是必要的。有时候需要将故障信息储存于非易失性存储器中,便于查看。这里以使用串口打印错误信息到pc显示屏为例,来说明一般需要显示什么信息。
编写或移植一个类似c标准库中的printf函数,可以格式化打印字符、字符串、十进制整数、十六进制整数。这里称为uartprintf()。
unsignedintwritedata(unsignedintaddr)
{
if((addr>=base_addr)&&(addr<=end_addr)){
…/*地址合法,进行处理*/
}else{/*地址错误,打印错误信息*/
uartprintf(文件%s的第%d行写数据时发生地址错误,错误地址为:0x%x\n,__file__,__line__,addr);
…/*错误处理代码*/
}
假设uartprintf()函数位于main.c模块的第256行,并且writedata()函数在读数据时传递了错误地址0x00000011,则会执行uartprintf()函数,打印如下所示的信息:
文件main.c的第256行写数据时发生地址错误,错误地址为:0x00000011。
类似这样的信息会有助于程序员定位分析错误产生的根源,更快的消除bug。
2.1具有形参的函数,需判断传递来的实参是否合法。
程序员可能无意识的传递了错误参数;外界的强干扰可能将传递的参数修改掉,或者使用随机参数意外的调用函数,因此在执行函数主体前,需要先确定实参是否合法。
intexam_fun(unsignedchar*str)
{
if(str!=null){//检查“假设指针不为空”这个条件
...//正常处理代码
}else{
uartprintf(…);//打印错误信息
…//处理错误代码
}
}
2.2 仔细检查函数的返回值
对函数返回的错误码,要进行全面仔细处理,必要时做错误记录。
char*dosomething(…)
{
char*p;
p=malloc(1024);
if(p==null){/*对函数返回值作出判断*/
uartprintf(…);/*打印错误信息*/
returnnull;
}
retuenp;
}
2.3 防止指针越界
如果动态计算一个地址时,要保证被计算的地址是合理的并指向某个有意义的地方。特别对于指向一个结构或数组的内部的指针,当指针增加或者改变后仍然指向同一个结构或数组。
2.4 防止数组越界
数组越界的问题前文已经讲述的很多了,由于c不会对数组进行有效的检测,因此必须在应用中显式的检测数组越界问题。下面的例子可用于中断接收通讯数据。
#definerec_buf_len100
unsignedcharrecbuf[rec_buf_len];
…//其它代码
voiduart_irqhandler(void)
{
staticreccount=0;//接收数据长度计数器
…//其它代码
if(reccount recbuf[reccount]=…;//从硬件取数据
reccount++;
…//其它代码
}else{
uartprintf(…);//打印错误信息
…//其它错误处理代码
}

}
在使用一些库函数时,同样需要对边界进行检查:
#definerec_buf_len100
unsignedcharrecbuf[rec_buf_len];
if(len memset(recbuf,0,len);//将数组recbuf清零
}else{
//处理错误
}
2.5 数学算数运算
检测除数是否为零
检测运算溢出情况
2.5.1 有符号整数除法,仅检测除数为零就够了吗?
两个整数相除,除了要检测除数是否为零外,还要检测除法是否溢出。对于一个signed long类型变量,它能表示的数值范围为:-2147483648 ~ +2147483647,如果让-2147483648 / -1,那么结果应该是+ 2147483648,但是这个结果已经超出了signed long所能表示的范围了。
#include
signedlongsl1,sl2,result;
/*初始化sl1和sl2*/
if((sl2==0)||((sl1==long_min)&&(sl2==-1))){
//处理错误
}else{
result=sl1/sl2;
}
2.5.2 加法溢出检测
a)无符号加法
#include
unsignedinta,b,result;
/*初始化a,b*/
if(uint_max-a0&&int_max-a //处理溢出
}else{
result=a+b;
}
2.5.3 乘法溢出检测
a)无符号乘法
#include
unsignedinta,b,result;
/*初始化a,b*/
if((a!=0)&&(uint_max/a //
}else{
result=a*b;
}
b)有符号乘法
#include
signedinta,b,tmp,result;
/*初始化a,b*/
tmp=a*b;
if(a!=0&&tmp/a!=b){
//
}else{
result=tmp;
}
检测移位时丢失有效位
2.6 其它可能出现运行时错误的地方
运行时错误检查是c 程序员需要加以特别的注意的,这是因为c语言在提供任何运行时检测方面能力较弱。对于要求可靠性较高的软件来说,动态检测是必需的。因此c 程序员需要谨慎考虑的问题是,在任何可能出现运行时错误的地方增加代码的动态检测。大多数的动态检测与应用紧密相关,在程序设计过程中要根据系统需求设置动态代码检测。
3.容错
1980年,美苏尚处于冷战阶段。这年,北美防空联合司令部曾报告称美国遭受导弹袭击。后来证实,这是反馈系统电路故障问题,但反馈系统软件没有考虑故障问题引发的误报。
3.1 关键数据多区备份,取数据采用“表决法”
ram中的数据在受到干扰情况下有可能被改变,对于系统关键数据必须进行保护。关键数据包括全局变量、静态变量以及需要保护的数据区域。数据备份与原数据不应该处于相邻位置,因此不应由编译器默认分配备份数据位置,而应该由程序员指定区域存储。可以将ram分为3个区域,第一个区域保存原码,第二个区域保存反码,第三个区域保存异或码,区域之间预留一定量的“空白”ram作为隔离。可以使用编译器的“分散加载”机制将变量分别存储在这些区域。需要进行读取时,同时读出3份数据并进行表决,取至少有两个相同的那个值。
3.2 非易失性存储器的数据存储
非易失性存储器包括但不限于flash、eeprom、铁电。仅仅将写入非易失性存储器中的数据再读出校验是不够的。强干扰情况下可能导致非易失性存储器内的数据错误,在写非易失性存储器的期间系统掉电将导致数据丢失,因干扰导致程序跑飞到写非易失性存储器函数中,将导致数据存储紊乱。一种可靠的办法是将非易失性存储器分成多个区,每个数据都将按照不同的形式写入到这些分区中,需要进行读取时,同时读出多份数据并进行表决,取相同数目较多的那个值。
对于因干扰导致程序跑飞到写非易失性存储器函数,还应该配合软件锁以及严格的入口检验,单单依靠写数据到多个区是不够的也是不明智的,应该在源头进行阻截。
3.3 软件锁
软件锁可以实现但不局限于环环相扣。对于初始化序列或者有一定先后顺序的函数调用,为了保证调用顺序或者确保每个函数都被调用,我们可以使用环环相扣,实质上这也是一种软件锁。此外对于一些安全关键代码语句(是语句,而不是函数),可以给它们设置软件锁,只有持有特定钥匙的,才可以访问这些关键代码。比如,向flash写一个数据,我们会判断数据是否合法、写入的地址是否合法,计算要写入的扇区。之后调用写flash子程序,在这个子程序中,判断扇区地址是否合法、数据长度是否合法,之后就要将数据写入flash。由于写flash语句是安全关键代码,所以程序给这些语句上锁:必须具有正确的钥匙才可以写flash。这样即使是程序跑飞到写flash子程序,也能大大降低误写的风险。
3.4 通信数据的检错
通讯线上的数据误码相对严重,通讯线越长,所处的环境越恶劣,误码会越严重。通讯数据除了传统的硬件奇偶校验外,还应该增加软件crc校验。超过16字节的数据应至少使用crc16。在通讯过程中,如果检测到发生了数据错误,则要求重新发送当前帧数据。
3.5 开关量输入的检测、确认
开关量容易受到尖脉冲干扰,如果不进行滤除,可能会造成误动作。一般情况下,需要对开关量输入信号进行多次采样,并进行逻辑判断直到确认信号无误为止。多次采样之间需要有一定时间间隔,具体跟开关量的最大切换频率有关,一般不小于1ms。
3.6 开关量输出
开关信号简单的一次输出是不安全的,干扰信号可能会翻转开关量输出的状态。采取重复刷新输出可以有效防止电平的翻转。
3.7 初始化信息的保存与恢复
微处理器的寄存器值也可能会因外界干扰而改变,外设初始化值需要在寄存器中长期保存,最容易被破坏。由于flash中的数据相对不易被破坏,可以将初始化信息预先写入flash,待程序空闲时比较与初始化相关的寄存器值是否被更改,如果发现非法更改则使用flash中的值进行恢复。
3.8 陷阱
对于8051内核单片机,由于没有相应的硬件支持,可以用纯软件设置软件陷阱,用来拦截一些程序跑飞。对于arm7或者cortex-m系列单片机,硬件已经内建了多种异常,软件需要根据硬件异常来编写陷阱程序,用来快速定位甚至恢复错误。
3.9 while循环
有时候程序员会使用while(!flag);语句来等待标志flag改变,比如串口发送时用来等待一字节数据发送完成。这样的代码时存在风险的,如果因为某些原因标志位一直不改变则会造成系统死机。良好冗余的程序是设置一个超时定时器,超过一定时间后,强制程序退出while循环。
2003年8月11日发生的w32.blaster.worm蠕虫事件导致全球经济损失高达5亿美元,这个漏洞是利用了windows分布式组件对象模型的远程过程调用接口中的一个逻辑缺陷:在调用getmachinename()函数时,循环只设置了一个不充分的结束条件。
原代码简化如下所示:
hresultgetmachinename(wchar*pwszpath,
wcharwszmachinename[max_computtername_length_fqdn+1])
{
wchar*pwszservername=wszmachinename;
wchar*pwsztemp=pwszpath+2;
while(*pwsztemp!=l’\\’)/*这句代码循环结束条件不充分*/
*pwszservername++=*pwsztemp++;
/*…*/
}
微软发布的安全补丁ms03-026解决了这个问题,为getmachinename()函数设置了充分终止条件。一个解决代码简化如下所示(并非微软补丁代码):
hresultgetmachinename(wchar*pwszpath,
wcharwszmachinename[max_computtername_length_fqdn+1])
{
wchar*pwszservername=wszmachinename;
wchar*pwsztemp=pwszpath+2;
wchar*end_addr=pwszservername+max_computtername_length_fqdn;
while((*pwsztemp!=l’\\’)&&(*pwsztemp!=l’\0’)
&&(pwszservername *pwszservername++=*pwsztemp++;
/*…*/
}
3.10 系统自检
对cpu、ram、flash、外部掉电保存存储器以及其他线路自检。

采用瑞萨单片机交流感应电机控制电路实现 交流感应电机系统的原理
关于LLC工作原理的介绍和应用
浙江移动战队逆流而上 智能机器人助力缓解疫情排查压力
采用FPGA和DSP技术相结合实现图像采集和处理系统的设计
用Raspberry Pi和SensorMedal制作IoT跳绳设备:用跳绳测试仪解决运动不足问题!
嵌入式软件可靠性设计三方面的考虑:防错,判错,容错的详细概述
远程KVM切换器是如何控制数据中心设备的
智能魔镜显示屏的作用是什么,为你带来一场家居科技秀
森海塞尔首款家庭影院音响系统在中国市场亮相,售价19999元
巨量转移技术最新动态:滚轮转印、流体自组装、磁动力
芯片剪切力测试推拉力测试机知识分享
vivoZ3评测 足够主流的千元性价比旗舰
交换机的常用配置命令
有源滤波器和无源滤波器的介绍
RF功率放大器的带宽越来越宽、 功率越来越高
中国(上海)机器视觉展VisionChina2021|智慧之眼,看可期未来
智能科技高速发展,欧菲光发力新赛道打开新的成长空间
教你们怎么去选择一个好的物联网平台
立体车库中的光电开关
全球融资租赁从四点对航空产业有着巨大的推动作用