嵌入式產品的可靠性自然與硬體密不可分,但在硬體確定、并且沒有第三方測驗的前提下,使用防御性編程思想寫出的代碼,往往具有更高的穩定性,
防御性編程首先需要認清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所能表示的范圍了,所以,在這種情況下,除了要檢測除數是否為零外,還要檢測除法是否溢位,
#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;
}
嵌入式硬體一般沒有浮點處理器,浮點數運算在嵌入式也比較少見并且溢位判斷嚴重依賴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. 如果有硬體看門狗,則使用它
在其它一切措施都失效的情況下,看門狗可能是最后的防線,它的原理特別簡單,但卻能大大提高設備的可靠性,如果設備有硬體看門狗,一定要為它撰寫驅動程式,
·要盡可能早的開啟看門狗
這是因為從上電復位結束到開啟看門狗的這段時間內,設備有可能被干擾而跳過看門狗初始化程式,導致看門狗失效,盡可能早的開啟看門狗,可以降低這種概率;
·不要在中斷中喂狗,除非有其他聯動措施
在中斷程式喂狗,由于干擾的存在,程式可能一直處于中斷之中,這樣會導致看門狗失效,如果在主程式中設定標志位,中斷程式喂狗時與這個標志位聯合判斷,也是允許的;
·喂狗間隔跟產品需求有關,并非特定的時間
產品的特性決定了喂狗間隔,對于不涉及安全性、實時性的設備,喂狗間隔比較寬松,但間隔時間不宜過長,否則被用戶感知到,是影響用戶體驗的,對于設計安全性、有實時控制類的設備,原則是盡可能快的復位,否則會造成事故,
克萊門汀號在進行第二階段的任務時,原本預訂要從月球飛行到太空深處的Geographos小行星進行探勘,然而這艘太空探測器在飛向小行星時卻由于一個軟體缺陷而使其中斷運作20分鐘,不但未能到達小行星,也因為控制噴嘴燃燒了11分鐘使電力供應降低,無法再透過遠端控制探測器,最終結束這項任務,但也導致了資源與資金的浪費,
“克萊門汀太空任務失敗這件事讓我感到十分震驚,它其實可以透過硬體中一款簡單的看門狗計時器避免掉這項意外,但由于當時的開發時間相當緊縮,程式設計人員沒時間撰寫程式來啟動它,”Ganssle說,
遺憾的是,1998年發射的近地號太空船(NEAR)也遇到了相同的問題,由于編程人員并未采納建議,因此,當推進器減速器系統故障時,29公斤的儲備燃料也隨之報銷──這同樣是一個本來可經由看門狗定時器編程而避免的問題,同時也證明要從其他程式設計人員的錯誤中學習并不容易,
7. 關鍵資料儲存多個備份,取資料采用“表決法”
RAM中的資料在受到干擾情況下有可能被改變,對于系統關鍵資料應該進行保護,關鍵資料包括全域變數、靜態變數以及需要保護的資料區域,備份資料與原資料不應該處于相鄰位置,因此不應由編譯器默認分配備份資料位置,而應該由程式員指定區域存盤,
可以將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、鐵電,僅僅將寫入非易失性存盤器中的資料再讀出校驗是不夠的,強干擾情況下可能導致非易失性存盤器內的資料錯誤,在寫非易失性存盤器的期間系統掉電將導致資料丟失,因干擾導致程式跑飛到寫非易失性存盤器函式中,將導致資料存盤紊亂,
一種可靠的辦法是將非易失性存盤器分成多個區,每個資料都將按照不同的形式寫入到這些磁區中,需要進行讀取時,同時讀出多份資料并進行表決,取相同數目較多的那個值,
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<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++;
/*… */
}
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/293777.html
標籤:其他
上一篇:【智力題】有 1000 瓶藥物,但是其中有一瓶是有毒的,小白鼠吃了一個星期以后就會死掉!請問,在一個星期內找出有毒的 藥物,最少需要多少只小白鼠?
