C語言是開發(fā)嵌入式應用的主要工具,然而C語言并非是專門為嵌入式系統(tǒng)設計,相當多的嵌入式系統(tǒng)較一般計算機系統(tǒng)對軟件安全性有更苛刻的要求。1998年,MISRA指出,一些在C看來可以接受,卻存在安全隱患的地方有127處之多。2004年,MISRA對C的限制增加到141條。
嵌入式系統(tǒng)應用工程師借用計算機專家創(chuàng)建的C語言,使嵌入式系統(tǒng)應用得以飛速發(fā)展,而MISRAC是嵌入式系統(tǒng)應用工程師對C語言嵌入式應用做出的貢獻。如今MISRA C已經(jīng)被越來越多的企業(yè)接受,成為用于嵌入式系統(tǒng)的C語言標準,特別是對安全性要求極高的嵌入式系統(tǒng),軟件應符合MISRA標準。
從本期開始,本刊將分6期,與讀者共同學習MISRAC。
第一講:“‘安全第一’的C語言編程規(guī)范”,簡述MISRAC的概況。
第二講:“跨越數(shù)據(jù)類型的重重陷阱”,介紹規(guī)范的數(shù)據(jù)定義和操作方式,重點在隱式數(shù)據(jù)類型轉(zhuǎn)換中的問題。
第三講:“指針、結(jié)構(gòu)體、聯(lián)合體的安全規(guī)范”,解析如何安全而高效地應用指針、結(jié)構(gòu)體和聯(lián)合體。
第四講:“防范表達式的失控”,剖析MISRAC中關(guān)于表達式、函數(shù)聲明和定義等的不良使用習慣,最大限度地減小各類潛在錯誤。
第五講:“準確的程序流控制”,表述C語言中控制表達式和程序流控制的規(guī)范做法。
第六講:“構(gòu)建安全的編譯環(huán)境”,講解與編譯器相關(guān)的規(guī)范編寫方式,避免來自編譯器的隱患。
C/C++語言無疑是當今嵌入式開發(fā)中最為常見的語言。早期的嵌入式程序大都是用匯編語言開發(fā)的,但人們很快就意識到匯編語言所帶來的問題——難移植、難復用、難維護和可讀性極差。很多程序會因為當初開發(fā)人員的離開而必須重新編寫,許多程序員甚至連他們自己幾個月前寫成的代碼都看不懂。C/C++語言恰恰可以解決這些問題。作為一種相對“低級”的高級語言,C/C++語言能夠讓嵌入式程序員更自由地控制底層硬件,同時享受高級語言帶來的便利。對于C語言和C++語言,很多的程序員會選擇C語言,而避開龐大復雜的C++語言。這是很容易理解的——C語言寫成的代碼量比C++語言的更小些,執(zhí)行效率也更高。
對于程序員來說,能工作的代碼并不等于“好”的代碼?!昂谩贝a的指標很多,包括易讀、易維護、易移植和可靠等。其中,可靠性對嵌入式系統(tǒng)非常重要,尤其是在那些對安全性要求很高的系統(tǒng)中,如飛行器、汽車和工業(yè)控制中。這些系統(tǒng)的特點是:只要工作稍有偏差,就有可能造成重大損失或者人員傷亡。一個不容易出錯的系統(tǒng),除了要有很好的硬件設計(如電磁兼容性),還要有很健壯或者說“安全”的程序。
然而,很少有程序員知道什么樣的程序是安全的程序。很多程序只是表面上可以干活,還存在著大量的隱患。當然,這其中也有C語言自身的原因。因為C語言是一門難以掌握的語言,其靈活的編程方式和語法規(guī)則對于一個新手來說很可能會成為機關(guān)重重的陷阱。同時,C語言的定義還并不完全,即使是國際通用的C語言標準,也還存在著很多未完全定義的地方。要求所有的嵌入式程序員都成為C語言專家,避開所有可能帶來危險的編程方式,是不現(xiàn)實的。最好的方法是有一個針對安全性的C語言編程規(guī)范,告訴程序員該如何做。
1 MISRAC規(guī)范 1994年,在英國成立了一個叫做汽車工業(yè)軟件可靠性聯(lián)合會(The Motor Industry Software Reliability Association,以下簡稱MISRA)的組織。它是致力于協(xié)助汽車廠商開發(fā)安全可靠的軟件的跨國協(xié)會,其成員包括:AB汽車電子、羅孚汽車、賓利汽車、福特汽車、捷豹汽車、路虎公司、Lotus公司、MIRA公司、Ricardo公司、TRW汽車電子、利茲大學和福特VISTEON汽車系統(tǒng)公司。
經(jīng)過了四年的研究和準備,MISRA于1998年發(fā)布了一個針對汽車工業(yè)軟件安全性的C語言編程規(guī)范——《汽車專用軟件的C語言編程指南》(Guidelines for the Use of the C Language in Vehicle Based Software),共有127條規(guī)則,稱為MISRAC:1998。[Page]
C語言并不乏國際標準。國際標準化組織(International Organization of Standardization,簡稱ISO)的“標準C語言”經(jīng)歷了從C90、C96到C99的變動。但是,嵌入式程序員很難將ISO標準當作編寫安全代碼的規(guī)范。一是因為標準C語言并不是針對代碼安全的,也并不是專門為嵌入式應用設計的;二是因為“標準C語言”太龐大了,很難操作。MISRAC:1998規(guī)范的產(chǎn)生恰恰彌補了這方面的空白。
隨著很多汽車廠商開始接受MISRAC編程規(guī)范,MISRAC:1998也成為汽車工業(yè)中最為著名的有關(guān)安全性的C語言規(guī)范。2004年,MISRA出版了該規(guī)范的新版本——MISRAC:2004。在新版本中,還將面向的對象由汽車工業(yè)擴大到所有的高安全性要求(Critical)系統(tǒng)。在MISRAC:2004中,共有強制規(guī)則121條,推薦規(guī)則20條,并刪除了15條舊規(guī)則。任何符合MISRAC:2004編程規(guī)范的代碼都應該嚴格的遵循121條強制規(guī)則的要求,并應該在條件允許的情況下盡可能符合20條推薦規(guī)則。
MISRAC:2004將其141條規(guī)則分為21個類別,每一條規(guī)則對應一條編程準則。詳細情況如表1所列。
最初,MISRAC:1998編程規(guī)范的建立是為了增強汽車工業(yè)軟件的安全性??赡茉斐善囀鹿实脑蛴泻芏?,如圖1所示,設計和制造時埋下的隱患約占總數(shù)的15%,其中也包括軟件的設計和制造。MISRAC:1998就是為了減小這部分隱患而制定的。
MISRAC編程規(guī)范的推出迎合了很多汽車廠商的需要,因為一旦廠商在程序設計上出現(xiàn)了問題,用來補救的費用將相當可觀。1999年7月22日,通用汽車公司(General Motors)就曾經(jīng)因為其軟件設計上的一個問題,被迫召回350萬輛已經(jīng)出廠的汽車,損失之大可想而知。
MISRAC規(guī)范不僅在汽車工業(yè)開始普及,也同時影響到了嵌入式開發(fā)的其他方向。嵌入式實時操作系統(tǒng)μC/OSII的2.52版本雖然已經(jīng)于2000年通過了美國航空管理局(FAA)的安全認證,但2003年作者就根據(jù)MISRAC:1998規(guī)范又對源碼做了相應的修改,如將
if ((pevent->OSEventTbl[y] &= ~bitx) == 0) {
/*… */
}
的寫法,改寫成
pevent->OSEventTbl[y] &= ~bitx;
if (pevent->OSEventTbl[y] == 0) {
/*… */
}
發(fā)布了2.62的新版本,并宣稱其源代碼99%符合MISRAC:1998規(guī)范。
一個程序能夠符合MISRAC編程規(guī)范,不僅需要程序員按照規(guī)范編程,編譯器也需要對所編譯的代碼進行規(guī)則檢查。現(xiàn)在,很多編譯器開發(fā)商都對MISRAC規(guī)范有了支持,比如IAR的編譯器就提供了對MISRAC:1998規(guī)范127條規(guī)則的檢查功能。
2 MISRAC對安全性的理解
MISRAC:2004的專家們大都來自于軟件工業(yè)或者汽車工業(yè)的知名公司,規(guī)范的制定不僅僅像過去一樣局限于汽車工業(yè)的C語言編程,同時還涵蓋了其他高安全性系統(tǒng)。
MISRAC:2004認為C程序設計中存在的風險可能由5個方面造成:程序員的失誤、程序員對語言的誤解、程序員對編譯器的誤解、編譯器的錯誤和運行出錯(runtime errors)。
程序員的失誤是司空見慣的。程序員是人,難免會犯錯誤。很多由程序員犯下的錯誤可以被編譯器及時地糾正(如鍵入錯誤的變量名等),但也有很多會逃過編譯器的檢查。相信任何一個程序員都曾經(jīng)犯過將“= =”誤寫成“=”的錯誤,編譯器可能不會認為
if(x=y)
是一個程序員的失誤。
再舉個例子,大家都知道++運算符。假如有下面的指令:
i=3;
printf(“%d”,++i);
輸出應該是多少?如果是:
printf(“%d”,i++);
呢?如果改成-i++呢?i+++i呢?i+++++i呢?絕大多數(shù)程序員恐怕已經(jīng)糊涂了。在MISRAC:2004中,會明確指出++或--運算符不得和其他運算符混合使用。
C語言非常靈活,它給了程序員非常大的自由。但事情有好有壞,自由越大,犯錯誤的機會也就越多。[Page]
如果說有些錯誤是程序員無心之失的話,那么因為程序員對C語言本身或是編譯器特性的誤解而造成的錯誤就是“明”知故犯了。C語言有一些概念很難掌握,非常容易造成誤解,如表達式的計算。請看下面這條語句:
if ( ishigh && (x == i++))
很多程序員認為執(zhí)行了這條指令后,i變量的值就會自動加1。但真正的情況如何呢?MISRA中有一條規(guī)則:邏輯運算符&&或||的右操作數(shù)不得帶有副作用(side effect)*,就是為了避免這種情況下可能出現(xiàn)的問題。
*所謂帶有副作用,就是指執(zhí)行某條語句時會改變運行環(huán)境,如執(zhí)行x=i++之后,i的值會發(fā)生變化。
另外,不同編譯器對同一語句的處理可能是不一樣的。例如整型變量的長度,不同編譯器的規(guī)定就不同。這就要求程序員不僅要清楚C語言本身的特性,還要了解所用的編譯器,難度很大。
還有些錯誤是由編譯器(或者說是編寫編譯器的程序員)本身造成的。這些錯誤往往較難發(fā)現(xiàn),有可能會一直存留在最后的程序中。
運行錯誤指的是那些在運行時出現(xiàn)的錯誤,如除數(shù)等于零、指針地址無效等問題。運行錯誤在語法檢查時一般無法發(fā)現(xiàn),但一旦發(fā)生很可能導致系統(tǒng)崩潰。例如:
#define NULL 0
……
char* p;
p=NULL;
printf(“Location of 0 is %d\n”, *p);
語法上沒有任何問題,但在某些系統(tǒng)上卻可能運行出錯。
C語言可以產(chǎn)生非常緊湊、高效的代碼,一個原因就是C語言提供的運行錯誤檢查功能很少,雖然運行效率得以提高,但也降低了系統(tǒng)的安全性。
有句話說得好,“正確的觀念重于一切”。MISRAC規(guī)范對于嵌入式程序員來講,一個很重要的意義就是提供給他們一些建議,讓他們逐漸樹立一些好的編程習慣和編程思路,慢慢摒棄那些可能存在風險的編程行為,編寫出更為安全、健壯的代碼。比如,很多嵌入式程序員都會忽略注釋的重要性,但這樣的做**降低程序的可讀性,也會給將來的維護和移植帶來風險。嵌入式程序員經(jīng)常要接觸到各種的編譯器,而很多C程序在不同編譯器下的處理是不一樣的。MISRAC:2004有一條強制規(guī)則,要求程序員把所有和編譯器特性相關(guān)的C語言行為記錄下來。這樣在程序員做移植工作時,風險就降低了。
3 MISRAC的負面效應 程序員可能會擔心采用MISRAC:2004規(guī)范會對他們的程序有負面影響,比如可能會影響代碼量、執(zhí)行效率和程序可讀性等。應該說,這種擔心不無道理??v觀141條MISRAC:2004編程規(guī)范,大多數(shù)的規(guī)則并不會對程序的代碼量、執(zhí)行效率和可讀性造成什么大的影響;一部分規(guī)則可能會以增加存儲器的占用空間為代價來增加執(zhí)行效率,或者增加代碼的可讀性;但是,也確實存在著一些規(guī)則可能會降低程序的執(zhí)行效率。
一個典型的例子就是關(guān)于聯(lián)合體的使用。MISRAC:2004有一條規(guī)則明確指出:不得使用聯(lián)合體。這是因為,在聯(lián)合體的存儲方式(如位填充、對齊方式、位順序等)上,各種編譯器的處理可能不同。比如,經(jīng)常會有程序員這樣做:一邊將采集得到的數(shù)據(jù)按照某種類型存入一個聯(lián)合體,而同時又采用另外一種數(shù)據(jù)類型將該數(shù)據(jù)讀出。如下面這段程序:
typedef union{
uint32_t word;
uint8_t bytes[4];
}word_msg_t;
unit32_t read_word_big_endian (void) {
word_msg_t tmp;
tmp.bytes[0] = read_byte();
tmp.bytes[1] = read_byte();
tmp.bytes[2] = read_byte();
tmp.bytes[3] = read_byte();
return (tmp.word);
}
原理上,這種聯(lián)合體很像是一個硬件上的雙口RAM存儲器。但程序員必須清楚,這種做法是有風險的。MISRAC:2004推薦用下面這種方法來做:
uint32_t read_word_big_endian (void) {
uint32_t word;
word=((unit32_t)read_byte())<<24;[Page]
word=word|(((unit32_t)read_byte())<<16);
word=word|(((unit32_t)read_byte())<<8);
word=word| ((unit32_t)read_byte());
return(word);
}
先不論為什么這樣做會更安全,只談執(zhí)行效率,這種采用二進制數(shù)移位的方法遠遠不如使用聯(lián)合體。到底是使用更安全的做法,還是采用效率更高的做法,需要程序員權(quán)衡。對于一些要求執(zhí)行效率很高的系統(tǒng),使用聯(lián)合體仍然是可以接受的方法。當然,這是建立在程序員充分了解所用編譯器的基礎上的,而且程序員必須對這種做法配有相應的注釋。
4 發(fā)展中的MISRAC MISRAC并非完美,它自身的發(fā)展也印證了這一點。MISRAC:2004就去掉了MISRAC:1998中的15條規(guī)則。今后的發(fā)展,MISRAC仍然要解決很多問題。比如,MISRAC:2004是基于C90標準的,但最新的國際C標準是C99,而C99中沒有確切定義的C語言特性幾乎比C90多了一倍,MISRAC如何適應新的標準還需要進一步探討。
另外,C++在嵌入式應用中也越來越受到重視,MISRA正在著手制定MISRAC++編程規(guī)范。讀者可以通過訪問網(wǎng)站http://www.了解MISRAC的發(fā)展動向。
5 對MISRAC的思考 嵌入式系統(tǒng)并不算是一個獨立的學科,但作為一個發(fā)展中的行業(yè),它確實需要有一些自己的創(chuàng)新之處。嵌入式工程師們不應僅僅局限于從計算機專家那里學習相關(guān)理論知識,并運用于自己的項目,還應該共同努力去完善自己行業(yè)的標準和規(guī)范,為嵌入式系統(tǒng)的發(fā)展做出貢獻。MISRAC編程規(guī)范就是一個很好的典范。它始于汽車工程師和軟件工程師經(jīng)驗的總結(jié),然后逐漸發(fā)展成為一種對整個嵌入式行業(yè)都有指導意義的規(guī)范。對于推動整個嵌入式行業(yè)的正規(guī)化發(fā)展,MISRAC無疑有著重要意義。
從另一個角度講,MISRAC規(guī)范也可以看成是嵌入式工程師對軟件業(yè)的一種完善。嵌入式工程師雖然不是計算機專家,但卻對嵌入式應用有著最深刻的了解,將自己在嵌入式應用中的經(jīng)驗和體會貢獻給其他行業(yè),也是他們應該肩負的責任。
參考文獻
1 MISRAC:2004, Guidelines for the use of the C language in critical systems. The Motor Industry Software Reliability Association, 2004
2 Harbison III. Samuel P, Steele Jr. Guy L. C語言參考手冊. 邱仲潘,等譯. 第5版. 北京:機械工業(yè)出版社,2003
3 Kernighan. Brian W, Ritchie. Dennis M. C程序設計語言. 徐寶文,等譯. 第2版. 北京:機械工業(yè)出版社,2001
4 Koenig Andrew. C陷阱與缺陷. 高巍譯. 北京:人民郵電出版社,2002
5 McCall Gavin. Introduction to MISRAC:2004, Visteon UK, http://www.MISRAC2.com/
6 Hennell Mike. MISRA CIts role in the bigger picture of critical software development, LDRA. http://www.MISRAC2.com/
7 Hatton Les. The MISRA C Compliance Suite—The next step, Oakwood Computing. http://www.MISRAC2.com/
8 Montgomery Steve. The role of MISRA C in developing automotive software, Ricardo Tarragon. http://www.MISRAC2.com/ |
|