嵌入式软件C语言可靠性设计-问题汇总(共30页).docx

上传人:飞****2 文档编号:13772250 上传时间:2022-05-01 格式:DOCX 页数:30 大小:85.99KB
返回 下载 相关 举报
嵌入式软件C语言可靠性设计-问题汇总(共30页).docx_第1页
第1页 / 共30页
嵌入式软件C语言可靠性设计-问题汇总(共30页).docx_第2页
第2页 / 共30页
点击查看更多>>
资源描述

《嵌入式软件C语言可靠性设计-问题汇总(共30页).docx》由会员分享,可在线阅读,更多相关《嵌入式软件C语言可靠性设计-问题汇总(共30页).docx(30页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。

1、精选优质文档-倾情为你奉上嵌入式软件可靠性设计 问题集锦目 录1、程序员理解错误1.1、英文标点被误写成中文标点;比较运算符=误写成赋值运算符=,代码if(x=5) 本意是比较变量x是否等于常量5,但是误将=写成了=,if语句恒为真。如果在逻辑判断表达式中出现赋值运算符,现在的大多数编译器会给出警告信息。并非所有程序员都会注意到这类警告,因此有经验的程序员使用下面的代码来避免此类错误: if(5=x) 将常量放在变量x的左边,即使程序员误将=写成了=,编译器会产生一个任谁也不能无视的语法错误信息:不可给常量赋值!1.2、+=与=+、-=与=-容易混 复合运算符会给程序带来隐含Bug,如下所示代

2、码: tmp=+1; 该代码本意是想表达tmp=tmp+1,但是将复合赋值运算符+=误写成=+:将正整数常量1赋值给变量tmp。编译器会欣然接受这类代码,连警告都不会产生。 -=与=-同理。类似的逻辑与&、位与&、逻辑或|、位或|、逻辑非!、位取反等。 字母l和数字1、字母o和数字0也易混淆,这种情况可借助编译器来纠正。1.3、程序员输入错误 阿拉伯数值输入,宏变量宏定义方式实现。防止多次录入数字出现手误。1.4、数组问题 C语言数组下标从0开始。定义int a30,是从a0 a29。1.5、switchcase语句中的break关键字 switchcase语句可以很方便的实现多分支结构。漏加

3、break,引起顺序执行多个case语句;break关键字用于跳出最近的那层循环语句或者switch语句。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_point

4、er未初始化*/1.5、变量赋值inta=34 int b=034 变量a和b相等吗? No。以0x为前缀的16进制常量,10进制常量不需要前缀,数字0为前缀的8进制。误用8进制的例子,最后一个数组元素赋值错误:1. a0=106; /*十进制数106*/2. a1=112;/*十进制数112*/3. a2=052; /*实际为十进制数42,本意为十进制52*/1.6、指针的加减运算 下面的代码运行在32位ARM架构上,执行后,a和p的值?inta=1;int*p=(int*)0x;a=a+1;p=p+1; a=2,但是p的结果是0x。指针p加1后,p的值增加了4。原因是指针做加减运算时是以指

5、针的数据类型为单位。p+1实际上是p+1*sizeof(int)。不理解这一点,在使用指针直接操作数据时极易犯错。 比如下面对连续RAM初始化零操作代码:unsignedint*pRAMaddr;/定义地址指针变量for(pRAMaddr=StartAddr;pRAMaddrEndAddr;pRAMaddr+=4)*pRAMaddr=0x;/指定RAM地址清零 由于pRAMaddr是一个指针变量,所以pRAMaddr+=4代码其实使pRAMaddr偏移了4*sizeof(int)=16个字节,所以每执行一次for循环,会使变量pRAMaddr偏移16个字节空间,但只有4字节空间被初始化为零。其

6、它的12字节数据的内容,在大多数架构处理器中都会是随机数。 对于sizeof(),这里强调两点,第一它是一个关键字,而不是函数,并且它默认返回无符号整型数据(无符号!);第二,使用sizeof获取数组长度时,不要对指针应用sizeof操作符,比如下面的例子:voidClearRAM(chararray)inti;for(i=0;isizeof(array)/sizeof(array0);i+) / array实际上是指针 /*for(i=0;isizeof(array20)/sizeof(array0);i+)*/arrayi=0x00;intmain(void)charFle20;Clear

7、RAM(Fle);/只能清除数组Fle中的前四个元素 对于数组array20,使用代码sizeof(array20)/sizeof(array0)可以获得数组的元素(这里为20),但数组名和指针容易混淆,有且只有一种情况下可以当做指针,就是数组名作为函数形参时,数组名被认为是指针。同时不能再兼任数组名。只有这种情况,数组名才可当做指针,但容易引发风险。在ClearRAM函数内,作为形参的array不再是数组名了,而成了指针。sizeof(array)相当于求指针变量占用的字节数,在32位系统下,该值为4,sizeof(array) / sizeof(array0) 的运算结果也为4。所以在ma

8、in函数中调用ClearRAM(Fle),也只能清除数组Fle中的前四个元素了。1.7、增量运算符+和减量运算符- 既可以做前缀也可以做后缀。前缀和后缀的区别在于值的增加或减少这一动作发生的时间是不同的。作为前缀是先自加或自减然后做别的运算,作为后缀时,是先做运算,之后再自加或自减。下面的例子可以很好的解释前缀和后缀的区别。1. inta=8,b=2,y;2. y=a+-b; 代码执行后,y的值是多少? y=(a+)+(-b); 当赋值给变量y时,a的值为8,b的值为1,所以变量y的值为9;赋值完成后,变量a自加,a的值变为9,千万不要以为y的值为10。分解成两条语句:1. y=a+(-b);

9、2. a=a+1;2、编译器语义检查 萝卜快了不洗泥。C语言足够灵活且几乎不进行任何运行时检查,比如数组越界、指针是否合法、运算结果是否溢出等。 C语言足够灵活,对于一个数组a30,它允许使用像a-1这样的形式来快速获取数组首元素所在地址前面的数据;2.1、数据类型问题 下面的两个例子的问题是什么? 死循环。unsigned chari;for(i=0;i=0;i-) 无符号char类型,范围为0255,所以无符号char类型变量i永远小于256(第一个for循环无限执行),永远大于等于0(第二个for循环无限执行)。2.2、误加标点符号1. if(ab);/这里误加了一个分号2. a=b;/

10、这句代码一直被执行2.3、编译器忽略掉多余的空格符和换行符1. if(n3)2. return/这里少加了一个分号3. logrec.data=x0;4. logrec.time=x1;5. logrec.code=x2; 这段代码的本意是n=3时,表达式logrec.data=x0;就不会被执行,给程序埋下了隐患。2.4、数组越界。代码在硬件上运行,一段时间后LCD显示屏上的一个数字不正常的被改变。经过一段时间的调试,问题被定位到下面的一段代码中:intSensorData30;.for(i=30;i0;i-)SensorDatai=.;. 这里声明了拥有30个元素的数组,不幸的是for循环

11、代码中误用了本不存在的数组元素SensorData30。按照代码改变了数组元素SensorData30所在位置的值, SensorData30所在的位置原本是一个LCD显示变量。 很多编译器会对上述代码产生警告:赋值超出数组界限。但并非所有程序员都对编译器警告保持足够敏感,而且编译器也并不能检查出数组越界的所有情况。2.5、数组声明具有外部链接时大小应显式声明 模块A中定义数组:intSensorData30; 在模块B中引用该数组,但由于你引用代码并不规范,这里没有显式声明数组大小,但编译器也允许这么做:extern intSensorData; 如果在模块B中存在和上面一样的代码:for(

12、i=30;i0;i-)SensorDatai=;. 这次,编译器不会给出警告信息,因为编译器压根就不知道数组的元素个数。所以,当一个数组声明为具有外部链接,它的大小应该显式声明。2.6、编译器检查不出数组越界 函数func()的形参是一个数组形式,函数代码简化如下所示:char*func(charSensorData30)unsigned inti;for(i=30;i0;i-)SensorDatai=; 这个给SensorData30赋初值的语句,编译器也是不给任何警告的。实际上,编译器是将数组名SensorData隐含的转化为指向数组第一个元素的指针,函数体是使用指针的形式来访问数组的,它

13、当然也不会知道数组元素的个数了。造成这种局面的原因之一是C编译器的作者们认为指针代替数组可以提高程序效率,而且,还可以简化编译器的复杂度。 指针和数组容易给程序造成混乱,如何区分? 可以将数组名等同于指针的情况有且只有一处,就是数组作为函数形参时。其它时候,数组名是数组名,指针是指针。 另一个例子:编译器同样检查不出数组越界。 用数组来缓存通讯中的一帧数据。在通讯中断中将接收的数据保存到数组中,直到一帧数据完全接收后再进行处理。即使定义的数组长度足够长,接收数据的过程中也可能发生数组越界,特别是干扰严重时。这是由于外界的干扰破坏了数据帧的某些位,对一帧的数据长度判断错误,接收的数据超出数组范围

14、,多余的数据会改写数组相邻的变量,造成系统崩溃。由于中断事件的异步性,这类数组越界编译器无法检查到。 如果局部数组越界,可能引发ARM架构硬件异常。一设备用于接收无线传感器的数据,一次升级后,发现接收设备工作一段时间后会死机。调试表明ARM7处理器发生了硬件异常,异常处理代码是一段死循环(死机的直接原因)。接收设备有一个硬件模块用于接收无线传感器的整包数据并存在自己的硬件缓冲区中,当一帧数据接收完成后,使用外部中断通知设备取数据,外部中断服务程序精简后如下所示:_irqExintHandler(void)Unsigned charDataBuf50;GetData(DataBug);/从硬件缓

15、冲区取一帧数据 由于存在多个无线传感器近乎同时发送数据的可能,加之GetData()函数保护力度不够,数组DataBuf在取数据过程中发生越界。由于数组DataBuf为局部变量,被分配在堆栈中,同在此堆栈中的还有中断发生时的运行环境以及中断返回地址。溢出的数据将这些数据破坏掉,中断返回时PC指针可能变成一个不合法值,硬件异常由此产生。 但如果被利用,则可作病毒。1988年,第一个网络蠕虫在一天之内感染了2000到6000台计算机,利用的正是一个标准输入库函数的数组越界Bug。起因是一个标准输入输出库函数gets(),原来设计为从数据流中获取一段文本,遗憾的是,gets()函数没有规定输入文本的

16、长度。gets()函数内部定义了一个500字节的数组,攻击者发送了大于500字节的数据,利用溢出的数据修改了堆栈中的PC指针,从而获取了系统权限。2.7、编译器与volatile限定符源文件定义变量unsigned inta; volatile unsigned inta;头文件声明变量extern unsigned longa; extern unsigned inta;编译器提示语法错误:变量a声明类型不一致编译器:不给错误信息(或仅一条警告) volatile属于类型限定符,另一个常见的类型限定符是const关键字。限定符volatile在嵌入式软件中至关重要,用来告诉编译器不要优化它修

17、饰的变量。 模块A源文件中定义变量:volatile unsigned intTimerCount=0; 该变量用来在一个定时器服务程序中进行软件计时: TimerCount+; /读取IO端口1的值 在模块A的头文件中,声明变量: extern unsigned intTimerCount; /这里漏掉了类型限定符volatile模块B中,要使用TimerCount变量进行精确的软件延时:#include“.A.h”/首先包含模块A的头文件TimerCount=0;while(TimerCount=TIMER_VALUE);/延时一段时间 这是一个死循环。 在模块B中,变量TimerCoun

18、t是被当作unsigned int类型变量。由于寄存器速度远快于RAM,编译器在使用非volatile限定变量时,先将变量从RAM中拷贝到寄存器中,如果同一个代码块再次用到该变量,就不再从RAM中拷贝数据而是直接使用之前寄存器备份值。代码while(TimerCount=TIMER_VALUE)中,变量TimerCount仅第一次执行时被使用,之后都是使用的寄存器备份值,而这个寄存器值一直为0,所以程序无限循环。下面的流程图说明了程序使用限定符volatile和不使用volatile的执行过程 使用了volatile会怎样?2.8、定义为volatile的变量的作用过程 优化器在用到这个变量时

19、必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面左侧函数有什么问题:代码目的是用来返指针*ptr指向值的平方int square(volatile int *ptr) return *ptr * *ptr;如果在两次读数据的期间,*ptr的值被改变,a和b将是不同的,则得不出a*a的结果来编译器将产生类似下面的代码int square(volatile int *ptr) int a,b;a = *ptr;b = *ptr;return a * b;正确代码:long square(volatile int *ptr) int a;a = *ptr;return a

20、* a;2.9、局部变量必须显式初始化l 例1:unsignedintGetTempValue(void)unsignedintsum;/定义局部变量,保存总值。但其初值并不一定为0for(i=0;i10;i+)sum+=CollectTemp();/函数CollectTemp可以得到当前的温度值 return(sum/10);l 例2:char*GetData(void)charbuffer100; /局部数组returnbuffer; 一旦程序执行结束离开,局部变量的作用域即被释放,所以返回指向局部变量的指针是没有实际意义的,该指针指向的区域可能会被其它程序使用,其值会被改变。3、不合理的

21、优先级 C语言有32个关键字,却有34个运算符。要记住所有运算符的优先级是困难的。不合理的#define会加重优先级问题,让问题变得更加隐蔽。#defineREADSDAIO0PIN & (111)/定义宏,读IO口p0.11的端口状态 /目的是判断端口p0.11是否为高电平if(READSDA=(111) 编译器在编译后将宏带入,原if语句变为:if(IO0PIN &(111)= (111) 运算符=的优先级是大于&的,代码IO0PIN & (111)=(111)等效为IO0PIN & 0x:判断端口P0.0是否为高电平,这与原意相差甚远。3.1、常规使用可能引起误会的运算符常被误会的优先级

22、表达式误认为真实结果取值运算符*、自增运算符+,优先级相同,但自右向左结合*p+(*p)+*(p+)成员选择运算符.高于取值运算符*p.f(*p).f*(p.f)数组下标运算符优先级,高于取值运算符*int *apint (*ap)ap为数组指针int *(ap)ap为指针数组函数()优先级高于取值运算符*int * fp()int (*fp)()fp为函数指针int * (fp()fp为函数,返回指针等于=和不等于!=运算符优先级高于位操作运算符&、 和 |val & mask != 0(val & mask)!= 0val &(mask != 0)等于=和不等于!=运算符高于赋值运算符=c

23、=getchar()!=EOF(c=getchar()!=EOFc=(getchar()!=EOF)算数运算符+和-优先级高于移位运算符msb4+lsb(msb4)+lsbmsb(4+lsb)4、隐式转换和强制转换4.1、有符号和无符号char和short类型自动转换 表达式中的有符号和无符号char和short类型都自动被转换为int类型. 在需要的情况下,将自动被转换为unsigned int(在short和int具有相同大小时)。这称为类型提升。提升在算数运算中通常不会有什么大的坏处,但如果位运算符 和 4; 如果不了解表达式里的类型提升,认为在运算过程中变量port一直是unsigne

24、d char类型的。期望的运算过程:port结果为0xa5,0xa54结果为0x0a。 实际上,result_8的结果却是0xfa。在ARM结构下,int类型为32位。变量port在运算前被提升为int类型:port结果为0xffffffa5,0xa54结果为0x0ffffffa,赋值给变量result_8,发生类型截断(隐性转换),result_8=0xfa。 正确表达式语句应该为: result_8=(unsigned char) (port) 4;/*强制转换*/4.2、混合数据类型运算中会转换成较高级别数据类型 数据类型的级别从高到低的顺序:long double、double、flo

25、at、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);/*u

26、32y=70000还是4464? */ u32x和u32y的结果都是4464(16位值范围0-65536,超出值域范围部分4464)。不要认为表达式中有一个高类别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;

27、 /*错误写法,u16a+u16b仍可能溢出*/4.3、赋值语句计算结果被转换成被赋予值的变量类型 这一过程可能导致类型提升、也可能导致类型降级。降级可能会导致问题。比如将运算结果为321的值赋值给8位char类型变量。程序必须对运算时的数据溢出做合理的处理。4.4、作为函数参数被传递时的数据类型转换 char和short会被转换为int,float会被转换为double。4.5、C语言强制类型转换规则 并非所有强制类型转换都是有风险的,把一个整数值转换为一种具有相同符号的更宽类型时,是绝对安全的。 精度高的类型强制转换为精度低的类型时,通过丢弃适当数量的最高有效位来获取结果,也就是说会发生数

28、据截断,并且可能改变数据的符号位。 精度低的类型强制转换为精度高的类型时,如果两种类型具有相同的符号,那么没什么问题;需要注意的是负的有符号精度低类型强制转换为无符号精度高类型时,会不直观的执行符号扩展,例如:unsignedintbob;signedcharfred=-1;bob=(unsignedint)fred;/*发生符号扩展,此时bob为0xFFFFFFFF*/4.6、通用编程建议 打开编译器所有警告开关/重视所有警告 使用静态分析工具分析代码 安全的读写数据(检查所有数组边界) 检查指针的合法性 检查函数入口参数合法性 检查所有返回值 在声明变量位置初始化所有变量 合理的使用括号

29、谨慎的进行强制转换 使用好的诊断信息日志和工具5、判错 编写或移植一个类似C标准库中的printf函数,可以格式化打印字符、字符串、十进制整数、十六进制整数。这里称为UARTprintf()。unsignedintWriteData(unsignedintaddr)if(addr=BASE_ADDR)&(addr=END_ADDR) /*地址合法,进行处理*/ else /*地址错误,打印错误信息*/ UARTprintf(文件%s的第%d行写数据时发生地址错误,错误地址为: 0x%xn,_FILE_,_LINE_,addr); /*错误处理代码*/ 假设UARTprintf()函数位于mai

30、n.c模块的第256行,并且WriteData()函数在读数据时传递了错误地址0x,则会执行UARTprintf()函数,打印如下所示的信息: 文件main.c的第256行写数据时发生地址错误,错误地址为:0x。 类似这样的信息会有助于程序员定位分析错误产生的根源,更快的消除Bug。5.1、具有形参的函数,需判断传递来的实参是否合法。 程序员可能无意识的传递了错误参数、外界的强干扰可能将传递的参数修改掉、或者使用随机参数意外的调用函数,因此在执行函数主体前,需要先确定实参是否合法。intexam_fun(unsignedchar*str)if(str!=NULL)/检查“假设指针不为空”这个条

31、件. /正常处理代码 elseUARTprintf();/打印错误信息 /处理错误代码5.2、仔细检查函数的返回值char*DoSomething() char*p; p=malloc(1024); if(p=NULL) /*对函数返回值作出判断*/UARTprintf();/*打印错误信息*/ returnNULL; retuenp;5.3、防止指针越界 如果动态计算一个地址时,要保证被计算的地址是合理的并指向某个有意义的地方。特别对于指向一个结构或数组的内部的指针,当指针增加或者改变后仍然指向同一个结构或数组。5.4、防止数组越界 C不会对数组进行有效的检测,因此必须在应用中显式的检测数组

32、越界问题。下面的例子可用于中断接收通讯数据。#defineREC_BUF_LEN100unsignedcharRecBufREC_BUF_LEN;/其它代码voidUart_IRQHandler(void)staticRecCount=0;/接收数据长度计数器 /其它代码if(RecCountREC_BUF_LEN) RecBufRecCount=; /从硬件取数据 RecCount+; /其它代码 elseUARTprintf();/打印错误信息/其它错误处理代码 在使用一些库函数时,同样需要对边界进行检查:#defineREC_BUF_LEN100unsignedcharRecBufREC

33、_BUF_LEN;if(lenREC_BUF_LEN) memset(RecBuf,0,len);/将数组RecBuf清零else/处理错误5.5、数学运算检测除数是否为零、检测运算溢出情况 有符号整数除法的运算检查 两个整数相除,除了检测除数是否为零外,还要检测除法是否溢出。对于一个signed long类型变量,它能表示的数值范围为:- +,如果让- / -1,那么结果应该是+ ,但是这个结果已经超出了signed long所能表示的范围了。#includesignedlongsl1,sl2,result;/*初始化sl1和sl2*/if(sl2=0)|(sl1=LONG_MIN)&(sl

34、2=-1)/处理错误 elseresult=sl1/sl2; 加法溢出检测 a)无符号加法#includeunsignedinta,b,result; /*初始化a,b*/if(UINT_MAX-ab)/处理溢出elseresult=a+b; b)有符号加法#includesignedinta,b,result;/*初始化a,b*/if(a0&INT_MAX-ab)|(ab)/处理溢出elseresult=a+b; 乘法溢出检测 a)无符号乘法#includeunsignedinta,b,result;/*初始化a,b*/if(a!=0)&(UINT_MAX/ab)/elseresult=a*

