編寫優質嵌入式C程序

前言:這是一年前我爲公司內部寫的一個文檔,旨在向年輕的嵌入式軟件工程師們介紹如何在裸機環境下編寫優質嵌入式C程序。感覺是有一定的參考價值,所以拿出來分享,拋磚引玉。



摘要:本文首先分析了C語言的陷阱和缺陷,對容易犯錯的地方進行歸納整理;分析了編譯器語義檢查的不足之處並給出防範措施,以Keil MDK編譯器爲例,介紹了該編譯器的特性、對未定義行爲的處理以及一些高級應用;在此基礎上,介紹了防禦性編程的概念,提出了編程過程中就應該防範於未然的多種措施;提出了測試對編寫優質嵌入式程序的重要作用以及常用測試方法;最後,本文試圖以更高的層次看待編程,討論一些通用的編程思想。

1. 簡介

      市面上介紹C語言以及編程方法的書數目繁多,但對如何編寫優質嵌入式C程序卻鮮有介紹,特別是對應用於單片機、ARM7、Cortex-M3這類微控制器上的優質C程序編寫方法幾乎是個空白。本文面向的,正是使用單片機、ARM7、Cortex-M3這類微控制器的底層編程人員。

       編寫優質嵌入式C程序絕非易事,它跟設計者的思維和經驗積累關係密切。嵌入式C程序員不僅需要熟知硬件的特性、硬件的缺陷等,更要深入一門語言編程,不浮於表面。爲了更方便的操作硬件,還需要對編譯器進行深入的瞭解。

       本文將從語言特性、編譯器、防禦性編程、測試和編程思想這幾個方面來討論如何編寫優質嵌入式C程序。與很多雜誌、書籍不同,本文提供大量真實實例、代碼段和參考書目,不僅介紹應該做什麼,還重點介紹如何做、以及爲什麼這樣做。編寫優質嵌入式C程序涉及面十分廣,需要程序員長時間的經驗積累,本文希望能縮短這一過程

2. C語言特性

       語言是編程的基石,C語言詭異且有種種陷阱和缺陷,需要程序員多年曆練才能達到較爲完善的地步。雖然有衆多書籍、雜誌、專題討論過C語言的陷阱和缺陷,但這並不影響本節再次討論它。總是有大批的初學者,前仆後繼的倒在這些陷阱和缺陷上,民用設備、工業設備甚至是航天設備都不例外。本節將結合具體例子再次審視它們,希望引起足夠重視。深入理解C語言特性,是編寫優質嵌入式C程序的基礎。

2.1處處都是陷阱

2.1.1 無心之過

       1)       “=”和”==”

              將比較運算符”==”誤寫成賦值運算符”=”,可能是絕大多數人都遇到過的,比如下面代碼:

  1. 1.  if(x=5)     
  2. 2.  {     
  3. 3.      //其它代碼     
  4. 4.  }  

       代碼的本意是比較變量x是否等於常量5,但是誤將”==”寫成了”=”,if語句恆爲真。如果在邏輯判斷表達式中出現賦值運算符,現在的大多數編譯器會給出警告信息。比如keil MDK會給出警告提示:“warning:  #187-D: use of "=" where"==" may have been intended”,但並非所有程序員都會注意到這類警告,因此有經驗的程序員使用下面的代碼來避免此類錯誤:

  1. 1.  if(5==x)     
  2. 2.  {     
  3. 3.      //其它代碼     
  4. 4.  }  

       將常量放在變量x的左邊,即使程序員誤將’==’寫成了’=’,編譯器會產生一個任誰也不能無視的語法錯誤信息:不可給常量賦值!

       2)       複合賦值運算符

       複合賦值運算符(+=、*=等等)雖然可以使表達式更加簡潔並有可能產生更高效的機器代碼,但某些複合賦值運算符也會給程序帶來隱含Bug,比如”+=”容易誤寫成”=+”,代碼如下:

  1. 1.  tmp=+1;  

       代碼本意是想表達tmp=tmp+1,但是將複合賦值運算符”+=”誤寫成”=+”:將正整數常量1賦值給變量tmp。編譯器會欣然接受這類代碼,連警告都不會產生。

       如果你能在調試階段就發現這個Bug,真應該慶祝一下,否則這很可能會成爲一個重大隱含Bug,且不易被察覺。

       複合賦值運算符”-=”也有類似問題存在。

       3)       其它容易誤寫

  • 使用了中文標點
  • 頭文件聲明語句最後忘記結束分號
  • 邏輯與&&和位與&、邏輯或||和位或|、邏輯非!和位取反~
  • 字母l和數字1、字母O和數字0

        這些誤寫其實容易被編譯器檢測出,只需要關注編譯器對此的提示信息,就能很快解決。

       很多的軟件Bug源自於輸入錯誤。在Google上搜索的時候,有些結果列表項中帶有一條警告,表明Google認爲它帶有惡意代碼。如果你在2009年1月31日一大早使用Google搜索的話,你就會看到,在那天早晨55分鐘的時間內,Google的搜索結果標明每個站點對你的PC都是有害的。這涉及到整個Internet上的所有站點,包括Google自己的所有站點和服務。Google的惡意軟件檢測功能通過在一個已知攻擊者的列表上查找站點,從而識別出危險站點。在1月31日早晨,對這個列表的更新意外地包含了一條斜槓(“/”)。所有的URL都包含一條斜槓,並且,反惡意軟件功能把這條斜槓理解爲所有的URL都是可疑的,因此,它愉快地對搜索結果中的每個站點都添加一條警告。很少見到如此簡單的一個輸入錯誤帶來的結果如此奇怪且影響如此廣泛,但程序就是這樣,容不得一絲疏忽。

