收藏 分享(赏)

嵌入式软件C语言可靠性设计 问题汇总.doc

上传人:weiwoduzun 文档编号:2777687 上传时间:2018-09-27 格式:DOC 页数:30 大小:90.38KB
下载 相关 举报
嵌入式软件C语言可靠性设计 问题汇总.doc_第1页
第1页 / 共30页
嵌入式软件C语言可靠性设计 问题汇总.doc_第2页
第2页 / 共30页
嵌入式软件C语言可靠性设计 问题汇总.doc_第3页
第3页 / 共30页
嵌入式软件C语言可靠性设计 问题汇总.doc_第4页
第4页 / 共30页
嵌入式软件C语言可靠性设计 问题汇总.doc_第5页
第5页 / 共30页
点击查看更多>>
资源描述

1、嵌入式软件可靠性设计 问题集锦目 录1、程序员理解错误 21.1、英文标点被误写成中文标点; .21.2、+=与=+、-=与=-容易混 .21.3、程序员输入错误 .21.4、数组问题 21.5、 switchcase 语句中的 break 关键字 .31.5、变量赋值 31.6、指针的加减运算 .41.7、增量运算符+和减量运算符- .52、编译器语义检查 62.1、数据类型问题 .62.2、误加标点符号 .62.3、编译器忽略掉多余的空格符和换行符 .62.4、数组越界。 72.5、数组声明具有外部链接时大小应显式声明 .72.6、编译器检查不出数组越界 82.7、编译器与 volatil

2、e 限定符 92.8、 定义为 volatile 的变量的作用过程 .112.9、局部变量必须显式初始化 113、不合理的优先级 123.1、常规使用可能引起误会的运算符 134、隐式转换和强制转换 134.1、有符号和无符号 char 和 short 类型自动转换 134.2、混合数据类型运算中会转换成较高级别数据类型 144.3、赋值语句计算结果被转换成被赋予值的变量类型 154.4、作为函数参数被传递时的数据类型转换 154.5、 C 语言强制类型转换规则 .154.6、通用编程建议 .155、判错 155.1、具有形参的函数,需判断传递来的实参是否合法。 165.2、仔细检查函数的返回

3、值 175.3、防止指针越界 .175.4、防止数组越界 .175.5、数学运算 .185.6、其它可能出现运行时错误的地方 206、容错 206.1、关键数据多区备份,取数据采用“表决法” .206.2、非易失性存储器的数据存储 216.3、软件锁 .216.4、通信数据的检错 .216.5、开关量输入的检测、确认 226.6、开关量输出 .226.7、初始化信息的保存与恢复 226.8、陷阱 .226.9、 while 循环 226.10、系统自检 .221、程序员理解错误1.1、英文标点被误写成中文标点;比较运算符=误写成赋值运算符=,代码 if(x=5) 本意是比较变量 x 是否等于常

4、量 5,但是误将=写成了=,if 语句恒为真。如果在逻辑判断表达式中出现赋值运算符,现在的大多数编译器会给出警告信息。并非所有程序员都会注意到这类警告,因此有经验的程序员使用下面的代码来避免此类错误:if(5=x) 将常量放在变量 x 的左边,即使程序员误将=写成了=,编译器会产生一个任谁也不能无视的语法错误信息:不可给常量赋值!1.2、+=与=+、-=与=-容易混复合运算符会给程序带来隐含 Bug,如下所示代码:tmp=+1;该代码本意是想表达 tmp=tmp+1,但是将复合赋值运算符+=误写成=+:将正整数常量1 赋值给变量 tmp。编译器会欣然接受这类代码,连警告都不会产生。-=与=-同

5、理。类似的逻辑与 break; case THING2: 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 未初始化*/ 1.5、变量赋值int a=34int b=034变量 a 和 b 相等吗?No。以0x为前缀的 16 进制常量,10 进

