????嵌入式產品的可靠性自然與硬件密不可分,但在硬件確定、并且沒有第三方測試的前提下,使用防御性編程思想寫出的代碼,往往具有更高的穩定性。
????防御性編程首先需要認清C語言的種種缺陷和陷阱,C語言對于運行時的檢查十分弱小,需要程序員謹慎的考慮代碼,在必要的時候增加判斷;防御性編程的另一個核心思想是假設代碼運行在并不可靠的硬件上,外接干擾有可能會打亂程序執行順序、更改RAM存儲數據等等。
1?具有形參的函數,需判斷傳遞來的實參是否合法
????程序員可能無意識的傳遞了錯誤參數;外界的強干擾可能將傳遞的參數修改掉,或者使用隨機參數意外的調用函數,因此在執行函數主體前,需要先確定實參是否合法。
2?仔細檢查函數的返回值
????對函數返回的錯誤碼,要進行全面仔細處理,必要時做錯誤記錄。
3?防止指針越界
????如果動態計算一個地址時,要保證被計算的地址是合理的并指向某個有意義的地方。特別對于指向一個結構或數組的內部的指針,當指針增加或者改變后仍然指向同一個結構或數組。
4?防止數組越界
????數組越界的問題前文已經講述的很多了,由于C不會對數組進行有效的檢測,因此必須在應用中顯式的檢測數組越界問題。下面的例子可用于中斷接收通訊數據。
????在使用一些庫函數時,同樣需要對邊界進行檢查,比如下面的memset(RecBuf,0,len)函數把RecBuf指指向的內存區的前len個字節用0填充,如果不注意len的長度,就會將數組RecBuf之外的內存區清零:
5?數學算數運算
5.1除法運算,只檢測除數為零就可靠嗎?
????除法運算前,檢查除數是否為零幾乎已經成為共識,但是僅檢查除數是否為零就夠了嗎?
????考慮兩個整數相除,對于一個signed long類型變量,它能表示的數值范圍為:-2147483648 ~+2147483647,如果讓-2147483648/ -1,那么結果應該是+2147483648,但是這個結果已經超出了signedlong所能表示的范圍了。所以,在這種情況下,除了要檢測除數是否為零外,還要檢測除法是否溢出。
?
#includesigned long sl1,sl2,result; /*初始化sl1和sl2*/ if((sl2==0)||(sl1==LONG_MIN && sl2==-1)) { //處理錯誤 } else { result = sl1 / sl2; }
?
5.2檢測運算溢出
????整數的加減乘運算都有可能發生溢出,在討論未定義行為時,給出過一個有符號整形加法溢出判斷代碼,這里再給出一個無符號整形加法溢出判斷代碼段:
????嵌入式硬件一般沒有浮點處理器,浮點數運算在嵌入式也比較少見并且溢出判斷嚴重依賴C庫支持,這里不討論。
5.3檢測移位
????在討論未定義行為時,提到有符號數右移、移位的數量是負值或者大于操作數的位數都是未定義行為,也提到不對有符號數進行位操作,但要檢測移位的數量是否大于操作數的位數。下面給出一個無符號整數左移檢測代碼段:
?
unsigned int ui1;unsigned int ui2;unsigned int uresult;/*初始化ui1,ui2*/if(ui2>=sizeof(unsigned int)*CHAR_BIT) {//處理錯誤 }else { uresult=ui1<?
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 軟件鎖標志 * 出口參數:IAP返回值(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?
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’?’)&&?(pwszServerName? ?
原文標題:嵌入式開發中的防御性C語言編程
文章出處:【微信公眾號:STM32嵌入式開發】歡迎添加關注!文章轉載請注明出處。
? ? ? 審核編輯:彭靜
評論
查看更多