2.1.2 數組下標

       數組常常也是引起程序不穩定的重要因素,C語言數組的迷惑性與數組下標從0開始密不可分,你可以定義int test[30],但是你絕不可以使用數組元素test [30],除非你自己明確知道在做什麼。

2.1.3 容易被忽略的break關鍵字

       1)       不能漏加的break

              switch…case語句可以很方便的實現多分支結構,但要注意在合適的位置添加break關鍵字。程序員往往容易漏加break從而引起順序執行多個case語句,這也許是C的一個缺陷之處。

       對於switch…case語句,從概率論上說,絕大多數程序一次只需執行一個匹配的case語句,而每一個這樣的case語句後都必須跟一個break。去複雜化大概率事件,這多少有些不合常情。

       2)       不能亂加的break

              break關鍵字用於跳出最近的那層循環語句或者switch語句,但程序員往往不夠重視這一點。

       1990年1月15日,AT&T電話網絡位於紐約的一臺交換機宕機並且重啓,引起它鄰近交換機癱瘓,由此及彼,一個連着一個,很快,114臺交換機每六秒宕機重啓一次,六萬人九小時內不能打長途電話。當時的解決方式:工程師重裝了以前的軟件版本。。。事後的事故調查發現,這是break關鍵字誤用造成的。《C專家編程》提供了一個簡化版的問題源碼:

  1. 1.  network code()    
  2. 2.  {    
  3. 3.      switch(line)   
  4. 4.       {    
  5. 5.          case  THING1:  
  6. 6.             {    
  7. 7.              doit1();   
  8. 8.           } break;    
  9. 9.          case  THING2:  
  10. 10.            {    
  11. 11.             if(x==STUFF)   
  12. 12.              {    
  13. 13.                 do_first_stuff();    
  14. 14.                 if(y==OTHER_STUFF)    
  15. 15.                     break;    
  16. 16.                 do_later_stuff();    
  17. 17.             }  /*代碼的意圖是跳轉到這裏… …*/    
  18. 18.             initialize_modes_pointer();   
  19. 19.            } break;    
  20. 20.         default :    
  21. 21.             processing();    
  22. 22.     } /*… …但事實上跳到了這裏。*/    
  23. 23.     use_modes_pointer(); /*致使modes_pointer未初始化*/    
  24. 24. }    
       那個程序員希望從if語句跳出,但他卻忘記了break關鍵字實際上跳出最近的那層循環語句或者switch語句。現在它跳出了switch語句,執行了use_modes_pointer()函數。但必要的初始化工作並未完成,爲將來程序的失敗埋下了伏筆。

2.1.4 意想不到的八進制

       將一個整形常量賦值給變量,代碼如下所示:

  1. 1.  int a=34, b=034;   

       變量a和b相等嗎?

       答案是不相等的。我們知道,16進制常量以’0x’爲前綴,10進制常量不需要前綴,那麼8進制呢?它與10進制和16進製表示方法都不相通,它以數字’0’爲前綴,這多少有點奇葩:三種進制的表示方法完全不相通。如果8進制也像16進制那樣以數字和字母表示前綴的話,或許更有利於減少軟件Bug,畢竟你使用8進制的次數可能都不會有誤使用的次數多!下面展示一個誤用8進制的例子,最後一個數組元素賦值錯誤:

  1. 1.  a[0]=106;       /*十進制數106*/    
  2. 2.  a[1]=112;         /*十進制數112*/     
  3. 3.  a[2]=052;       /*實際爲十進制數42,本意爲十進制52*/   

2.1.5指針加減運算

       指針的加減運算是特殊的。下面的代碼運行在32位ARM架構上,執行之後,a和p的值分別是多少?

  1. 1.  int a=1;    
  2. 2.  int *p=(int *)0x00001000;    
  3. 3.  a=a+1;    
  4. 4.  p=p+1;   

       對於a的值很容判斷出結果爲2,但是p的結果卻是0x00001004。指針p加1後,p的值增加了4,這是爲什麼呢?原因是指針做加減運算時是以指針的數據類型爲單位。p+1實際上是按照公式p+1*sizeof(int)來計算的。不理解這一點,在使用指針直接操作數據時極易犯錯。

某項目使用下面代碼對連續RAM初始化零操作,但運行發現有些RAM並沒有被真正清零。

  1. 1.  unsigned int *pRAMaddr;         //定義地址指針變量    
  2. 2.  for(pRAMaddr=StartAddr;pRAMaddr<EndAddr;pRAMaddr+=4)    
  3. 3.  {    
  4. 4.       *pRAMaddr=0x00000000;   //指定RAM地址清零    
  5. 5.  }   

       通過分析我們發現,由於pRAMaddr是一個無符號int型指針變量,所以pRAMaddr+=4代碼其實使pRAMaddr偏移了4*sizeof(int)=16個字節,所以每執行一次for循環,會使變量pRAMaddr偏移16個字節空間,但只有4字節空間被初始化爲零。其它的12字節數據的內容,在大多數架構處理器中都會是隨機數。

2.1.6關鍵字sizeof

       不知道有多少人最初認爲sizeof是一個函數。其實它是一個關鍵字,其作用是返回一個對象或者類型所佔的內存字節數,對絕大多數編譯器而言,返回值爲無符號整形數據。需要注意的是,使用sizeof獲取數組長度時,不要對指針應用sizeof操作符,比如下面的例子:

  1. 1.  void ClearRAM(char array[])    
  2. 2.  {    
  3. 3.      int i ;    
  4. 4.      for(i=0;i<sizeof(array)/sizeof(array[0]);i++)     //這裏用法錯誤,array實際上就是指針  
  5. 5.      {    
  6. 6.          array[i]=‘a’;    
  7. 7.      }    
  8. 8.  }    
  9. 9.      
  10. 10. int main(void)    
  11. 11. {    
  12. 12.     char Fle[20];    
  13. 13.         
  14. 14.     ClearRAM(Fle);          //只能清除數組Fle中的前四個元素    
  15. 15. }    

       我們知道,對於一個數組array[20],我們使用代碼sizeof(array)/sizeof(array[0])可以獲得數組的元素(這裏爲20),但數組名和指針往往是容易混淆的,有且只有一種情況下數組名是可以當做指針的,那就是數組名作爲函數形參時,數組名被認爲是指針,同時,它不能再兼任數組名。注意只有這種情況下,數組名纔可以當做指針,但不幸的是這種情況下容易引發風險。在ClearRAM函數內,作爲形參的array[]不再是數組名了,而成了指針。sizeof(array)相當於求指針變量佔用的字節數,在32位系統下,該值爲4,sizeof(array)/sizeof(array[0])的運算結果也爲4。所以在main函數中調用ClearRAM(Fle),也只能清除數組Fle中的前四個元素了。





轉載出處:http://blog.csdn.net/zhzht19861011/article/details/45508029
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章