6、制常量不需要前缀,数字0为前缀的 8进制。误用 8 进制的例子,最后一个数组元素赋值错误:1. a0=106; /*十进制数 106*/ 2. a1=112; /*十进制数 112*/ 3. a2=052; /*实际为十进制数 42,本意为十进制 52*/ 1.6、指针的加减运算下面的代码运行在 32 位 ARM 架构上,执行后,a 和 p 的值?int a=1; int *p=(int*)0x00001000; a=a+1; p=p+1; a=2,但是 p 的结果是 0x00001004。指针 p 加 1 后,p 的值增加了 4。原因是指针做加减运算时是以指针的数据类型为单位。p+1 实际上

7、是 p+1*sizeof(int)。不理解这一点,在使用指针直接操作数据时极易犯错。比如下面对连续 RAM 初始化零操作代码:unsigned int *pRAMaddr; /定义地址指针变量 for(pRAMaddr=StartAddr;pRAMaddr=0;i-) 无符号 char 类型,范围为 0255,所以无符号 char 类型变量 i 永远小于 256(第一个for 循环无限执行),永远大于等于 0(第二个 for 循环无限执行)。2.2、误加标点符号1. if(ab); /这里误加了一个分号 2. a=b; /这句代码一直被执行 2.3、编译器忽略掉多余的空格符和换行符1. if(

8、n=3 时,表达式 logrec.data=x0;就不会被执行,给程序埋下了隐患。2.4、数组越界。代码在硬件上运行,一段时间后 LCD 显示屏上的一个数字不正常的被改变。经过一段时间的调试,问题被定位到下面的一段代码中:int SensorData30; for(i=30;i0;i-) SensorDatai=.; 这里声明了拥有 30 个元素的数组,不幸的是 for 循环代码中误用了本不存在的数组元素 SensorData30。按照代码改变了数组元素 SensorData30所在位置的值, SensorData30所在的位置原本是一个 LCD 显示变量。很多编译器会对上述代码产生警告:赋值

9、超出数组界限。但并非所有程序员都对编译器警告保持足够敏感,而且编译器也并不能检查出数组越界的所有情况。2.5、数组声明具有外部链接时大小应显式声明模块 A 中定义数组:int SensorData30;在模块 B 中引用该数组,但由于你引用代码并不规范,这里没有显式声明数组大小,但编译器也允许这么做:Comment w1: extern int SensorData;如果在模块 B中存在和上面一样的代码:for(i=30;i0;i-) SensorDatai=; 这次,编译器不会给出警告信息,因为编译器压根就不知道数组的元素个数。所以,当一个数组声明为具有外部链接,它的大小应该显式声明。2.6

10、、编译器检查不出数组越界函数 func()的形参是一个数组形式,函数代码简化如下所示:char * func(char SensorData30) unsigned int i; for(i=30;i0;i-) SensorDatai=; 这个给 SensorData30赋初值的语句,编译器也是不给任何警告的。实际上,编译器是将数组名 SensorData隐含的转化为指向数组第一个元素的指针,函数体是使用指针的形式来访问数组的,它当然也不会知道数组元素的个数了。造成这种局面的原因之一是 C 编译器的作者们认为指针代替数组可以提高程序效率,而且,还可以简化编译器的复杂度。指针和数组容易给程序造成

11、混乱,如何区分?可以将数组名等同于指针的情况有且只有一处,就是数组作为函数形参时。其它时候,数组名是数组名,指针是指针。另一个例子:编译器同样检查不出数组越界。用数组来缓存通讯中的一帧数据。在通讯中断中将接收的数据保存到数组中,直到一帧数据完全接收后再进行处理。即使定义的数组长度足够长,接收数据的过程中也可能发生数组越界,特别是干扰严重时。这是由于外界的干扰破坏了数据帧的某些位,对一帧的数据长度判断错误,接收的数据超出数组范围,多余的数据会改写数组相邻的变量,造成系统崩溃。由于中断事件的异步性,这类数组越界编译器无法检查到。如果局部数组越界,可能引发 ARM 架构硬件异常。一设备用于接收无线传

12、感器的数据,一次升级后,发现接收设备工作一段时间后会死机。调试表明 ARM7 处理器发生了硬件异常,异常处理代码是一段死循环(死机的直接原因)。接收设备有一个硬件模块用于接收无线传感器的整包数据并存在自己的硬件缓冲区中,当一帧数据接收完成后,使用外部中断通知设备取数据,外部中断服务程序精简后如下所示:_irq ExintHandler(void) Unsigned char DataBuf50; GetData(DataBug); /从硬件缓冲区取一帧数据 由于存在多个无线传感器近乎同时发送数据的可能,加之 GetData()函数保护力度不够,数组 DataBuf 在取数据过程中发生越界。由于

13、数组 DataBuf 为局部变量,被分配在堆栈中,同在此堆栈中的还有中断发生时的运行环境以及中断返回地址。溢出的数据将这些数据破坏掉,中断返回时 PC 指针可能变成一个不合法值,硬件异常由此产生。但如果被利用,则可作病毒。1988 年,第一个网络蠕虫在一天之内感染了 2000 到6000 台计算机,利用的正是一个标准输入库函数的数组越界 Bug。起因是一个标准输入输出库函数 gets(),原来设计为从数据流中获取一段文本,遗憾的是,gets()函数没有规定输入文本的长度。gets()函数内部定义了一个 500 字节的数组,攻击者发送了大于 500 字节的数据,利用溢出的数据修改了堆栈中的 PC

14、 指针,从而获取了系统权限。2.7、编译器与 volatile 限定符源文件定义变量 unsigned int a; volatile unsigned int a;头文件声明变量 extern unsigned long a; extern unsigned int a;编译器提示语法错误:变量a声明类型不一致编译器:不给错误信息(或仅一条警告)volatile 属于类型限定符,另一个常见的类型限定符是 const 关键字。限定符volatile 在嵌入式软件中至关重要,用来告诉编译器不要优化它修饰的变量。模块 A 源文件中定义变量:volatile unsigned int TimerCo

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

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

17、新读取这个变量的值,而不是使用保存在寄存器里的备份。下面左侧函数有什么问题:代码目的是用来返指针*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 *

18、a;2.9、局部变量必须显式初始化 例 1:unsigned intGetTempValue(void) unsigned int sum; /定义局部变量,保存总值。但其初值并不一定为 0for(i=0;imsb 4; Comment w2: 如果不了解表达式里的类型提升,认为在运算过程中变量 port一直是 unsigned char类型的。期望的运算过程:port 结果为 0xa5,0xa54 结果为 0x0a。实际上,result_8 的结果却是 0xfa。在 ARM结构下,int 类型为 32位。变量 port在运算前被提升为 int类型:port 结果为 0xffffffa5,0x

19、a54 结果为 0x0ffffffa,赋值给变量 result_8,发生类型截断(隐性转换),result_8=0xfa。正确表达式语句应该为:result_8=(unsigned char) (port) 4; /*强制转换*/4.2、混合数据类型运算中会转换成较高级别数据类型数据类型的级别从高到低的顺序:long double、double、float、unsigned long long、long long、unsigned long、long、unsigned int、int。这种类型提升通常都是件好事,但往往有很多程序员不能真正理解这句话,从而做一些想当然的事情,比如下面的例子,in

20、t 类型表示 16位。uint16_t u16a = 40000; /* 16位无符号变量*/ uint16_t u16b = 30000; /*16位无符号变量*/ uint32_t u32x; /*32位无符号变量 */ uint32_t u32y; u32x = u16a + u16b; /* u32x = 70000还是 4464 ? */ u32y =(uint32_t)(u16a + u16b); /* u32y=70000还是 4464? */ u32x和 u32y的结果都是 4464(16 位值范围 0-65536,超出值域范围部分 4464)。不要认为表达式中有一个高类别 u

21、int32_t类型变量,编译器都会帮你把所有其他低类别都提升到 uint32_t类型。正确的书写方式:u32x = (uint32_t)u16a +(uint32_t)u16b;或者: u32x = (uint32_t)u16a + u16b; 后一种写法在本表达式中是正确的,但是在其它表达式中不一定正确,比如:uint16_t u16a,u16b,u16c; uint32_t u32x; u32x = u16a + u16b + (uint32_t)u16c; /*错误写法,u16a+u16b 仍可能溢出*/4.3、赋值语句计算结果被转换成被赋予值的变量类型这一过程可能导致类型提升、也可能导

22、致类型降级。降级可能会导致问题。比如将运算结果为 321 的值赋值给 8 位 char 类型变量。程序必须对运算时的数据溢出做合理的处理。4.4、作为函数参数被传递时的数据类型转换char 和 short 会被转换为 int,float 会被转换为 double。4.5、 C 语言强制类型转换规则 并非所有强制类型转换都是有风险的,把一个整数值转换为一种具有相同符号的更宽类型时,是绝对安全的。 精度高的类型强制转换为精度低的类型时,通过丢弃适当数量的最高有效位来获取结果,也就是说会发生数据截断,并且可能改变数据的符号位。 精度低的类型强制转换为精度高的类型时,如果两种类型具有相同的符号,那么没

23、什么问题;需要注意的是负的有符号精度低类型强制转换为无符号精度高类型时,会不直观的执行符号扩展,例如:unsigned int bob; signed char fred = -1; bob=(unsigned int )fred; /*发生符号扩展,此时 bob 为 0xFFFFFFFF*/ 4.6、通用编程建议 打开编译器所有警告开关/重视所有警告 使用静态分析工具分析代码 安全的读写数据(检查所有数组边界) 检查指针的合法性 检查函数入口参数合法性 检查所有返回值 在声明变量位置初始化所有变量 合理的使用括号 谨慎的进行强制转换 使用好的诊断信息日志和工具5、判错编写或移植一个类似 C

24、标准库中的 printf 函数,可以格式化打印字符、字符串、十进制整数、十六进制整数。这里称为 UARTprintf()。unsigned int WriteData(unsigned int addr) if(addr= BASE_ADDR) /*初始化 sl1 和 sl2*/ if(sl2=0)|(sl1=LONG_MIN) 加法溢出检测a)无符号加法#include unsigned int a,b,result; /*初始化 a,b*/ if(UINT_MAX-a signed int a,b,result; /*初始化 a,b */ if(a0 乘法溢出检测a)无符号乘法#inclu

25、de unsigned int a,b,result; /*初始化 a,b*/ if(a!=0) /*初始化 a,b*/ tmp=a * b; if(a!=0 检测移位时丢失有效位5.6、其它可能出现运行时错误的地方C 语言在提供任何运行时检测方面能力较弱。要求可靠性较高的软件来说,动态检测是必需的。因此 C 程序员需要谨慎考虑的问题是,在任何可能出现运行时错误的地方增加代码的动态检测。大多数的动态检测与应用紧密相关,在程序设计过程中要根据系统需求设置动态代码检测。6、容错6.1、关键数据多区备份,取数据采用“表决法”RAM 中的数据在受到干扰情况下有可能被改变,对于系统关键数据必须进行保护。

26、关键数据包括全局变量、静态变量以及需要保护的数据区域。数据备份与原数据不应该处于相邻位置,因此不应由编译器默认分配备份数据位置,而应该由程序员指定区域存储。可以将 RAM 分为 3 个区域,第一个区域保存原码,第二个区域保存反码,第三个区域保存异或码,区域之间预留一定量的“空白”RAM 作为隔离。可以使用编译器的“分散加载”机制将变量分别存储在这些区域。需要进行读取时,同时读出 3 份数据并进行表决,取至少有两个相同的那个值。6.2、非易失性存储器的数据存储非易失性存储器包括但不限于 Flash、EEPROM、铁电。仅仅将写入非易失性存储器中的数据再读出校验是不够的。强干扰情况下可能导致非易失

27、性存储器内的数据错误,在写非易失性存储器的期间系统掉电将导致数据丢失,因干扰导致程序跑飞到写非易失性存储器函数中,将导致数据存储紊乱。一种可靠的办法是将非易失性存储器分成多个区,每个数据都将按照不同的形式写入到这些分区中,需要进行读取时,同时读出多份数据并进行表决,取相同数目较多的那个值。对于因干扰导致程序跑飞到写非易失性存储器函数,还应该配合软件锁以及严格的入口检验,单单依靠写数据到多个区是不够的也是不明智的,应该在源头进行阻截。6.3、软件锁软件锁可以实现但不局限于环环相扣。对于初始化序列或者有一定先后顺序的函数调用,为了保证调用顺序或者确保每个函数都被调用,我们可以使用环环相扣,实质上这

28、也是一种软件锁。此外对于一些安全关键代码语句(是语句,而不是函数),可以给它们设置软件锁,只有持有特定钥匙的,才可以访问这些关键代码。比如,向 Flash 写一个数据,我们会判断数据是否合法、写入的地址是否合法,计算要写入的扇区。之后调用写 Flash 子程序,在这个子程序中,判断扇区地址是否合法、数据长度是否合法,之后就要将数据写入 Flash。由于写 Flash 语句是安全关键代码,所以程序给这些语句上锁:必须具有正确的钥匙才可以写 Flash。这样即使是程序跑飞到写Flash 子程序,也能大大降低误写的风险。6.4、通信数据的检错通讯线上的数据误码相对严重,通讯线越长,所处的环境越恶劣,

29、误码会越严重。通讯数据除了传统的硬件奇偶校验外,还应该增加软件 CRC 校验。超过 16 字节的数据应至少使用 CRC16。在通讯过程中,如果检测到发生了数据错误,则要求重新发送当前帧数据。6.5、开关量输入的检测、确认开关量容易受到尖脉冲干扰,如果不进行滤除,可能会造成误动作。一般情况下,需要对开关量输入信号进行多次采样,并进行逻辑判断直到确认信号无误为止。多次采样之间需要有一定时间间隔,具体跟开关量的最大切换频率有关,一般不小于 1ms。6.6、开关量输出开关信号简单的一次输出是不安全的,干扰信号可能会翻转开关量输出的状态。采取重复刷新输出可以有效防止电平的翻转。6.7、初始化信息的保存与

30、恢复微处理器的寄存器值也可能会因外界干扰而改变,外设初始化值需要在寄存器中长期保存,最容易被破坏。由于 Flash 中的数据相对不易被破坏,可以将初始化信息预先写入Flash,待程序空闲时比较与初始化相关的寄存器值是否被更改,如果发现非法更改则使用Flash 中的值进行恢复。6.8、陷阱对于 8051 内核单片机,由于没有相应的硬件支持,可以用纯软件设置软件陷阱,用来拦截一些程序跑飞。对于 ARM7 或者 Cortex-M 系列单片机,硬件已经内建了多种异常,软件需要根据硬件异常来编写陷阱程序,用来快速定位甚至恢复错误。6.9、 while 循环有时候程序员会使用 while(!flag);语句来等待标志 flag 改变,比如串口发送时用来等待一字节数据发送完成。这样的代码时存在风险的,如果因为某些原因标志位一直不改变则会造成系统死机。良好冗余的程序是设置一个超时定时器,超过一定时间后,强制程序退出 while 循环。6.10、系统自检对 CPU、RAM、Flash、外部掉电保存存储器以及其他线路自检。

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 企业管理 > 管理学资料

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报