????嵌入式產(chǎn)品的可靠性自然與硬件密不可分,但在硬件確定、并且沒有第三方測試的前提下,使用防御性編程思想寫出的代碼,往往具有更高的穩(wěn)定性。
????防御性編程首先需要認清C語言的種種缺陷和陷阱,C語言對于運行時的檢查十分弱小,需要程序員謹慎的考慮代碼,在必要的時候增加判斷;防御性編程的另一個核心思想是假設(shè)代碼運行在并不可靠的硬件上,外接干擾有可能會打亂程序執(zhí)行順序、更改RAM存儲數(shù)據(jù)等等。
1?具有形參的函數(shù),需判斷傳遞來的實參是否合法
????程序員可能無意識的傳遞了錯誤參數(shù);外界的強干擾可能將傳遞的參數(shù)修改掉,或者使用隨機參數(shù)意外的調(diào)用函數(shù),因此在執(zhí)行函數(shù)主體前,需要先確定實參是否合法。
2?仔細檢查函數(shù)的返回值
????對函數(shù)返回的錯誤碼,要進行全面仔細處理,必要時做錯誤記錄。
3?防止指針越界
????如果動態(tài)計算一個地址時,要保證被計算的地址是合理的并指向某個有意義的地方。特別對于指向一個結(jié)構(gòu)或數(shù)組的內(nèi)部的指針,當(dāng)指針增加或者改變后仍然指向同一個結(jié)構(gòu)或數(shù)組。
4?防止數(shù)組越界
????數(shù)組越界的問題前文已經(jīng)講述的很多了,由于C不會對數(shù)組進行有效的檢測,因此必須在應(yīng)用中顯式的檢測數(shù)組越界問題。下面的例子可用于中斷接收通訊數(shù)據(jù)。
????在使用一些庫函數(shù)時,同樣需要對邊界進行檢查,比如下面的memset(RecBuf,0,len)函數(shù)把RecBuf指指向的內(nèi)存區(qū)的前l(fā)en個字節(jié)用0填充,如果不注意len的長度,就會將數(shù)組RecBuf之外的內(nèi)存區(qū)清零:
5?數(shù)學(xué)算數(shù)運算
5.1除法運算,只檢測除數(shù)為零就可靠嗎?
????除法運算前,檢查除數(shù)是否為零幾乎已經(jīng)成為共識,但是僅檢查除數(shù)是否為零就夠了嗎?
????考慮兩個整數(shù)相除,對于一個signed long類型變量,它能表示的數(shù)值范圍為:-2147483648 ~+2147483647,如果讓-2147483648/ -1,那么結(jié)果應(yīng)該是+2147483648,但是這個結(jié)果已經(jīng)超出了signedlong所能表示的范圍了。所以,在這種情況下,除了要檢測除數(shù)是否為零外,還要檢測除法是否溢出。
?
#includesigned long sl1,sl2,result; /*初始化sl1和sl2*/ if((sl2==0)||(sl1==LONG_MIN && sl2==-1)) { //處理錯誤 } else { result = sl1 / sl2; }
?
5.2檢測運算溢出
????整數(shù)的加減乘運算都有可能發(fā)生溢出,在討論未定義行為時,給出過一個有符號整形加法溢出判斷代碼,這里再給出一個無符號整形加法溢出判斷代碼段:
?
#includeunsigned int a,b,result; /*初始化a,b*/ if(UINT_MAX-a ?
????嵌入式硬件一般沒有浮點處理器,浮點數(shù)運算在嵌入式也比較少見并且溢出判斷嚴重依賴C庫支持,這里不討論。
5.3檢測移位
????在討論未定義行為時,提到有符號數(shù)右移、移位的數(shù)量是負值或者大于操作數(shù)的位數(shù)都是未定義行為,也提到不對有符號數(shù)進行位操作,但要檢測移位的數(shù)量是否大于操作數(shù)的位數(shù)。下面給出一個無符號整數(shù)左移檢測代碼段:
?
unsigned int ui1; unsigned int ui2; unsigned int uresult; /*初始化ui1,ui2*/ if(ui2>=sizeof(unsigned int)*CHAR_BIT) { //處理錯誤 } else { uresult=ui1<?
6?如果有硬件看門狗,則使用它
????在其它一切措施都失效的情況下,看門狗可能是最后的防線。它的原理特別簡單,但卻能大大提高設(shè)備的可靠性。如果設(shè)備有硬件看門狗,一定要為它編寫驅(qū)動程序。相關(guān)推薦:STM32實例-窗口看門狗實驗。
要盡可能早的開啟看門狗
????這是因為從上電復(fù)位結(jié)束到開啟看門狗的這段時間內(nèi),設(shè)備有可能被干擾而跳過看門狗初始化程序,導(dǎo)致看門狗失效。盡可能早的開啟看門狗,可以降低這種概率;
不要在中斷中喂狗,除非有其他聯(lián)動措施
????在中斷程序喂狗,由于干擾的存在,程序可能一直處于中斷之中,這樣會導(dǎo)致看門狗失效。如果在主程序中設(shè)置標(biāo)志位,中斷程序喂狗時與這個標(biāo)志位聯(lián)合判斷,也是允許的;
喂狗間隔跟產(chǎn)品需求有關(guān),并非特定的時間
????產(chǎn)品的特性決定了喂狗間隔。對于不涉及安全性、實時性的設(shè)備,喂狗間隔比較寬松,但間隔時間不宜過長,否則被用戶感知到,是影響用戶體驗的。對于設(shè)計安全性、有實時控制類的設(shè)備,原則是盡可能快的復(fù)位,否則會造成事故。
????克萊門汀號在進行第二階段的任務(wù)時,原本預(yù)訂要從月球飛行到太空深處的Geographos小行星進行探勘,然而這艘太空探測器在飛向小行星時卻由于一個軟件缺陷而使其中斷運作20分鐘,不但未能到達小行星,也因為控制噴嘴燃燒了11分鐘使電力供應(yīng)降低,無法再透過遠端控制探測器,最終結(jié)束這項任務(wù),但也導(dǎo)致了資源與資金的浪費。
????“克萊門汀太空任務(wù)失敗這件事讓我感到十分震驚,它其實可以透過硬件中一款簡單的看門狗計時器避免掉這項意外,但由于當(dāng)時的開發(fā)時間相當(dāng)緊縮,程序設(shè)計人員沒時間編寫程序來啟動它,”Ganssle說。
????遺憾的是,1998年發(fā)射的近地號太空船(NEAR)也遇到了相同的問題。由于編程人員并未采納建議,因此,當(dāng)推進器減速器系統(tǒng)故障時,29公斤的儲備燃料也隨之報銷──這同樣是一個本來可經(jīng)由看門狗定時器編程而避免的問題,同時也證明要從其他程序設(shè)計人員的錯誤中學(xué)習(xí)并不容易。
7關(guān)鍵數(shù)據(jù)儲存多個備份,取數(shù)據(jù)采用“表決法”
??? RAM中的數(shù)據(jù)在受到干擾情況下有可能被改變,對于系統(tǒng)關(guān)鍵數(shù)據(jù)應(yīng)該進行保護。關(guān)鍵數(shù)據(jù)包括全局變量、靜態(tài)變量以及需要保護的數(shù)據(jù)區(qū)域。備份數(shù)據(jù)與原數(shù)據(jù)不應(yīng)該處于相鄰位置,因此不應(yīng)由編譯器默認分配備份數(shù)據(jù)位置,而應(yīng)該由程序員指定區(qū)域存儲。相關(guān)文章:單片機中的RAM vs ROM。
????可以將RAM分為3個區(qū)域,第一個區(qū)域保存原碼,第二個區(qū)域保存反碼,第三個區(qū)域保存異或碼,區(qū)域之間預(yù)留一定量的“空白”RAM作為隔離??梢允褂镁幾g器的“分散加載”機制將變量分別存儲在這些區(qū)域。需要進行讀取時,同時讀出3份數(shù)據(jù)并進行表決,取至少有兩個相同的那個值。
????假如設(shè)備的RAM從0x1000_0000開始,我需要在RAM的0x1000_0000~0x10007FFF內(nèi)存儲原碼,在0x1000_9000~0x10009FFF內(nèi)存儲反碼,在0x1000_B000~0x1000BFFF內(nèi)存儲0xAA的異或碼,編譯器的分散加載可以設(shè)置為:
?
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) } }?
????如果一個關(guān)鍵變量需要多處備份,可以按照下面方式定義變量,將三個變量分別指定到三個不連續(xù)的RAM區(qū)中,并在定義時按照原碼、反碼、0xAA的異或碼進行初始化。
?
uint32 plc_pc=0; //原碼 __attribute__((section("MY_BK1"))) uint32 plc_pc_not=~0x0; //反碼 __attribute__((section("MY_BK2"))) uint32 plc_pc_xor=0x0^0xAAAAAAAA; //異或碼?
????當(dāng)需要寫這個變量時,這三個位置都要更新;讀取變量時,讀取三個值做判斷,取至少有兩個相同的那個值。
????為什么選取異或碼而不是補碼?這是因為MDK的整數(shù)是按照補碼存儲的,正數(shù)的補碼與原碼相同,在這種情況下,原碼和補碼是一致的,不但起不到冗余作用,反而對可靠性有害。比如存儲的一個非零整數(shù)區(qū)因為干擾,RAM都被清零,由于原碼和補碼一致,按照3取2的“表決法”,會將干擾值0當(dāng)做正確的數(shù)據(jù)。
8 對非易失性存儲器進行備份存儲
????非易失性存儲器包括但不限于Flash、EEPROM、鐵電。僅僅將寫入非易失性存儲器中的數(shù)據(jù)再讀出校驗是不夠的。強干擾情況下可能導(dǎo)致非易失性存儲器內(nèi)的數(shù)據(jù)錯誤,在寫非易失性存儲器的期間系統(tǒng)掉電將導(dǎo)致數(shù)據(jù)丟失,因干擾導(dǎo)致程序跑飛到寫非易失性存儲器函數(shù)中,將導(dǎo)致數(shù)據(jù)存儲紊亂。相關(guān)文章:EEPROM和Flash這樣講,我早就懂了。
????一種可靠的辦法是將非易失性存儲器分成多個區(qū),每個數(shù)據(jù)都將按照不同的形式寫入到這些分區(qū)中,需要進行讀取時,同時讀出多份數(shù)據(jù)并進行表決,取相同數(shù)目較多的那個值。
9 軟件鎖
????對于初始化序列或者有一定先后順序的函數(shù)調(diào)用,為了保證調(diào)用順序或者確保每個函數(shù)都被調(diào)用,我們可以使用環(huán)環(huán)相扣,實質(zhì)上這也是一種軟件鎖。此外對于一些安全關(guān)鍵代碼語句(是語句,而不是函數(shù)),可以給它們設(shè)置軟件鎖,只有持有特定鑰匙的,才可以訪問這些關(guān)鍵代碼。也可以通俗的理解為,關(guān)鍵安全代碼不能按照單一條件執(zhí)行,要額外的多設(shè)置一個標(biāo)志。
????比如,向Flash寫一個數(shù)據(jù),我們會判斷數(shù)據(jù)是否合法、寫入的地址是否合法,計算要寫入的扇區(qū)。之后調(diào)用寫Flash子程序,在這個子程序中,判斷扇區(qū)地址是否合法、數(shù)據(jù)長度是否合法,之后就要將數(shù)據(jù)寫入Flash。
????由于寫Flash語句是安全關(guān)鍵代碼,所以程序給這些語句上鎖:必須具有正確的鑰匙才可以寫Flash。這樣即使是程序跑飛到寫Flash子程序,也能大大降低誤寫的風(fēng)險。
?
?/*************************************************************** * 名稱:RamToFlash() * 功能:復(fù)制RAM的數(shù)據(jù)到FLASH,命令代碼51。 * 入口參數(shù):dst 目標(biāo)地址,即FLASH起始地址。以512字節(jié)為分界 * src 源地址,即RAM地址。地址必須字對齊 * no 復(fù)制字節(jié)個數(shù),為512/1024/4096/8192 * ProgStart 軟件鎖標(biāo)志 * 出口參數(shù):IAP返回值(paramout緩沖區(qū)) CMD_SUCCESS,SRC_ADDR_ERROR,DST_ADDR_ERROR, SRC_ADDR_NOT_MAPPED,DST_ADDR_NOT_MAPPED,COUNT_ERROR,BUSY,未選擇扇區(qū) ?****************************************************************/?? 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; // 設(shè)置命令字 paramin[1] = dst; // 設(shè)置參數(shù) paramin[2] = src; paramin[3] = no; paramin[4] = Fcclk/1000; ?????if(ProgStart==0xA5)????????????????//只有軟件鎖標(biāo)志正確時,才執(zhí)行關(guān)鍵代碼?? { iap_entry(paramin, paramout); // 調(diào)用IAP服務(wù)程序 ProgStart=0; } else { paramout[0]=PROG_UNSTART; } }?
????該程序段是編程lpc1778內(nèi)部Flash,其中調(diào)用IAP程序的函數(shù)iap_entry(paramin, paramout)是關(guān)鍵安全代碼,所以在執(zhí)行該代碼前,先判斷一個特定設(shè)置的安全鎖標(biāo)志ProgStart,只有這個標(biāo)志符合設(shè)定值,才會執(zhí)行編程Flash操作。如果因為意外程序跑飛到該函數(shù),由于ProgStart標(biāo)志不正確,是不會對Flash進行編程的。
10 通信
????通訊線上的數(shù)據(jù)誤碼相對嚴重,通訊線越長,所處的環(huán)境越惡劣,誤碼會越嚴重。拋開硬件和環(huán)境的作用,我們的軟件應(yīng)能識別錯誤的通訊數(shù)據(jù)。對此有一些應(yīng)用措施:
制定協(xié)議時,限制每幀的字節(jié)數(shù);
????每幀字節(jié)數(shù)越多,發(fā)生誤碼的可能性就越大,無效的數(shù)據(jù)也會越多。對此以太網(wǎng)規(guī)定每幀數(shù)據(jù)不大于1500字節(jié),高可靠性的CAN收發(fā)器規(guī)定每幀數(shù)據(jù)不得多于8字節(jié),對于RS485,基于RS485鏈路應(yīng)用最廣泛的Modbus協(xié)議一幀數(shù)據(jù)規(guī)定不超過256字節(jié)。因此,建議制定內(nèi)部通訊協(xié)議時,使用RS485時規(guī)定每幀數(shù)據(jù)不超過256字節(jié);
使用多種校驗
????編寫程序時應(yīng)使能奇偶校驗,每幀超過16字節(jié)的應(yīng)用,建議至少編寫CRC16校驗程序。
增加額外判斷
??? 1)增加緩沖區(qū)溢出判斷。這是因為數(shù)據(jù)接收多是在中斷中完成,編譯器檢測不出緩沖區(qū)是否溢出,需要手動檢查,在上文介紹數(shù)據(jù)溢出一節(jié)中已經(jīng)詳細說明。
??? 2)增加超時判斷。當(dāng)一幀數(shù)據(jù)接收到一半,長時間接收不到剩余數(shù)據(jù),則認為這幀數(shù)據(jù)無效,重新開始接收??蛇x,跟不同的協(xié)議有關(guān),但緩沖區(qū)溢出判斷必須實現(xiàn)。這是因為對于需要幀頭判斷的協(xié)議,上位機可能發(fā)送完幀頭后突然斷電,重啟后上位機是從新的幀開始發(fā)送的,但是下位機已經(jīng)接收到了上次未發(fā)送完的幀頭,所以上位機的這次幀頭會被下位機當(dāng)成正常數(shù)據(jù)接收。這有可能造成數(shù)據(jù)長度字段為一個很大的值,填滿該長度的緩沖區(qū)需要相當(dāng)多的數(shù)據(jù)(比如一幀可能1000字節(jié)),影響響應(yīng)時間;另一方面,如果程序沒有緩沖區(qū)溢出判斷,那么緩沖區(qū)很可能溢出,后果是災(zāi)難性的。
重傳機制
????如果檢測到通訊數(shù)據(jù)發(fā)生了錯誤,則要有重傳機制重新發(fā)送出錯的幀。
11 開關(guān)量輸入的檢測、確認
????開關(guān)量容易受到尖脈沖干擾,如果不進行濾除,可能會造成誤動作。一般情況下,需要對開關(guān)量輸入信號進行多次采樣,并進行邏輯判斷直到確認信號無誤為止。
12 開關(guān)量輸出
????開關(guān)信號簡單的一次輸出是不安全的,干擾信號可能會翻轉(zhuǎn)開關(guān)量輸出的狀態(tài)。采取重復(fù)刷新輸出可以有效防止電平的翻轉(zhuǎn)。
13 初始化信息的保存和恢復(fù)
????微處理器的寄存器值也可能會因外界干擾而改變,外設(shè)初始化值需要在寄存器中長期保存,最容易被破壞。由于Flash中的數(shù)據(jù)相對不易被破壞,可以將初始化信息預(yù)先寫入Flash,待程序空閑時比較與初始化相關(guān)的寄存器值是否被更改,如果發(fā)現(xiàn)非法更改則使用Flash中的值進行恢復(fù)。
????公司目前使用的4.3寸LCD顯示屏抗干擾能力一般。如果顯示屏與控制器之間的排線距離過長或者對使用該顯示屏的設(shè)備打靜電或者脈沖群,顯示屏有可能會花屏或者白屏。
????對此,我們可以將初始化顯示屏的數(shù)據(jù)保存在Flash中,程序運行后,每隔一段時間從顯示屏的寄存器讀出當(dāng)前值和Flash存儲的值相比較,如果發(fā)現(xiàn)兩者不同,則重新初始化顯示屏。下面給出校驗源碼,僅供參考。
????定義數(shù)據(jù)結(jié)構(gòu):
????定義const修飾的結(jié)構(gòu)體變量,存儲LCD部分寄存器的初始值,這個初始值跟具體的應(yīng)用初始化有關(guān),不一定是表中的數(shù)據(jù),通常情況下,這個結(jié)構(gòu)體變量被存儲到Flash中。
?
/*LCD部分寄存器設(shè)置值列表*/ 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*/ };?
????實現(xiàn)函數(shù)如下所示,函數(shù)會遍歷結(jié)構(gòu)體變量中的每一個命令,以及每一個命令下的初始值,如果有一個不正確,則跳出循環(huán),執(zhí)行重新初始化和恢復(fù)措施。這個函數(shù)中的MY_DEBUGF宏是我自己的調(diào)試函數(shù),使用串口打印調(diào)試信息,在接下來的第五部分將詳細敘述。
????通過這個函數(shù),我可以長時間監(jiān)控顯示屏的哪些命令、哪些位容易被干擾。程序里使用了一個被妖魔化的關(guān)鍵字:goto。大多數(shù)C語言書籍對goto關(guān)鍵字談之色變,但你應(yīng)該有自己的判斷。在函數(shù)內(nèi)部跳出多重循環(huán),除了goto關(guān)鍵字,又有哪種方法能如此簡潔高效!
?
/** * lcd 顯示冗余 * 每隔一段時間調(diào)用該程序一次 */ 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?
14 陷阱
????對于8051內(nèi)核單片機,由于沒有相應(yīng)的硬件支持,可以用純軟件設(shè)置軟件陷阱,用來攔截一些程序跑飛。對于ARM7或者Cortex-M系列單片機,硬件已經(jīng)內(nèi)建了多種異常,軟件需要根據(jù)硬件異常來編寫陷阱程序,用來快速定位甚至恢復(fù)錯誤。
15 阻塞處理
????有時候程序員會使用while(!flag);語句阻塞在此等待標(biāo)志flag改變,比如串口發(fā)送時用來等待一字節(jié)數(shù)據(jù)發(fā)送完成。這樣的代碼時存在風(fēng)險的,如果因為某些原因標(biāo)志位一直不改變則會造成系統(tǒng)死機。
????一個良好冗余的程序是設(shè)置一個超時定時器,超過一定時間后,強制程序退出while循環(huán)。
??? 2003年8月11日發(fā)生的W32.Blaster.Worm蠕蟲事件導(dǎo)致全球經(jīng)濟損失高達5億美元,這個漏洞是利用了Windows分布式組件對象模型的遠程過程調(diào)用接口中的一個邏輯缺陷:在調(diào)用GetMachineName()函數(shù)時,循環(huán)只設(shè)置了一個不充分的結(jié)束條件。
????原代碼簡化如下所示:
????微軟發(fā)布的安全補丁MS03-026解決了這個問題,為GetMachineName()函數(shù)設(shè)置了充分終止條件。一個解決代碼簡化如下所示(并非微軟補丁代碼):
?
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’?’)&&?(pwszServerName?
審核編輯:湯梓紅
評論
查看更多