35、b; b)有符号乘法#includesignedinta,b,tmp,result;/*初始化a,b*/tmp=a*b;if(a!=0&tmp/a!=b)/elseresult=tmp; 检测移位时丢失有效位5.6、其它可能出现运行时错误的地方 C语言在提供任何运行时检测方面能力较弱。要求可靠性较高的软件来说,动态检测是必需的。因此C 程序员需要谨慎考虑的问题是,在任何可能出现运行时错误的地方增加代码的动态检测。大多数的动态检测与应用紧密相关,在程序设计过程中要根据系统需求设置动态代码检测。6、容错6.1、关键数据多区备份,取数据采用“表决法” RAM中的数据在受到干扰情况下有可能被改变,对于系统关键数据必须进行保护。关键数据包括全局变量、静态变量以及需要保护的数据区域。 数据备份与原数据不应该处于相邻位置,因此不应由编译器默认分配备份数据位置,而应该由程序员指定区域存储。 可以将RAM分为3个区域,第一个区域保存原码,第二个区域保存反码,第三个区域保存异或码,区域之间预留一定量的“空白”RAM作为隔离。可以使用编译器的“分散加载”机制将变量分别存储在这些区域。需要进行读取时,同时读出3份数据并进行表决,取至少有两个相同的那个值。6.2、非易失性存储器的数据存储 非易失性存储器包括但不限于Flash、EEPROM、铁电。仅仅将写入非易失性存储器中的数据再读出校验是不够的。强干扰

展开阅读全文
相关资源
相关搜索

当前位置:首页 > 教育专区 > 教案示例

本站为文档C TO C交易模式,本站只提供存储空间、用户上传的文档直接被用户下载,本站只是中间服务平台,本站所有文档下载所得的收益归上传人(含作者)所有。本站仅对用户上传内容的表现方式做保护处理,对上载内容本身不做任何修改或编辑。若文档所含内容侵犯了您的版权或隐私,请立即通知淘文阁网,我们立即给予删除!客服QQ:136780468 微信:18945177775 电话:18904686070

工信部备案号:黑ICP备15003705号© 2020-2023 www.taowenge.com 淘文阁