PDP商品的安全性大自然与硬体密切相关,但在硬体确认、因而没服务器端试验的大前提下,采用防御型程式设计价值观写下的标识符,常常具备更高的灵活性。
防御型程式设计具体来说须要正确认识C词汇的诸多瑕疵和圈套,C词汇对运转时的检查和极为力量强大,须要合作开发人员慎重的考量标识符,在必要性的这时候减少推论;防御型程式设计的另两个中心价值观是假定标识符运转在并不可信的硬体上,外置阻碍有可能将会被打乱程序代码次序、更动RAM储存统计数据之类。
1 具备实参的表达式,需推论传达来的std与否不合法
合作开发人员可能将潜意识的传达了严重错误模块;外间的强阻碍可能将将传达的模块修正掉,或是采用乱数模块不幸的调用表达式,因而在继续执行表达式市场主体前,须要先确认std与否不合法。
2 细细观察表达式的codice
对表达式回到的严重万萨县,要展开全面性细细处置,必要性时做严重错误历史记录。
3 避免操作符越境
如果动态计算两个地址时,要保证被计算的地址是合理的并指向某个有意义的地方。特别对指向两个结构或数组的内部的操作符,当操作符减少或是改变后仍然指向同两个结构或数组。
4 避免数组越境
数组越境的问题前文已经讲述的很多了,由于C不会对数组展开有效的检测,因而必须在应用中显式的检测数组越境问题。下面的例子可用于中断接收通讯统计数据。
在采用一些库表达式时,同样须要对边界展开检查和,比如下面的memset(RecBuf,0,len)表达式把RecBuf指指向的内存区的前len个字节用0填充,如果不注意len的长度,就会将数组RecBuf之外的内存区清零:
5 数学算数运算
5.1除法运算,只检测除数为零就可信吗?
除法运算前,检查和除数与否为零几乎已经成为共识,但是仅检查和除数与否为零就够了吗?
考量两个整数相除,对两个signed long类型变量,它能表示的数值范围为:-2147483648 ~+2147483647,如果让-2147483648/ -1,那么结果应该是+2147483648,但是这个结果已经超出了signedlong所能表示的范围了。所以,在这种情况下,除了要检测除数与否为零外,还要检测除法与否溢出。
#include <limits.h> signed long sl1,sl2,result; /*初始化sl1和sl2*/ if((sl2==0)||(sl1==LONG_MIN && sl2==-1)) { //处置严重错误 } else { result = sl1 / sl2; }5.2检测运算溢出
整数的加减乘运算都有可能将发生溢出,在讨论未定义行为时,给出过两个有符号整形加法溢出推论标识符,这里再给出两个无符号整形加法溢出推论标识符段:
#include <limits.h> unsigned int a,b,result;/*初始化a,b*/if(UINT_MAX-a<b) {//处置溢出 }else { result=a+b; }PDP硬体一般没浮点处置器,浮点数运算在PDP也比较少见因而溢出推论严重依赖C库支持,这里不讨论。
5.3检测移位
在讨论未定义行为时,提到有符号数右移、移位的数量是负值或是大于操作数的位数都是未定义行为,也提到不对有符号数展开位操作,但要检测移位的数量与否大于操作数的位数。下面给出两个无符号整数左移检测标识符段:
unsigned int ui1;unsigned int ui2;unsigned int uresult;/*初始化ui1,ui2*/if(ui2>=sizeof(unsigned int)*CHAR_BIT) {//处理严重错误 }else { uresult=ui1<<ui2; }6 如果有硬体看门狗,则采用它
在其它一切措施都失效的情况下,看门狗可能将是最后的防线。它的原理特别简单,但却能大大提高设备的安全性。如果设备有硬体看门狗,一定要为它编写驱动程序。相关推荐:STM32实例-窗口看门狗实验。
要尽可能将早的开启看门狗这是因为从上电复位结束到开启看门狗的这段时间内,设备有可能将被阻碍而跳过看门狗初始化程序,导致看门狗失效。尽可能早的开启看门狗,可以降低这种概率;
不要在中断中喂狗,除非有其他联动措施在中断程序喂狗,由于阻碍的存在,程序可能将一直处于中断之中,这样会导致看门狗失效。如果在主程序中设置标志位,中断程序喂狗时与这个标志位联合推论,也是允许的;
喂狗间隔跟商品需求有关,并非特定的时间商品的特性决定了喂狗间隔。对不涉及安全性、实时性的设备,喂狗间隔比较宽松,但间隔时间不宜过长,否则被用户感知到,是影响用户体验的。对设计安全性、有实时控制类的设备,原则是尽可能将快的复位,否则会造成事故。
克莱门汀号在展开第二阶段的任务时,原本预订要从月球飞行到太空深处的Geographos小行星展开探勘,然而这艘太空探测器在飞向小行星时却由于两个软件瑕疵而使其中断运作20分钟,不但未能到达小行星,也因为控制喷嘴燃烧了11分钟使电力供应降低,无法再透过远端控制探测器,最终结束这项任务,但也导致了资源与资金的浪费。
“克莱门汀太空任务失败这件事让我感到极为震惊,它其实可以透过硬体中一款简单的看门狗计时器避免掉这项不幸,但由于当时的合作开发时间相当紧缩,程序设计人员没时间编写程序来启动它,”Ganssle说。
遗憾的是,1998年发射的近地号太空船(NEAR)也遇到了相同的问题。由于程式设计人员并未采纳建议,因而,当推进器减速器系统故障时,29公斤的储备燃料也随之报销──这同样是两个本来可经由看门狗定时器程式设计而避免的问题,同时也证明要从其他程序设计人员的严重错误中学习并不容易。
7关键统计数据储存多个备份,取统计数据采用“表决法”
RAM中的统计数据在受到阻碍情况下有可能将被改变,对系统关键统计数据应该展开保护。关键统计数据包括全局变量、静态变量以及须要保护的数据区域。备份统计数据与原统计数据不应该处于相邻位置,因而不应由编译器默认分配备份统计数据位置,而应该由合作开发人员指定区域储存。相关文章:单片机中的RAM vs ROM。
可以将RAM分为3个区域,第两个区域保存原码,第二个区域保存反码,第三个区域保存异或码,区域之间预留一定量的“空白”RAM作为隔离。可以采用编译器的“分散加载”机制将变量分别储存在这些区域。须要展开读取时,同时读出3份统计数据并展开表决,取至少有两个相同的那个值。
假如设备的RAM从0x1000_0000开始,我须要在RAM的0x1000_0000~0x10007FFF内储存原码,在0x1000_9000~0x10009FFF内储存反码,在0x1000_B000~0x1000BFFF内储存0xAA的异或码,编译器的分散加载可以设置为:
LR_IROM1 0x00000000 0x00080000 { ; load region size_region ER_IROM1 0x00000000 0x00080000 { ; load address = execution address *.o (RESET, +First)*(InRoot$$Sections) .ANY (+RO) } RW_IRAM1 0x10000000 0x00008000 { ;保存原码 .ANY (+RW +ZI ) }RW_IRAM3 0x10009000 0x00001000{ ;保存反码 .ANY (MY_BK1) } RW_IRAM2 0x1000B000 0x00001000 { ;保存异或码 .ANY (MY_BK2) } }如果两个关键变量须要多处备份,可以按照下面方式定义变量,将三个变量分别指定到三个不连续的RAM区中,并在定义时按照原码、反码、0xAA的异或码展开初始化。
uint32 plc_pc=0; //原码 __attribute__((section(“MY_BK1”))) uint32 plc_pc_not=~0x0; //反码__attribute__((section(“MY_BK2”))) uint32 plc_pc_xor=0x0^0xAAAAAAAA; //异或码当须要写这个变量时,这三个位置都要更新;读取变量时,读取三个值做推论,取至少有两个相同的那个值。
为什么选取异或码而不是补码?这是因为MDK的整数是按照补码储存的,正数的补码与原码相同,在这种情况下,原码和补码是一致的,不但起不到冗余作用,反而对安全性有害。比如储存的两个非零整数区因为阻碍,RAM都被清零,由于原码和补码一致,按照3取2的“表决法”,会将阻碍值0当做正确的数据。
8 对非易失性储存器展开备份存储
非易失性储存器包括但不限于Flash、EEPROM、铁电。仅仅将写入非易失性储存器中的统计数据再读出校验是不够的。强阻碍情况下可能将导致非易失性储存器内的统计数据严重错误,在写非易失性储存器的期间系统掉电将导致统计数据丢失,因阻碍导致程序跑飞到写非易失性储存器表达式中,将导致统计数据储存紊乱。相关文章:EEPROM和Flash这样讲,我早就懂了。
一种可信的办法是将非易失性储存器分成多个区,每个统计数据都将按照不同的形式写入到这些分区中,须要展开读取时,同时读出多份统计数据并展开表决,取相同数目较多的那个值。
9 软件锁
对初始化序列或是有一定先后次序的表达式调用,为了保证调用次序或是确保每个函数都被调用,我们可以采用环环相扣,实质上这也是一种软件锁。此外对一些安全关键标识符语句(是语句,而不是表达式),可以给它们设置软件锁,只有持有特定钥匙的,才可以访问这些关键标识符。也可以通俗的理解为,关键安全标识符不能按照单一条件继续执行,要额外的多设置两个标志。
比如,向Flash写两个统计数据,我们会推论统计数据与否不合法、写入的地址与否不合法,计算要写入的扇区。之后调用写Flash子程序,在这个子程序中,推论扇区地址与否不合法、统计数据长度与否不合法,之后就要将统计数据写入Flash。
由于写Flash语句是安全关键标识符,所以程序给这些语句上锁:必须具备正确的钥匙才可以写Flash。这样即使是程序跑飞到写Flash子程序,也能大大降低误写的风险。
/*************************************************************** * 名称:RamToFlash() * 功能:复制RAM的统计数据到FLASH,命令标识符51。 * 入口模块:dst 目标地址,即FLASH起始地址。以512字节为分界 * src 源地址,即RAM地址。地址必须字对齐 * no 复制字节个数,为512/1024/4096/8192 * ProgStart 软件锁标志 * 出口模块:IAPcodice(paramout缓冲区) CMD_SUCCESS,SRC_ADDR_ERROR,DST_ADDR_ERROR, SRC_ADDR_NOT_MAPPED,DST_ADDR_NOT_MAPPED,COUNT_ERROR,BUSY,未选择扇区****************************************************************/ void RamToFlash(uint32 dst, uint32 src, uint32 no,uint8 ProgStart) {PLC_ASSERT(“Sector number”,(dst>=0x00040000)&&(dst<=0x0007FFFF));PLC_ASSERT(“Copy bytes number is 512”,(no==512)); PLC_ASSERT(“ProgStart==0xA5”,(ProgStart==0xA5)); paramin[0] = IAP_RAMTOFLASH; // 设置命令字 paramin[1] = dst; // 设置模块 paramin[2] = src; paramin[3] = no; paramin[4] = Fcclk/1000;if(ProgStart==0xA5) //只有软件锁标志正确时,才继续执行关键标识符 { iap_entry(paramin, paramout); // 调用IAP服务程序 ProgStart=0; } else {paramout[0]=PROG_UNSTART; } }该程序段是程式设计lpc1778内部Flash,其中调用IAP程序的表达式iap_entry(paramin, paramout)是关键安全标识符,所以在继续执行该标识符前,先推论两个特定设置的安全锁标志ProgStart,只有这个标志符合设定值,才会继续执行程式设计Flash操作。如果因为不幸程序跑飞到该表达式,由于ProgStart标志不正确,是不会对Flash展开程式设计的。
10 通信
通讯线上的统计数据误码相对严重,通讯线越长,所处的环境越恶劣,误码会越严重。抛开硬体和环境的作用,我们的软件应能识别严重错误的通讯统计数据。对此有一些应用措施:
制定协议时,限制每帧的字节数;每帧字节数越多,发生误码的可能性就越大,无效的统计数据也会越多。对此以太网规定每帧统计数据不大于1500字节,高安全性的CAN收发器规定每帧统计数据不得多于8字节,对RS485,基于RS485链路应用最广泛的Modbus协议一帧统计数据规定不超过256字节。因而,建议制定内部通讯协议时,采用RS485时规定每帧统计数据不超过256字节;
采用多种校验编写程序时应使能奇偶校验,每帧超过16字节的应用,建议至少编写CRC16校验程序。
减少额外推论1)减少缓冲区溢出推论。这是因为统计数据接收多是在中断中完成,编译器检测不出缓冲区与否溢出,须要手动检查和,在上文介绍统计数据溢出一节中已经详细说明。
2)减少超时推论。当一帧统计数据接收到一半,长时间接收不到剩余统计数据,则认为这帧统计数据无效,重新开始接收。可选,跟不同的协议有关,但缓冲区溢出推论必须实现。这是因为对须要帧头推论的协议,上位机可能将发送完帧头后突然断电,重启后上位机是从新的帧开始发送的,但是下位机已经接收到了上次未发送完的帧头,所以上位机的这次帧头会被下位机当成正常统计数据接收。这有可能将造成统计数据长度字段为两个很大的值,填满该长度的缓冲区须要相当多的统计数据(比如一帧可能将1000字节),影响响应时间;另一方面,如果程序没缓冲区溢出推论,那么缓冲区很可能将溢出,后果是灾难性的。
重传机制如果检测到通讯统计数据发生了严重错误,则要有重传机制重新发送出错的帧。
11 开关量输入的检测、确认
开关量容易受到尖脉冲阻碍,如果不进行滤除,可能将会造成误动作。一般情况下,须要对开关量输入信号展开多次采样,并展开逻辑推论直到确认信号无误为止。
12 开关量输出
开关信号简单的一次输出是不安全的,阻碍信号可能将会翻转开关量输出的状态。采取重复刷新输出可以有效避免电平的翻转。
13 初始化信息的保存和恢复
微处置器的寄存器值也可能将会因外间阻碍而改变,外设初始化值须要在寄存器中长期保存,最容易被破坏。由于Flash中的统计数据相对不易被破坏,可以将初始化信息预先写入Flash,待程序空闲时比较与初始化相关的寄存器值与否被更动,如果发现非法更动则采用Flash中的值展开恢复。
公司目前采用的4.3寸LCD显示屏抗阻碍能力一般。如果显示屏与控制器之间的排线距离过长或是对采用该显示屏的设备打静电或是脉冲群,显示屏有可能将会花屏或是白屏。
对此,我们可以将初始化显示屏的统计数据保存在Flash中,程序运转后,每隔一段时间从显示屏的寄存器读出当前值和Flash储存的值相比较,如果发现两者不同,则重新初始化显示屏。下面给出校验源码,仅供参考。
定义统计数据结构:
定义const修饰的结构体变量,储存LCD部分寄存器的初始值,这个初始值跟具体的应用初始化有关,不一定是表中的统计数据,通常情况下,这个结构体变量被储存到Flash中。
/*LCD部分寄存器设置值列表*/ lcd_redu_list_struct const lcd_redu_list_str[]= {{SSD1963_Get_Address_Mode,{0x20} ,1}, /*1*/ {SSD1963_Get_Pll_Mn ,{0x3b,0x02,0x04} ,3}, /*2*/ {SSD1963_Get_Pll_Status ,{0x04} ,1}, /*3*/ {SSD1963_Get_Lcd_Mode ,{0x24,0x20,0x01,0xdf,0x01,0x0f,0x00} ,7}, /*4*/ {SSD1963_Get_Hori_Period ,{0x02,0x0c,0x00,0x2a,0x07,0x00,0x00,0x00},8}, /*5*/{SSD1963_Get_Vert_Period ,{0x01,0x1d,0x00,0x0b,0x09,0x00,0x00} ,7}, /*6*/ {SSD1963_Get_Power_Mode ,{0x1c} ,1}, /*7*/ {SSD1963_Get_Display_Mode,{0x03} ,1}, /*8*/ {SSD1963_Get_Gpio_Conf ,{0x0F,0x01} ,2}, /*9*/ {SSD1963_Get_Lshift_Freq ,{0x00,0xb8} ,2}, /*10*/ };实现表达式如下所示,表达式会遍历结构体变量中的每两个命令,以及每两个命令下的初始值,如果有两个不正确,则跳出循环,继续执行重新初始化和恢复措施。这个表达式中的MY_DEBUGF宏是我自己的调试表达式,采用串口打印调试信息,在接下来的第五部分将详细叙述。
通过这个表达式,我可以长时间监控显示屏的哪些命令、哪些位容易被阻碍。程序里采用了两个被妖魔化的关键字:goto。大多数C词汇书籍对goto关键字谈之色变,但你应该有自己的推论。在表达式内部跳出多重循环,除了goto关键字,又有哪种方法能如此简洁高效!
/** * lcd 显示冗余 * 每隔一段时间调用该程序一次 */ void lcd_redu(void) { uint8_t tmp[8]; uint32_t i,j; uint32_t lcd_init_flag; lcd_init_flag =0;for(i=0;i<sizeof(lcd_redu_list_str)/sizeof(lcd_redu_list_str[0]);i+) {LCD_SendCommand(lcd_redu_list_str[i].lcd_command);uyDelay(10);for(j=0;j<lcd_redu_list_str[i].lcd_value_num;j++) {tmp[j]=LCD_ReadData();if(tmp[j]!=lcd_redu_list_str[i].lcd_get_value[j]) {lcd_init_flag=0x55;MY_DEBUGF(MENU_DEBUG,(“读lcd寄存器值与预期不符,命令为:0x%x,第%d个模块, 该模块正确值为:0x%x,实际读出值为:0x%x\n”,lcd_redu_list_str[i].lcd_command,j+1,lcd_redu_list_str[i].lcd_get_value[j],tmp[j]));goto handle_lcd_init; } } }handle_lcd_init:if(lcd_init_flag==0x55) {//重新初始化LCD //一些必要性的恢复措施 } }14 圈套
对8051内核单片机,由于没相应的硬体支持,可以用纯软件设置软件圈套,用来拦截一些程序跑飞。对ARM7或是Cortex-M系列单片机,硬体已经内建了多种异常,软件须要根据硬体异常来编写圈套程序,用来快速定位甚至恢复严重错误。
15 阻塞处置
有这时候合作开发人员会采用while(!flag);语句阻塞在此等待标志flag改变,比如串口发送时用来等待一字节统计数据发送完成。这样的标识符时存在风险的,如果因为某些原因标志位一直不改变则会造成系统死机。
两个良好冗余的程序是设置两个超时定时器,超过一定时间后,强制程序退出while循环。
2003年8月11日发生的W32.Blaster.Worm蠕虫事件导致全球经济损失高达5亿美元,这个漏洞是利用了Windows分布式组件对象模型的远程过程调用接口中的两个逻辑瑕疵:在调用GetMachineName()表达式时,循环只设置了两个不充分的结束条件。
原标识符简化如下所示:
微软发布的安全补丁MS03-026解决了这个问题,为GetMachineName()表达式设置了充分终止条件。两个解决标识符简化如下所示(并非微软补丁标识符):
HRESULT GetMachineName( 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<end_addr)) /*充分终止条件*/ *pwszServerName++= *pwszTemp++; /*… */ }