2021年8月13日 星期五

MISRA--作為工業標準的C編程規範(1)

 MISRA (The Motor Industry Software Reliability Association汽車工業軟件可靠性聯會)是位於英國的一個跨國汽車工業協會,其成員包括了大部分歐美汽車生產商。

其核心使命是為汽車工業提供服務和協助,幫助廠方開發安全的、高可靠性的嵌入式軟件。
這個組織最出名的成果是所謂的MISRA C Coding Standard,這一標準中包括了127條C語言編碼標準,通常認為,如果能夠完全遵守這些標準,則你的C代碼是易讀、可靠、可移植和易於維護的。
最近很多嵌入式開發者都以MISRA C來衡量自己的編碼風格,比如著名的uC/OS-II就得意地宣稱自己99%遵守MISRA標準。
而《嵌入式開發雜誌》也專門載文號召大家學習。編碼規範通常是一個公司自定的“土政策”,居然有人去做標準,而且還得到廣泛的認可,這不禁引起我強烈的興趣。
可惜這份標準的文本需要花錢去買,而且短短幾十頁,要價非常昂貴。 MISRA在網上公佈了一些文檔,其中有關於MISRA C Coding Standard的Clarification報告,從中間你可以大致猜到MISRA標準本身是什麼。
我仔細閱讀了這些文檔,並且通過閱讀其他一些介紹性文檔,大致了解了MISRA標準的主要內容。這些條款確有過人之處,對於C/C++語言工程項目的代碼質量管理能夠起到良好的指導性作用,對於大部分軟件開發企業來說,在MISRA的基礎上適當修改就可以形成自己的規範。
當然其中也有一些過於嚴苛的東西,這就需要各個開發部門靈活處理了。我個人的體會,編碼規範雖然很簡單,但是要完全執行,不折不扣,需要開發部門有很高的組織性和紀律性,並且有很好的代碼評審機制。因此,如果能夠嚴格地遵守編碼規範,本身就是一個開發部門實力的證明。

這裡不可能將所有規則一一列出(事實上正式文本我一條也沒看到),只列出一些比較有意思的條款,讓大家有機會了解MISRA的風格。
具體的內容,感興趣的朋友可以自己到www.misra.org.uk去了解。

Rule 1.嚴格遵循ANSI C89標準,不允許任何擴展。


Rule 3.如果要嵌入彙編語言,則必須將所有彙編語句包裝在C函數里,而且這些函數中只有彙編語句,沒有常規C語句。

    Rule 7.不得使用三元操作符(? : )

Rule 10.不得殘留被註釋掉的廢代碼。

Rule 11.所有標識符不超過31字符。

Rule 12.不同名空間中的變數名不得相同。
        例如:
             typedef struct MyStruct {... } MyStruct; (違規)

             struct Person {
               char* name;
               ...
             };

             char name[32]; (違規)

Rule 13.不得使用char, int, float, double, long等基本類型,應該用自己定義的類型顯示表示類型的大小,如CHAR8, UCHAR8, INT16, INT32, FLOAT32, LONG64, ULONG64等。
Rule 14.不得使用類型char,必須顯示聲明為unsigned char或者signed char。

Rule 18.所有數字常數應當加上合適的後綴表示類型,例如51L, 42U, 34.12F等。

Rule 19.禁止使用八進制數。 (因為086U這樣的常數很容易引起誤解)。

Rule 21.不得定義與外部作用域中某個標識符同名的對象,以避免遮蓋外部作用域中的標識符。

Rule 23.具有文件作用域的對象盡量聲名為static的。

