4.防御性编程
嵌入式产品的可靠性自然与硬件密不可分,但在硬件确定、并且没有第三方测试的前提下,使用防御性编程思想写出的代码,往往具有更高的稳定性。
防御性编程首先需要认清C语言的种种缺陷和陷阱,C语言对于运行时的检查十分弱小,需要程序员谨慎的考虑代码,在必要的时候增加判断;防御性编程的另一个核心思想是假设代码运行在并不可靠的硬件上,外接干扰有可能会打乱程序执行顺序、更改RAM存储数据等等。
4.1具有形参的函数,需判断传递来的实参是否合法。
程序员可能无意识的传递了错误参数;外界的强干扰可能将传递的参数修改掉,或者使用随机参数意外的调用函数,因此在执行函数主体前,需要先确定实参是否合法。
1. int exam_fun( unsigned char *str )
2. {
3. if( str != NULL ) // 检查“假设指针不为空”这个条件
4. {
5. //正常处理代码
6. }
7. else
8. {
9. //处理错误代码
10. }
11. }
4.2仔细检查函数的返回值
对函数返回的错误码,要进行全面仔细处理,必要时做错误记录。
1. char *DoSomething(…)
2. {
3. char * p;
4. p=malloc(1024);
5. if(p==NULL) /*对函数返回值作出判断*/
6. {
7. UARTprintf(…); /*打印错误信息*/
8. return NULL;
9. }
10. retuen p;
11. }
4.3 防止指针越界
如果动态计算一个地址时,要保证被计算的地址是合理的并指向某个有意义的地方。特别对于指向一个结构或数组的内部的指针,当指针增加或者改变后仍然指向同一个结构或数组。
4.4 防止数组越界
数组越界的问题前文已经讲述的很多了,由于C不会对数组进行有效的检测,因此必须在应用中显式的检测数组越界问题。下面的例子可用于中断接收通讯数据。
1. #define REC_BUF_LEN 100
2. unsigned char RecBuf[REC_BUF_LEN];
3. //其它代码
4. void Uart_IRQHandler(void)
5. {
6. static RecCount=0; //接收数据长度计数器
7. //其它代码
8. if(RecCount< REC_BUF_LEN) //判断数组是否越界
9. {
10. RecBuf[RecCount]=…; //从硬件取数据
11. RecCount++;
12. //其它代码
13. }
14. else
15. {
16. //错误处理代码
17. }
18. //其它代码
19. }
在使用一些库函数时,同样需要对边界进行检查,比如下面的memset(RecBuf,0,len)函数把RecBuf指指向的内存区的前len个字节用0填充,如果不注意len的长度,就会将数组RecBuf之外的内存区清零:
1. #define REC_BUF_LEN 100
2. unsigned char RecBuf[REC_BUF_LEN];
3.
4. if(len< REC_BUF_LEN)
5. {
6. memset(RecBuf,0,len); //将数组RecBuf清零
7. }
8. else
9. {
10. //处理错误
11. }
4.5 数学算数运算
4.5.1除法运算,只检测除数为零就可靠吗?
除法运算前,检查除数是否为零几乎已经成为共识,但是仅检查除数是否为零就够了吗?
考虑两个整数相除,对于一个signed long类型变量,它能表示的数值范围为:-2147483648 ~+2147483647,如果让-2147483648/ -1,那么结果应该是+2147483648,但是这个结果已经超出了signedlong所能表示的范围了。所以,在这种情况下,除了要检测除数是否为零外,还要检测除法是否溢出。
1. #include
2. signed long sl1,sl2,result;
3. /*初始化sl1和sl2*/
4. if((sl2==0)||(sl1==LONG_MIN && sl2==-1))
5. {
6. //处理错误
7. }
8. else
9. {
10. result = sl1 / sl2;
11. }
4.5.2检测运算溢出
整数的加减乘运算都有可能发生溢出,在讨论未定义行为时,给出过一个有符号整形加法溢出判断代码,这里再给出一个无符号整形加法溢出判断代码段:
1. #include
2. unsigned int a,b,result;
3. /*初始化a,b*/
4. if(UINT_MAX-a
5. {
6. //处理溢出
7. }
8. else
9. {
10. result=a+b;
11. }
嵌入式硬件一般没有浮点处理器,浮点数运算在嵌入式也比较少见并且溢出判断严重依赖C库支持,这里不讨论。
4.5.3检测移位
在讨论未定义行为时,提到有符号数右移、移位的数量是负值或者大于操作数的位数都是未定义行为,也提到不对有符号数进行位操作,但要检测移位的数量是否大于操作数的位数。下面给出一个无符号整数左移检测代码段:
1. unsigned int ui1;
2. unsigned int ui2;
3. unsigned int uresult;
4.
5. /*初始化ui1,ui2*/
6. if(ui2>=sizeof(unsigned int)*CHAR_BIT)
7. {
8. //处理错误
9. }
10. else
11. {
12. uresult=ui1<
13. }
4.6如果有硬件看门狗,则使用它
在其它一切措施都失效的情况下,看门狗可能是最后的防线。它的原理特别简单,但却能大大提高设备的可靠性。如果设备有硬件看门狗,一定要为它编写驱动程序。
要尽可能早的开启看门狗
这是因为从上电复位结束到开启看门狗的这段时间内,设备有可能被干扰而跳过看门狗初始化程序,导致看门狗失效。尽可能早的开启看门狗,可以降低这种概率;
不要在中断中喂狗,除非有其他联动措施
在中断程序喂狗,由于干扰的存在,程序可能一直处于中断之中,这样会导致看门狗失效。如果在主程序中设置标志位,中断程序喂狗时与这个标志位联合判断,也是允许的;
喂狗间隔跟产品需求有关,并非特定的时间
产品的特性决定了喂狗间隔。对于不涉及安全性、实时性的设备,喂狗间隔比较宽松,但间隔时间不宜过长,否则被用户感知到,是影响用户体验的。对于设计安全性、有实时控制类的设备,原则是尽可能快的复位,否则会造成事故。
克莱门汀号在进行第二阶段的任务时,原本预订要从月球飞行到太空深处的Geographos小行星进行探勘,然而这艘太空探测器在飞向小行星时却由于一个软件缺陷而使其中断运作20分钟,不但未能到达小行星,也因为控制喷嘴燃烧了11分钟使电力供应降低,无法再透过远端控制探测器,最终结束这项任务,但也导致了资源与资金的浪费。
“克莱门汀太空任务失败这件事让我感到十分震惊,它其实可以透过硬件中一款简单的看门狗计时器避免掉这项意外,但由于当时的开发时间相当紧缩,程序设计人员没时间编写程序来启动它,”Ganssle说。
遗憾的是,1998年发射的近地号太空船(NEAR)也遇到了相同的问题。由于编程人员并未采纳建议,因此,当推进器减速器系统故障时,29公斤的储备燃料也随之报销──这同样是一个本来可经由看门狗定时器编程而避免的问题,同时也证明要从其他程序设计人员的错误中学习并不容易。
4.7关键数据储存多个备份,取数据采用“表决法”
RAM中的数据在受到干扰情况下有可能被改变,对于系统关键数据应该进行保护。关键数据包括全局变量、静态变量以及需要保护的数据区域。备份数据与原数据不应该处于相邻位置,因此不应由编译器默认分配备份数据位置,而应该由程序员指定区域存储。
可以将RAM分为3个区域,第一个区域保存原码,第二个区域保存反码,第三个区域保存异或码,区域之间预留一定量的“空白”RAM作为隔离。可以使用编译器的“分散加载”机制将变量分别存储在这些区域。需要进行读取时,同时读出3份数据并进行表决,取至少有两个相同的那个值。
假如设备的RAM从0x1000_0000开始,我需要在RAM的0x1000_0000~0x10007FFF内存储原码,在0x1000_9000~0x10009FFF内存储反码,在0x1000_B000~0x1000BFFF内存储0xAA的异或码,编译器的分散加载可以设置为:
1. LR_IROM1 0x00000000 0x00080000 { ; load region size_region
2. ER_IROM1 0x00000000 0x00080000 { ; load address = execution address
3. *.o (RESET, +First)
4. *(InRoot$$Sections)
5. .ANY (+RO)
6. }
7. RW_IRAM1 0x10000000 0x00008000 { ;保存原码
8. .ANY (+RW +ZI )
9. }
10.
11. RW_IRAM3 0x10009000 0x00001000{ ;保存反码
12. .ANY (MY_BK1)
13. }
14.
15. RW_IRAM2 0x1000B000 0x00001000 { ;保存异或码
16. .ANY (MY_BK2)
17. }
18. }
如果一个关键变量需要多处备份,可以按照下面方式定义变量,将三个变量分别指定到三个不连续的RAM区中,并在定义时按照原码、反码、0xAA的异或码进行初始化。
1. uint32 plc_pc=0; //原码
2. __attribute__((section("MY_BK1"))) uint32 plc_pc_not=~0x0; //反码
3. __attribute__((section("MY_BK2"))) uint32 plc_pc_xor=0x0^0xAAAAAAAA; //异或码
当需要写这个变量时,这三个位置都要更新;读取变量时,读取三个值做判断,取至少有两个相同的那个值。
为什么选取异或码而不是补码?这是因为MDK的整数是按照补码存储的,正数的补码与原码相同,在这种情况下,原码和补码是一致的,不但起不到冗余作用,反而对可靠性有害。比如存储的一个非零整数区因为干扰,RAM都被清零,由于原码和补码一致,按照3取2的“表决法”,会将干扰值0当做正确的数据。
4.8对非易失性存储器进行备份存储
非易失性存储器包括但不限于Flash、EEPROM、铁电。仅仅将写入非易失性存储器中的数据再读出校验是不够的。强干扰情况下可能导致非易失性存储器内的数据错误,在写非易失性存储器的期间系统掉电将导致数据丢失,因干扰导致程序跑飞到写非易失性存储器函数中,将导致数据存储紊乱。
一种可靠的办法是将非易失性存储器分成多个区,每个数据都将按照不同的形式写入到这些分区中,需要进行读取时,同时读出多份数据并进行表决,取相同数目较多的那个值。
4.9软件锁
对于初始化序列或者有一定先后顺序的函数调用,为了保证调用顺序或者确保每个函数都被调用,我们可以使用环环相扣,实质上这也是一种软件锁。此外对于一些安全关键代码语句(是语句,而不是函数),可以给它们设置软件锁,只有持有特定钥匙的,才可以访问这些关键代码。也可以通俗的理解为,关键安全代码不能按照单一条件执行,要额外的多设置一个标志。
比如,向Flash写一个数据,我们会判断数据是否合法、写入的地址是否合法,计算要写入的扇区。之后调用写Flash子程序,在这个子程序中,判断扇区地址是否合法、数据长度是否合法,之后就要将数据写入Flash。
由于写Flash语句是安全关键代码,所以程序给这些语句上锁:必须具有正确的钥匙才可以写Flash。这样即使是程序跑飞到写Flash子程序,也能大大降低误写的风险。
1. /****************************************************************************
2. * 名称:RamToFlash()
3. * 功能:复制RAM的数据到FLASH,命令代码51。
4. * 入口参数:dst 目标地址,即FLASH起始地址。以512字节为分界
5. * src 源地址,即RAM地址。地址必须字对齐
6. * no 复制字节个数,为512/1024/4096/8192
7. * ProgStart 软件锁标志
8. * 出口参数:IAP返回值(paramout缓冲区) CMD_SUCCESS,SRC_ADDR_ERROR,DST_ADDR_ERROR,
9. SRC_ADDR_NOT_MAPPED,DST_ADDR_NOT_MAPPED,COUNT_ERROR,BUSY,未选择扇区
10. ****************************************************************************/
11. void RamToFlash(uint32 dst, uint32 src, uint32 no,uint8 ProgStart)
12. {
13. PLC_ASSERT("Sector number",(dst>=0x00040000)&&(dst<=0x0007FFFF));
14. PLC_ASSERT("Copy bytes number is 512",(no==512));
15. PLC_ASSERT("ProgStart==0xA5",(ProgStart==0xA5));
16.
17. paramin[0] = IAP_RAMTOFLASH; // 设置命令字
18. paramin[1] = dst; // 设置参数
19. paramin[2] = src;
20. paramin[3] = no;
21. paramin[4] = Fcclk/1000;
22. if(ProgStart==0xA5) //只有软件锁标志正确时,才执行关键代码
23. {
24. iap_entry(paramin, paramout); // 调用IAP服务程序
25. ProgStart=0;
文章来源于:电子工程世界 原文链接