Rule 24.在同一個編譯單元中,同一個標識符不應該同事具有內部鏈接和外部鏈接的聲名。


        這裡我略作說明:

                我們通常將一些放在頭文件裡的變數聲名為“外部鏈接”的,如:
         extern UINT32 g_count; //俗話叫變數聲明(對應於變數定義,不分配實際空間)

        對於“使用”這個變數的.c文件來說,這很好,因為g_count始終保持外部鏈接性質。可是對於定義g_count(實際分配空間)的.c文件來說,如果包含了上述的頭文件,則在這個編譯單元里就發生了內部鏈接和外部鏈接的衝突。
 解決辦法是,定義g_count的文件盡量不要包含聲名g_count的頭文件。個人感覺這不是任何時候都做得到的,尤其是在對付遺留代碼的時候。

Rule 25.具有外部鏈接性質的標識符應該只聲明一次。

Rule 27.外部對像不得在多個文件中聲名。

Rule 28.禁止使用register關鍵字。

Rule 29.自動對象(棧對象)使用前必須賦初值。

Rule 33.操作符&&和||的右側表達式不得具有副作用(side-effect)。
        也就是說,象if (x == 20 && ++y == 19)這樣的表達式被禁止。

Rule 35.在返回布林值的表達式中不得出現賦值操作。
        也就是說,我們常用的if (!(fp = fopen("fname", "r"))) { /* error */ }
        被禁止。

Rule 37.不得對有符號數施加位操作,例如1 << 4將被禁止,必須寫1UL << 4;

Rule 39.不得對有符號表達式施加一元"-"操作符。

Rule 40.不得對有副作用的表達式施加sizeof操作符。

Rule 42.除了循環控制語句,不得使用逗號表達式。

Rule 44.禁止冗餘的顯式轉型。比如: double pi = (double) 3.1416F;

Rule 45.禁止從任意類型到指標的強制轉型,禁止從指標到任意類型的強制轉型。
        例如:void* p = (void*)0xFFFF8888UL;

Rule 49.顯示測試值是否為零。

Rule 50.不得顯式判斷浮點數的相等性和不等性。

Rule 52.不得遺留“永遠不會用到”的代碼。

Rule 53.所有非空語句必須具有副作用。

Rule 55.除了switch語句,不得使用標號(label)。

Rule 56.不得使用goto.

Rule 57.不得使用continue。

Rule 58.除了switch語句,不得使用break.

Rule 59. if, else if, else, while, do..while, for語句塊必須使用{}括起。
Rule 60.任何if..else if語句,最後必須有一個收尾的else。例如:
         if (ans == 'Y') {
           ...
         }
         else if (ans == 'N') {
           ...
         }
         else if (ans == 'C') {
           ...
         }
         else {
           ;
         }

Rule 67.循環計數器的值不得在循環體內修改。

Rule 70.禁止任何直接和間接的遞歸函數呼叫。

Rule 82.每個函數只能有一個推出點。

Rule 86.如果一個函數可能返回錯誤信息,則呼叫後必須加以測試。

Rule 92.不應該使用#undef

Rule 95.不得將巨集作為參數傳給巨集函數

Rule 98.在一個巨集定義中,#或##符號只能出現一次。

Rule 101.禁止指標運算(代之以陣列下標運算)。

Rule 102.禁止超過兩級的指標。

Rule 104.禁止使用指向函數的非常量指標。

Rule 106.不得將棧對象的地址傳給外部作用域的對象。

************************************************** ******************
後面的規則針對實時嵌入式系統,對其他類型的開發未必適用,如:

Rule 118.禁止使用動態記憶體分配(也就是不得使用malloc, calloc和realloc)。

Rule 119.禁止使用errno。

Rule 120.禁止使用offsetof.

Rule 121.禁止使用<locale.h>

Rule 122.禁止使用setjmp, longjmp.

Rule 123.禁止使用<signal.h>

Rule 124.禁止使用<stdio.h>(不能用printf, scanf了!)

Rule 125.禁止使用atoi, atof, atol。 (這個我很贊成,建議使用strtol, strtod等函數)

Rule 126.禁止使用abort, exit, getenv。

Rule 127.禁止使用<time.h>

資料來源:https://b8807053.pixnet.net/blog/post/3612211

沒有留言: