版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認領(lǐng)
文檔簡介
C語言:如何用更高層次編寫嵌入式C代碼
1.簡介
市面上介紹c語言以及編程方法的書數(shù)目繁多,但對如何編寫優(yōu)質(zhì)嵌入式
C程序卻鮮有介紹,特別是對應用于單片機、ARM7、Corlex-M3這類微
控制器上的優(yōu)質(zhì)C程序編寫方法幾乎是個空白。本文面向的,正是使用亙
片機、ARM7、Conex-M3這類微控制器的底層編程人員。
編寫優(yōu)質(zhì)嵌入式C程序絕非易事,它跟設計者的思維和經(jīng)驗積累關(guān)系密
切。嵌入式C程序員不僅需要熟知硬件的特性、硬件的缺陷等,更要深入
一門語言編程,不浮于表面。為了更方便的操作硬件,還需要對編譯器進
行深入的了解。
本文將從語言特性、編譯器、防御性編程、測試和編程思想這幾個方面來
討論如何編寫優(yōu)質(zhì)嵌入式C程序。與很多雜志、書籍不同,本文提供大員
真實實例、代碼段和參考書目,不僅介紹應該做什么,還重點介紹如何
做、以及為什么這樣做。編寫優(yōu)質(zhì)嵌入式C程序涉及面十分廣,需要程序
員長時間的經(jīng)驗積累,本文希望能縮短這一過程。
2.C語言特性
語言是編程的基石,C語言詭異且有種種陷阱和缺陷,需要程序員多年歷
練才能達到較為完善的地步。雖然有眾多書籍、雜志、專題討論過C語言
的陷阱和缺陷,但這并不影響本節(jié)再次討論它,總是有大批的初學者,前
仆后繼的倒在這些陷阱和缺陷上,民用設備、工業(yè)設備甚至是航天設備都
不例外。本節(jié)將結(jié)合具體例子再次審視它們,希望引起足夠重視。深入理
解C語言特性,是編寫優(yōu)質(zhì)嵌入式C程序的基礎。
2.1處處都是陷阱
2.1.1無心之過
1)“="和”==,,
將比較運算符"=="誤寫成賦值運算符"=",可能是絕大多數(shù)人都遇到過
的,比如下面代碼:
1.if(x=5)
2.(
3.//其它代碼
4.}
代碼的本意是比較變量x是否等于常量5,但是誤將“=="寫成了“=",if語
句恒為真。如果在邏輯判斷表達式中出現(xiàn)賦值運算符,現(xiàn)在的大多數(shù)編送
器會給出警告信息。比如keilMDK會給出警告提示:“warning:#187-D:
useofwhere,,=="mayhavebeenintended”,但并非所有程序員都會注
意到這類警告,因此有經(jīng)驗的程序員使用下面的代碼來避免此類錯誤:
1.if(5==x)
2.(
3.//其它代碼
4.)
將常量放在變量x的左邊,即使程序員誤將―寫成了』L編譯器會產(chǎn)生
一個任誰也不能無視的語法錯誤信息:不可給常量賦值!
2)復合賦值運算符
復合賦值運算符(+=、*=等等)雖然可以使表達式更加簡潔并有可能產(chǎn)生
更高效的機器代碼,但某些復合賦值運算符也會給程序帶來隱含Bug,比
如“+="容易誤寫成“=+”,代碼如下:
1.tmp=+l;
代碼本意是想表達tmp=tmp+l,但是將復合賦值運算符“+="誤寫成“=+”:
將正整數(shù)常量1賦值給變量imp。編譯器會欣然接受這類代碼,連警告都
不會產(chǎn)生。
如果你能在調(diào)試階段就發(fā)現(xiàn)這個Bug,真應該慶祝一下,否則這很可能會
成為一個重大隱含Bug,且不易被察覺。
復合賦值運算符也有類似問題存在。
3)其它容易誤寫
?使用了中文標點
?頭文件聲明語句最后忘記結(jié)束分號
?邏輯與&&和位與&、邏輯或||和位或|、邏輯非!和位取反?
?字母I和數(shù)字1、字母0和數(shù)字0
這些誤寫其實容易被編譯器檢測出,只需要關(guān)注編譯器對此的提示信息,
就能很快解決。
很多的軟件Bug源自于輸入錯誤。在Google上搜索的時候,有些結(jié)果列
表項中帶有一條警告,表明Google認為它帶有惡意代碼。如果你在2009
年1月31日一大早使用Google搜索的話,你就會看到,在那天早晨55
分鐘的時間內(nèi),Google的搜索結(jié)果標明每個站點對你的PC都是有害的。
這涉及到整個Internet上的所有站點,包括Google自己的所有站點和服
務。Google的惡意軟件檢測功能通過在一個已知攻擊者的列表上查找站
點,從而識別出危險站點。在1月31FI早晨,對這個列表的更新意外地
包含了一條斜杠(“/”)。所有的URL都包含一條斜杠,并且,反惡意軟件功
能把這條斜杠理解為所有的URL都是可疑的,因此,它愉快地對搜索結(jié)
果中的每個站點都添加一條警告。很少見到如比簡單的一個輸入錯誤帶來
的結(jié)果如此奇怪且影響如此廣泛,但程序就是這樣,容不得一絲疏忽。
2.L2數(shù)組下標
數(shù)組常常也是引起程序不穩(wěn)定的重要因素,C語言數(shù)組的迷惑性與數(shù)組下
標從0開始密不可分,你可以定義inttest[30],但是你絕不可以使用數(shù)組
元素test[30],除非你自己明確知道在做什么。
2.1.3容易被忽略的break關(guān)鍵字
1)不能漏加的break
switch…case語句可以很方便的實現(xiàn)多分支結(jié)構(gòu),但要注意在合適的位置
添加break關(guān)鍵字。程序員往往容易漏加break從而引起順序執(zhí)行多個
case語句,這也許是C的一個缺陷之處。
對于*witch…case語句,從概率論上說,絕大多數(shù)程序一次只需執(zhí)行一個
匹配的case語句,而每一個這樣的case語句后都必須跟一個break。去復
雜化大概率事件,這多少有些不合常情。
2)不能亂加的break
break關(guān)鍵字用于跳出最近的那層循環(huán)語句或者switch語句,但程序員往
往不夠重視這一點。
1990年1月15日,AT&T電話網(wǎng)絡位于紐約的一臺交換機宕機并且重
啟,引起它鄰近交換機癱瘓,由此及彼,一個連著一個,很快,114型交
換機每六秒宕機重啟一次,六萬人九小時內(nèi)不能打長途電話。當時的解決
方式:工程師重裝了以前的軟件版本。。。事后的事故調(diào)查發(fā)現(xiàn),這是
break關(guān)鍵字誤用造成的?!禖專家編程》提供了一個簡化版的問題源碼:
1.networkcode()
2.{
3.switch(line)
4.{
5.caseTHING1:
6.{
7.doitl();
8.)break;
9.caseTHING2:
10.
11.if(x==STUFF)
12.
13.do_firststuff();
14.if(y==OTHER_STUFF)
15.break;
16.dolaterstuff();
17.1/*代碼的意圖是跌轉(zhuǎn)到這里…*/
18.initializ^modespointer();
19.}break;
20.default:
21.processing();
22.}/*……但事實上跳到了這里。*/
23.use_modes_pointer();/*致使未初始化*/
24.}
那個程序員希望從if語句跳出,但他卻忘記了break關(guān)鍵字實際上跳出最
近的那層循環(huán)語句或者switch語句?,F(xiàn)在它跳出了switch語句,執(zhí)行了
use_modes_pointe「0函數(shù)。但必要的初始化匚作并未完成,為將來程序的
失敗埋下了伏筆。
2.L4意想不到的八進制
將一個整形常量賦值給變量,代碼如下所示:
1.inta=34,b-034;
變量a和b相等嗎?
答案是不相等的。我們知道,16進制常量以,Ox,為前綴,10進制常量不需
要前綴,那么8進制呢?它與10進制和16進制表示方法都不相同,它以
數(shù)字'0'為前綴,這多少有點奇葩:三種進制的表示方法完全不相同。如果
8進制也像16進制那樣以數(shù)字和字母表示前綴的話,或許更有利于減少軟
件Bug,畢竟你使用8進制的次數(shù)可能都不會有誤使用的次數(shù)多!下面展
示一個誤用8進制的例子,最后一個數(shù)組元素賦值錯誤:
1.a[0]=106;/*十進制數(shù)106*/
2.a[l]=112;/*十進制數(shù)112*/
3.a[2]=052;/*實際為十進制數(shù)42,本意為十進制52*/
2.1.5指針加減運算
**指針的加減運算是特殊的。**下面的代碼運行在32位ARM架構(gòu)上,執(zhí)
行之后,a和p的值分別是多少?
1.inta=l;
2.int*p=(int*)0x00001000;
3.a=a+l;
4.p=p+l;
對于a的值很容判斷出結(jié)果為2,但是p的結(jié)果卻是0x00001004。指針p
加1后,p的值增加了4,這是為什么呢?原因是指針做加減運算時是以
指針的數(shù)據(jù)類型為單位。p+1實際上是按照公式p+l*sizeof(int)來計算
的。不理解這一點,在使用指針直接操作數(shù)據(jù)時極易犯錯。
某項目使用下面代碼對連續(xù)RAM初始化零操作,但運行發(fā)現(xiàn)有些RAM
并沒有被真正清零。
1.unsignedint*pRAMaddr;//定義地址指針變量
2.for(pRAMaddr=StartAddr;pRAMaddr<EndAddr;pRAMaddr+=4)
3.(
4.*pRAMaddr=0x00000000;//指定RAM地址清零
5.)
通過分析我們發(fā)現(xiàn),由于pRAMaddr是一個無符號int型指針變量,所以
pRAMaddr+=4代碼其實使pRAMaddr偏移了4*sizeof(int)=16個字節(jié),所
以每執(zhí)行一次for循環(huán),會使變量pRAMaddr偏移16個字節(jié)空間,但只有
4字節(jié)空間被初始化為零。其它的12字節(jié)數(shù)據(jù)的內(nèi)容,在大多數(shù)架構(gòu)處理
器中都會是隨機數(shù)。
2.1.6關(guān)鍵字sizeof
不知道有多少人最初認為sizeof是一個函數(shù)。其實它是一個關(guān)鍵字,其作
用是返回一個對象或者類型所占的內(nèi)存字節(jié)數(shù),對絕大多數(shù)編譯器而言,
返回值為無符號整形數(shù)據(jù)。需要注意的是,使用sizeof獲取數(shù)組長度時,
不要對指針應用sizeof操作符,比如下面的例子:
1.voidClearRAM;chararray[])
2.(
3.inti;
4.for(i=0;i<sizeof(array)/sizeof(array[0]);i++)//這里,用
法錯誤,array實際上是指針
5.{
6.array(i]=0x00;
7.}
8.}
9.
10.intmain(void)
11.(
12.charFle(20];
13.
14.ClearRAM:Fle);//只能清除數(shù)組Fie中的前四個元素
15.}
我們知道,對于一個數(shù)組array[20],我們使用代碼
sizeof(array)/sizeof(array[0])可以獲得數(shù)組的元素(這里為20),但數(shù)組
名和指針往往是容易混淆的,有且只有一種情況下數(shù)組名是可以當做指針
的,那就是**數(shù)組名作為函數(shù)形參時,數(shù)組名被認為是指針,同時,它不
能再兼任數(shù)組名。**注意只有這種情況下,數(shù)組名才可以當做指針,但不
幸的是這種情況下容易引發(fā)風險。在ClearRAM函數(shù)內(nèi),作為形參的
array|]不再是數(shù)組名了,而成了指針。sizeof(array)相當于求指針變量占用
的字節(jié)數(shù),在32位系統(tǒng)下,該值為數(shù)sizeof(an*ay)/sizeof(array[0])的運
算結(jié)果也為4。所以在main函數(shù)中調(diào)用ClearRAM(Fle),也只能清除數(shù)分
Fie中的前四個元素了。
2.1.7增量運算符』和減量運算符」,
增量運算符'斗+”和減量運算符“既可以做前綴也可以做后綴。**前綴和
后綴的區(qū)別在于值的增加或減少這一動作發(fā)生的時間是不同的。**作為前
綴是先自加或自減然后做別的運算,作為后綴時,是先做運算,之后再自
加或自減。許多程序員對此認識不夠,就容易埋下隱患。下面的例子可以
很好的解釋前綴和后綴的區(qū)別。
1.inta=8,b=2,y;
2.y=a+++—b;
代碼執(zhí)行后,y的值是多少?
這個例子并非是挖空心思設計出來專門讓你絞盡腦汁的C難題(如果你覺
得自己對C細節(jié)掌握很有信心,做一些C難題檢驗一下是個不錯的選擇。
那么,《TheCPuzzleBook》這本書一定不要錯過),你甚至可以將這個
難懂的語句作為不友好代碼的例子。但是它也可以讓你更好的理解C語
言。根據(jù)運算符優(yōu)先級以及編譯器識別字符的貪心法原則,第二句代碼可
以寫成更明確的形式:
1.y?(a++)+(--b);
當賦值給變量y時,a的值為8,b的值為1,所以變量y的值為9:賦值完
成后,變量a自加,a的值變?yōu)?,千萬不要以為y的值為10。這條賦值
語句相當于下面的兩條語句:
1.y=a+(--b);
2.a=a+l;
2.1.8邏輯與,&&,和邏輯或W的陷阱
為了提高系統(tǒng)效率,邏輯與和邏輯或操作的規(guī)定如下:**如果對第一個操
作數(shù)求值后就可以推斷出最終結(jié)果,第二個操作數(shù)就不會進行求值!**匕
如下面代碼:
1.if((i>=0)&&(i++<=max))
2.(
3.//其它代碼
4.)
在這個代碼中,只有當i>=0時,i++才會被執(zhí)行。這樣,i是否自增是不月多
明確的,這可能會埋下隱患。邏輯或與之類似,
2.1.9結(jié)構(gòu)體的填充
結(jié)構(gòu)體可能產(chǎn)生填充,因為對大多數(shù)處理器而言,訪問按字或者半字對齊
的數(shù)據(jù)速度更快,當定義結(jié)構(gòu)體時,編譯器為了性能優(yōu)化,可能會將它們
按照半字或字對齊,這樣會帶來填充問題。比如以下兩個個結(jié)構(gòu)體:
第一個結(jié)構(gòu)體:
1.struct{
2.charc:
3.shorts:
4.int
5.}str_testl;
第二個結(jié)構(gòu)體:
1.struct{
2.charc:
3.intx:
4.shorts:
5.}str_test2;
這兩個結(jié)構(gòu)體元素都是相同的變量,只是元素換了下位置,那么這兩個結(jié)
構(gòu)體變量占用的內(nèi)存大小相同嗎?
其實這兩個結(jié)構(gòu)體變量占用的內(nèi)存是不同的,對于KeilMDK編譯器,默
認情況下第一個結(jié)構(gòu)體變量占用8個字節(jié),第二個結(jié)構(gòu)體占用12個字
節(jié),差別很大。第一個結(jié)構(gòu)體變量在內(nèi)存中的存儲格式如圖2-1所示:
spandata='7>
圖2-1:結(jié)構(gòu)體變量1內(nèi)存分布
第二個結(jié)構(gòu)體變量在內(nèi)存中的存儲格式如圖2-2所示。對比兩個圖可以看
出MDK編譯器是是怎么將數(shù)據(jù)對齊的,這其中的填充內(nèi)容是之前內(nèi)存中
的數(shù)據(jù),是隨機的,所以不能在結(jié)構(gòu)之間逐字節(jié)比較;另外,合理的排布
結(jié)構(gòu)體內(nèi)的元素位置,可以最大限度減少填充,節(jié)省RAM。
spandata='7>
圖2-2:結(jié)構(gòu)體變量2內(nèi)存分布
2.2不可輕視的優(yōu)先級
C語言有32個關(guān)鍵字,卻有34個運算符。要記住所有運算符的優(yōu)先級是
困難的。稍不注意,你的代碼邏輯和實際執(zhí)行就會有很大出入。
比如下面將BCD碼轉(zhuǎn)換為十六進制數(shù)的代碼:
1.result=(uTimeValue>>4)*10+uTimeValue&OxOF;
這里uTimcValuc存放的BCD碼,想要轉(zhuǎn)換成16進制數(shù)據(jù),實際運行發(fā)
現(xiàn),如果uTimeValue的值為0x23,按照我設定的邏輯,result的值應該
是0x17,但運算結(jié)果卻是0x07。經(jīng)過種種排查后,才發(fā)現(xiàn)'+'的優(yōu)先級是
大于'&'的,相當于(uTimeValue?4)*10+uTimeValue與OxOF位與,結(jié)果
自然與邏輯不符。符合邏輯的代碼應該是:
1.result=(uTimeValue>>4)*10+(uTimeValue&OxOF);
不合理的#^10行詵會加重優(yōu)先級問題,讓問題變得更加隱蔽。
1.#defineREADSDAIOOPIN&//讀I??趐O.11的端口狀態(tài)
2.
3.if(READSDA==)//判斷端口p。.11是否為高電平
4.(
5.//其它代碼
6.}
編譯器在編譯后將宏帶入,原代碼語句變?yōu)椋?/p>
1.if(IO0PIN&(1<<11)==(1<<11))
2.{
3.//其它代碼
4.)
運算符‘二二'的優(yōu)先級是大于'&'的,代碼IOOPIN&(1?11)1))等效
為IOOPIN&OXOOOOOOOI:判斷端口P0.0是否為高電平,這與原意相差甚
遠。因此,使用宏定義的時候,最好將被定義的內(nèi)容用括號括起來。
按照常規(guī)方式使用時,可能引起誤會的運算符還有很多,如表2-1所示。
C語言的運算符當然不會只止步于數(shù)目繁多!
有一個簡便方法可以避免優(yōu)先級問題:不清楚的優(yōu)先級就加上"()",但這
樣至少有會帶來兩個問題:
?過多的括號影響代碼的可讀性,包括自己和以后的維護人員
?別人的代碼不一定用括號來解決優(yōu)先級問題,但你總要讀別人的代碼
無論如何,在嵌入式編程方面,該掌握的基礎知識,偷巧不得。建議花一
些時間,將優(yōu)先級順序以及容易出錯的優(yōu)先級運算符理清幾遍。
2.3隱式轉(zhuǎn)換
C語言的設計理念一直被人吐槽,因為它認為C程序員完全清楚自己在做
什么,其中一個證據(jù)就是隱式轉(zhuǎn)換。C語言規(guī)定,**不同類型的數(shù)據(jù)(比
如char和int型數(shù)據(jù))需要轉(zhuǎn)換成同一類型后,才可進行計算。**如果你
混合使用類型,比如用char類型數(shù)據(jù)和int類型數(shù)據(jù)做減法,C使用一個
規(guī)則集合來自動(隱式的)完成類型轉(zhuǎn)換。這可能很方便,但也很危險。
這就要求我們理解這個轉(zhuǎn)換規(guī)則并且能應用到程序中去!
1.當出現(xiàn)在表達式里時,有符號和無符號的char和short類型都將自動被
轉(zhuǎn)換為int類型,在需要的情況下,將自動被轉(zhuǎn)換為unsignedint(在
short和int具有相同大小時)」這稱為類型提升。
提升在算數(shù)運算中通常不會有什么大的壞處,但如果位運算符~和vv應
用在基本類型為unsignedchar或unsignedshort的操作數(shù),結(jié)果應該立
即強制轉(zhuǎn)換為unsignedchar或者unsignedshort類型(取決于操作時使
用的類型)。
1.uin18_tport=0x5aU;
2.uint8_tresult8;
3.result8=(-port)>>4;
假如我們不了解表達式里的類型提升,認為在運算過程中變量port一直是
unsignedchar類型的。我們來看一下運算過程:~port結(jié)果為0xa5,
0xa5>>4結(jié)果為0x0a,這是我們期望的值。但實際上,result_8的結(jié)果卻
是Oxfa!在ARM結(jié)構(gòu)下,int類型為32位。變量port在運算前被提升為
int類型:?port結(jié)果為Oxffffffa5,0xa5>>4結(jié)果為OxOffffffa,賦值給變
量result_8,發(fā)生類型截斷(這也是隱式的!),result_8=0xfa。經(jīng)過這
么詭異的隱式轉(zhuǎn)換,結(jié)果跟我們期望的值,已經(jīng)大相徑庭!正確的表達式
語句應該為:
1.result_8=(unsignedchar)(-port)>>4;/*強制轉(zhuǎn)換*/
2.在包含兩種數(shù)據(jù)類型的任何運算里,兩個值都會被轉(zhuǎn)換成兩種類型里
較高的級別。類型級別從高到低的順序是longdouble、double、
floatsunsignedlonglongslonglongsunsignedlong、long、unsigned
int>into
這種類型提升通常都是件好事,但往往有很多程序員不能真正理解這句
話,比如下面的例子(int類型表示16位)。
1.uintl6tul6a=40000;/*16位無符號變量*/
2.uintl6tul6b=30000;位無符號變量*/
3.uint32tu32x;/*32位無符號變量*/
4.uint32tu32y;
5.u32x=ul6a+ul6b;/*u32x-70000還是
4464?*/
6.u32y=(uint32_t)(ul6a+ul6b);/*u32y=70000還是
4464?*/
u32x和1132y的結(jié)果都是4464(70000%65536)!不要認為表達式中有一
個高類別uint32」類型變.量,編譯器都會幫你把所有其他低類別都提升至I
uint32_t類型。正確的書寫方式:
1.u32x=(uint32_t)ul6a+(uint32_t)ul6b;或者:
2.u32x=(uint32_t)ul6a+ul6b;
后一種寫法在本表達式中是正確的,但是在其它表達式中不一定正確,比
如:
1.uint16_tul6a,ul6b,ul6c;
2.uint32_tu32x;
3.u32x=ul6a+ul6b+(uint32_t)ul6c;/*錯決寫法,ul6a+ul6b仍可能溢
Hi*/
3.在賦值語句里,計算的最后結(jié)果被轉(zhuǎn)換成將要被賦予值的那個變量的
類型。這一過程可能導致類型提升也可能導致類型降級。降級可能會
導致問題。比如將運算結(jié)果為321的值賦值給8位char類型變量。程
序必須對運算時的數(shù)據(jù)溢出做合理的處理。很多其他語言,像Pascal
(C語言設計者之一曾撰文狠狠批評過Pascal語言),都不允許混合
使用類型,但C語言不會限制你的自由,即便這經(jīng)常引起B(yǎng)ug。
4.當作為函數(shù)的參數(shù)被傳遞時,char和shorl會被轉(zhuǎn)換為int,floal會被
轉(zhuǎn)換為doubleo
當不得已混合使用類型時,一個比較好的習慣是使用類型強制轉(zhuǎn)換。強制
類型轉(zhuǎn)換可以避免編譯器隱式轉(zhuǎn)換帶來的錯誤,同時也向以后的維護人員
傳遞一些有用信息。這有個前提:你要對強制類型轉(zhuǎn)換有足夠的了解!下
面總結(jié)一些規(guī)則:
?并非所有強制類型轉(zhuǎn)換都是由風險的,把一個整數(shù)值轉(zhuǎn)換為一種具有
相同符號的更寬類型時,是絕對安全的。
?精度高的類型強制轉(zhuǎn)換為精度低的類型時,通過丟棄適當數(shù)量的最高
有效位來獲取結(jié)果,也就是說會發(fā)生數(shù)據(jù)截斷,并且可能改變數(shù)據(jù)的
符號位。
?精度低的類型強制轉(zhuǎn)換為精度高的類型時,如果兩種類型具有相同的
符號,那么沒什么問題:需要注意的是負的有符號精度低類型強制轉(zhuǎn)
換為無符號精度高類型時,會不直觀的執(zhí)行符號擴展,例如:
1.unsignedintbob;
2.signedcharfred?-1;
3.
4.bob=(unsignedint)fred;/*發(fā)生符號擴展,此時bob為
OxFFFFFFFF*/
3.編譯器
如果你和一個優(yōu)秀的程序員共事,你會發(fā)現(xiàn)他對他使用的工具非常熟悉,
就像一個畫家了解他的畫具一樣。----比爾.蓋茨
3.1不能簡單的認為是個工具
?嵌入式程序開發(fā)跟硬件密切相關(guān),需要使用C語言來讀寫底層寄存
器、存取數(shù)據(jù)、控制硬件等,C語言和硬件之間由編譯器來聯(lián)系,一些
C標準不支持的便件特性操作,由編譯器提供。
?匯編可以很輕易的讀寫指定RAM地址、可以將代碼段放入指定的
Flash地址、可以精確的設置變量在RAM中分布等等,所有這些操
作,在深入了解編譯器后,也可以使用C語言實現(xiàn)。
?C語言標準并非完美,有著數(shù)目繁多的未定義行為,這些未定義行為完
全由編譯器自主決定,了解你所用的編譯器對這些未定義行為的處
理,是必要的。
?嵌入式編譯器對調(diào)試做了優(yōu)化,會提供一些工具,可以分析代碼性
能,查看外設組件等,了解編譯器的這些特性有助于提高在線調(diào)試的
效率。
?此外,堆棧操作、代碼優(yōu)化、數(shù)據(jù)類型的范圍等等,都是要深入了解
編譯器的理由。
?如果之前你認為編譯器只是個工具,能夠編譯就好。那么,是時候改
變這種思想了。
3.2不能依賴編譯器的語義檢查
編譯器的語義檢查很弱小,甚至還會“掩蓋”錯侯?,F(xiàn)代的編譯器設計是件
浩瀚的工程,為了讓編譯器設計簡單一些,目前幾乎所有編譯器的語義檢
查都比較弱小。為了獲得更快的執(zhí)行效率,C語言被設計的足夠靈活月.兒
乎不進行任何運行的檢查,比如數(shù)組越界、指針是否合法、運算結(jié)果是否
溢出等等。這就造成了很多編譯正確但執(zhí)行奇怪的程序。
C語言足夠靈活,對于一個數(shù)組test[30],它允許使用像這樣的形
式來快速獲取數(shù)組首元素所在地址前面的數(shù)據(jù);允許將一個常數(shù)強制轉(zhuǎn)換
為函數(shù)指針,使用代碼泡()())()))()來調(diào)用位于0地址的函數(shù)。C語言給
了程序員足夠的自由,但也由程序員承擔濫用自由帶來的責任。
3?2.1莫名的死機
下面的兩個例子都是死循環(huán),如果在不常用分支中出現(xiàn)類似代碼,將會造
成看似莫名其妙的死機或者重啟。
1.unsignedchari;//例程1
2.for(i=0;i<256;i++)
3.(
4.//其它代碼
5.)
1.unsignedchari;//例程2
2.for(i?10;i>-0;i--)
3.(
4.//其它代碼
5.)
對于無符號char類型,表示的范圍為0~255,所以無符號char類型變量i
永遠小于256(第一個for循環(huán)無限執(zhí)行),永遠大于等于0(第二個for
循環(huán)無線執(zhí)行)。需要說明的是,賦值代碼仁256是被C語言允許的,即
使這個初值已經(jīng)超出了變量i可以表示的范圍。C語言會千方百計的為程
序員創(chuàng)造出錯的機會,可見一斑。
3.2.2不起眼的改變
假如你在if語句后誤加了一個分號,可能會完全改變了程序邏輯。編譯器
也會很配合的幫忙掩蓋,甚至連警告都不提示,代碼如下:
1.if(a>b);//這里誤加了一個分號
2.a=b;//這句代碼一直被執(zhí)行
不但如此,編譯器還會忽略掉多余的空格符和換行符,就像下面的代碼也
不會給出足夠提示:
1.if(n<3)
2.return//這里少加了一個分號
3.logrec.data=x[0];
4.logrec.time=x[1];
5.logrec.code=x(2];
這段代碼的本意是n<3時程序直接返回,由于程序員的失誤,return少了
一個結(jié)束分號。編譯器將它翻譯成返回表達式logrec.data=x[0]的結(jié)果,
return后面即使是一個表達式也是C語言允許的。這樣當n>=3時,表達
式logrcc.data=x[0];就不會被執(zhí)行,給程序埋下了隱患。
3.2.3難查的數(shù)組越界
上文曾提到數(shù)組常常是引起程序不穩(wěn)定的重要因素,程序員往往不經(jīng)意間
就會寫數(shù)組越界。
一位同事的代碼在硬件上運行,一段時間后就會發(fā)現(xiàn)LCD顯示屏上的一
個數(shù)字不正常的被改變。經(jīng)過一段時間的調(diào)試,問題被定位到下面的一段
代碼中:
1.intSensorData(30];
2.//其他代碼
3.for(i=30;i>0;i--)
4.(
5.SensorData[i]=...;
6.//其他代碼
7.)
這里聲明了擁有30個元素的數(shù)組,不幸的是for循環(huán)代碼中誤用了本不存
在的數(shù)組元素ScnsorData[30],但C語言卻默許這么使用,并欣然的按照
代碼改變了數(shù)組元素ScnsorData[30]所在位置的值,SensorData[30]所在
的位置原本是一個LCD顯示變量,這正是顯示屏上的那個值不正常被改
變的原因。真慶幸這么輕而易舉的發(fā)現(xiàn)了這個Bug。
其實很多編譯器會對上述代碼產(chǎn)生一個警告:賦值超出數(shù)組界限。但并非
所有程序員都對編譯器警告保持足夠敏感,況且,編譯器也并不能檢查出
數(shù)組越界的所有情況。比如下面的例子:
你在模塊A中定義數(shù)組:
1.intSensorData[30];
在模塊B中引用該數(shù)組,但由于你引用代碼并不規(guī)范,這里沒有顯示聲明
數(shù)組大小,但編譯器也允許這么做:
1.externintSensorData[];
這次,編譯器不會給出警告信息,因為編譯器壓根就不知道數(shù)組的元素個
數(shù)。所以,當一個數(shù)組聲明為具有外部鏈接,它的大小應該顯式聲明。
再舉一個編譯器檢查不出數(shù)組越界的例子。函數(shù)func()的形參是一個數(shù)組
形式,函數(shù)代碼簡化如下所示:
1.char*func(charSensorData[30])
3.unsignedinti;
4.for(i=30;i>0;i--)
5.{
6.SensorData(i]=...;
7.//其他代碼
8.}
9.}
這個給SensorData[30]賦初值的語句,編譯器也是不給任何警告的。實際
上,編譯器是將數(shù)組名Sensor隱含的轉(zhuǎn)化為指向數(shù)組第一個元素的指針,
函數(shù)體是使用指針的形式來訪問數(shù)組的,它當然也不會知道數(shù)組元素的個
數(shù)了。造成這種局面的原因之一是C編譯器的作者們認為指針代替數(shù)組可
以提高程序效率,而且,可以簡化編譯器的復雜度。
指針和數(shù)組是容易給程序造成混亂的,我們有必要仔細的區(qū)分它們的不
同。其實換一個角度想想,它們也是容易區(qū)分的:可以將數(shù)組名等同于指
針的情況有且只有一處,就是上面例子提到的數(shù)組作為函數(shù)形參時。其它
時候,數(shù)組名是數(shù)組名,指針是指針。
下面的例子編譯器同樣檢查不出數(shù)組越界。
我們常常用數(shù)組來緩存通訊中的一幀數(shù)據(jù)。在通訊中斷中將接收的數(shù)據(jù)保
存到數(shù)組中,直到一幀數(shù)據(jù)完全接收后再進行處理。即使定義的數(shù)組長度
足夠長,接收數(shù)據(jù)的過程中也可能發(fā)生數(shù)組越界,特別是干擾嚴重時。這
是由于外界的干擾破壞了數(shù)據(jù)幀的某些位,對一幀的數(shù)據(jù)長度判斷錯誤,
接收的數(shù)據(jù)超出數(shù)組范圍,多余的數(shù)據(jù)改寫與數(shù)組相鄰的變量,造成系統(tǒng)
崩潰。由于中斷事件的異步性,這類數(shù)組越界編譯器無法檢查到。
如果局部數(shù)組越界,可能引發(fā)ARM架構(gòu)硬件異常。
同事的一個設備用于接收無線傳感器的數(shù)據(jù),一次軟件升級后,發(fā)現(xiàn)接收
設備工作一段時間后會死機。調(diào)試表明ARM7處理器發(fā)生了硬件異常,異
常處理代碼是一段死循環(huán)(死機的直接原因),接收設備有一個硬件模塊
用于接收無線傳感器的整包數(shù)據(jù)并存在自己的緩沖區(qū)中,當硬件模塊接收
數(shù)據(jù)完成后,使用外部中斷通知設備取數(shù)據(jù),外部中斷服務程序精簡后如
下所示:
1.irqExintHandler(void)
2.(
3.unsignedcharDataBuf(50];
4.GetData(DataBug);//從硬件線沖區(qū)取一頓數(shù)據(jù)
5.//其他代碼
6.)
由于存在多個無線傳感器近乎同時發(fā)送數(shù)據(jù)的可能加之GelDala。函數(shù)保護
力度不夠,數(shù)組DataBuf在取數(shù)據(jù)過程中發(fā)生越界。由于數(shù)組DalaBuf為
局部變量,被分配在堆棧中,同在此堆棧中的還有中斷發(fā)生時的運行環(huán)境
以及中斷返回地址。溢出的數(shù)據(jù)將這些數(shù)據(jù)破壞掉,中斷返回時PC指針
可能變成一個不合法值,硬件異常由此產(chǎn)生。
如果我們精心設計溢出部分的數(shù)據(jù),化數(shù)據(jù)為指令,就可以利用數(shù)組越界
來修改PC指針的值,使之指向我們希望執(zhí)行的代碼。
1988年,第一個網(wǎng)絡蠕蟲在一天之內(nèi)感染了2000到6000臺計算機,這個
蠕蟲程序利用的正是一個標準輸入庫函數(shù)的數(shù)組越界Bug。起因是一個標
準輸入輸出庫函數(shù)gets。,原來設計為從數(shù)據(jù)流中獲取一段文本,遺憾的
是,gels。函數(shù)沒有規(guī)定輸入文本的長度。gets。函數(shù)內(nèi)部定義了一個500
字節(jié)的數(shù)組,攻擊者發(fā)送了大于500字節(jié)的數(shù)據(jù),利用溢出的數(shù)據(jù)修改了
堆棧中的PC指針,從而獲取了系統(tǒng)權(quán)限。目前,雖然有更好的庫函數(shù)來
代替gets函數(shù),但gets函數(shù)仍然存在著。
3.2.4神奇的volatile
做嵌入式設備開發(fā),如果不對volatile修飾符具有足夠了解,實在是說不
過去。volatile是C涪言32個關(guān)鍵字中的一個,屬于類型限定符,常用的
const關(guān)鍵字也屬于類型限定符。
volatile限定符用來告訴編譯器,該對象的值無任何持久性,不要對它進行
任何優(yōu)化;它迫使編譯器每次需要該對象數(shù)據(jù)內(nèi)容時都必須讀該對象,而
不是只讀一次數(shù)據(jù)并將它放在寄存器中以便后續(xù)訪問之用(這樣的優(yōu)化可
以提高系統(tǒng)速度)。
這個特性在嵌入式應用中很有用,比如你的10口的數(shù)據(jù)不知道什么時候
就會改變,這就要求編譯器每次都必須真正的讀取該IO端口。這里使用
了詞語“真正的讀”,是因為由于編譯器的優(yōu)化,你的邏輯反應到代碼上是
對的,但是代碼經(jīng)過編譯器翻譯后,有可能與你的邏輯不符。你的代碼邏
輯可能是每次都會讀取10端口數(shù)據(jù),但實際上編譯器將代碼翻譯成匯編
時,可能只是讀一次10端口數(shù)據(jù)并保存到寄存器中,接下來的多次讀10
口都是使用寄存器中的值來進行處理。因為讀寫寄存器是最快的,這樣可
以優(yōu)化程序效率。與之類似的,中斷里的變量、多線程中的共享變量等都
存在這樣的問題。
不使用volatile,可能造成運行邏輯錯誤,但是不必要的使用volatile會造
成代碼效率低下(編譯器不優(yōu)化volatile限定的變量),因此清楚的知道
何處該使用volatile限定符,是一個嵌入式程序員的必修內(nèi)容。
一個程序模塊通常由兩個文件組成,源文件和頭文件。如果你在源文件定
義變量:
1.unsignedinttest;
并在頭文件中聲明該變量:
1.externunsignedlongtest;
編譯器會提示一個語法錯誤:變量>test,聲明類型不一致。但如果你在源
文件定義變量:
1.volatileunsignedinttest;
在頭文件中這樣聲明變量:
1.externunsignedinttest;/*缺少volatile限定符*/
編譯器卻不會給出錯誤信息(有些編譯器僅給出一條警告)。當你在另外
一個模塊(該模塊包含聲明變量test的頭文件)使用變量test時,它已經(jīng)
不再具有volatile限定,這樣很可能造成一些亙大錯誤。比如下面的例
子,注意該例子是為了說明volatile限定符而專門構(gòu)造出的,因為現(xiàn)實中
的volatile使用Bug大都隱含,并且難以理解。
在模塊A的源文件中,定義變量:
1.volatileunsignedintTimerCount=0;
該變量用來在一個定時器中斷服務程序中進行軟件計時:
1.TimerCount++;
在模塊A的頭文件中,聲明變量:
1.externunsignedintTimerCount;//這里漏掉了類型限定符
volatile
在模塊B中,要使用TimerCount變量進行精確的軟件延時:
1.耳include//首先包含模塊A的頭文件
2.〃其他代碼
3.TimerCount=0;
4.while(TimerCount<=TIMER_VALUE);//延時一段時間(感謝網(wǎng)友chhfish
指出這里的邏輯錯誤)
5.//其他代碼
實際上,這是一個死循環(huán)。由于模塊A頭文件中聲明變量TimerCount時
漏掉了volatile限定符,在模塊B中,變量TimerCount是被當作unsigned
int類型變量。由于寄存器速度遠快于RAM,編譯器在使用非volatile限
定變量時是先將變量從RAM中拷貝到寄存器中,如果同一個代碼塊再次
用到該變量,就不再從RAM中拷貝數(shù)據(jù)而是直接使用之前寄存器備份
值。代碼while(TimarCount〈二TIMER_VALUE)中,變量TimerCount僅第
一次執(zhí)行時被使用,之后都是使用的寄存器備份值,而這個寄存器值一直
為0,所以程序無限循環(huán)。圖3-1的流程圖說明了程序使用限定符volatile
和不使用volatile的執(zhí)行過程。
為了更容易的理解編譯器如何處理volatile限定符,這里給出未使用
volatile限定符和使用volatile限定符程序的反匯編代碼:
?沒有使用關(guān)鍵字volatile,在keilMDKV4.54下編譯,默認優(yōu)化級別,
如下所示(注意最后兩行):
122:unIdleCount=0;
2.123:
3.0x00002E10E59F11D4LDRRI,[PC,#0x01D4]
4.0x00002E14E3A05000MOVR5r#keyl(0x00000000)
5.0x00002E18E1A00005MOVR0,R5
6.0x00002ElCE5815000STRR5,[RI]
7.124:while(unidieCount!=200);//延時2s鐘
8.125:
9.0x00002E20E35000C8CMPROz#0x000000C8
10.0x00002E241AFFFFFDBNE0x00002E20</span>
?使用關(guān)鍵字volatile,在keilMDKV4.54下編譯,默認優(yōu)化級別,如下
所示(注意最后三行):
122::unIdleCount=0;
2.123:
3.0x00002E10E59F01D4LDRR0,[PC,#0x01D4]
4.0x00002E14E3A05000MOVR5,#keyl(0x00000000)
5.0x00002E18E5805000STRR5,[R0]
6.124:while(unidieCount!=200);//延時2s鐘
7.125:
8.0x00002ElCE5901000LDRRI,[R0]
9.0x00002E20E35100C8CMPRI,#0x000000C8
10.0x00002E241AFFFFFCBNE0x00002ElC
可以看?到,如果沒有使用volatile關(guān)鍵字,程序一直比較R0內(nèi)數(shù)據(jù)與
0xC8是否相等,但R0中的數(shù)據(jù)是0,所以程序會一直在這里循環(huán)比較
(死循環(huán));再看使用了volatile關(guān)鍵字的反匯編代碼,程序會先從變量
中讀出數(shù)據(jù)放到R1寄存器中,然后再讓R1內(nèi)數(shù)據(jù)與0xC8相比較,這才
是我們C代碼的正確邏輯!
3.2.5局部變量
ARM架構(gòu)下的編譯器會頻繁的使用堆棧,堆棧用于存儲函數(shù)的返回值、
AAPCS規(guī)定的必須保護的寄存器以及局部變量,包括局部數(shù)組、結(jié)構(gòu)
體、聯(lián)合體和C++的類。默認情況下,堆棧的位置、初始值都是由編譯器
設置,因此需要對編譯器的堆棧有一定了解。從堆棧中分配的局部變量的
初值是不確定的,因此需要運行時顯式初始化該變量。一旦離開局部變量
的作用域,這個變量立即被釋放,其它代碼也就可以使用它,因此堆棧中
的一個內(nèi)存位置可能對應整個程序的多個變量,
局部變量必須顯式初始化,除非你確定知道你要做什么。下面的代碼得到
的溫度值跟預期會有很大差別,因為在使用局部變量sum時,并不能保證
它的初值為0。編譯器會在第一次運行時清零堆棧區(qū)域,這加重了此類
Bug的隱蔽性。
1.unsignedintGetTempValue(void)
2.(
3.unsignedintsum;//定義局部變量,保存總
值
4.for(i=0;i<10;i++)
5.(
6.sum+=CollectTemp();//函數(shù)Co工上ectTerap可
以得到當前的溫度值
7.)
8.return(sum/10);
9.}
由于一旦程序離開局部變量的作用域即被釋放,所以下面代碼返回指向局
部變量的指針是沒有實際意義的,該指針指向的區(qū)域可能會被其它程序使
用,其值會被改變。
1.char*GetData(void)
2.{
3.charbuffer[100];//局部數(shù)組
4....
5.returnbuffer;
6.}
3.2.6使用外部工具
由于編譯器的語義檢查比較弱,我們可以使用第三方代碼分析工具,使用
這些工具來發(fā)現(xiàn)潛在的問題,這里介紹其中比較著名的是PC-Linto
PC-Lint由GimpelSoftware公司開發(fā),可以檢查C代碼的語法和語義并給
出潛在的BUG報告。PC-Lint可以顯著降低調(diào)試時間。
目前公司ARM7和Cortex-M3內(nèi)核多是使用KeilMDK編譯器來開發(fā)程
序,通過簡單配過,PC-Lint可以被集成到MDK上,以便更方便的檢查弋
碼。MDK已經(jīng)提供了PC-Linl的配置模板,所以整個配置過程十分簡單,
KeilMDK開發(fā)套件并不包含PC-Lint程序,在此之前,需要預先安裝可
用的PC-Lint程序,配置過程如下:
1.點擊菜單Tools…Set-upPC-Lint...
spandata='7>
PC-LintIncludeFolders:該列表路徑下的文件才會被PC-Lint檢查,此
外,這些路徑下的文件內(nèi)使用include包含的文件也會被檢查;
LintExecutable:指定PC-Lint程序的路徑
ConfigurationFile:指定配置文件的路徑,該配置文件由MDK編譯器提
供。
2.菜單Tools—Lint文件路徑.c/.h
檢查當前文件。
3.菜單Tools—LintAllC-SourceFiles
檢查所有C源文件。
PC-Lint的輸出信息顯示在MDK編譯器的BuildOulput窗口中,雙擊其中
的一條信息可以跳轉(zhuǎn)到源文件所在位置。
編譯器語義檢查的弱小在很大程度上助長了不可靠代碼的廣泛存在。隨著
時代的進步,現(xiàn)在越來越多的編譯器開發(fā)商意識到了語義檢查的重要性,
編譯器的語義檢查也越來越強大,比如公司使用的KeilMDK編譯器,M
然它的編輯器依然不盡人意,但在其V4.47及以上版本中增加了動態(tài)語法
檢查并加強了語義檢查,可以友好的提示更多警告信息。建議經(jīng)常關(guān)注編
譯器官方網(wǎng)站并將編譯器升級到V4.47或以上版本,升級的另一個好處是
這些版本的編輯器增加了標識符自動補全功能,可以大大節(jié)省編碼的時
間。
3.3你覺得有意義的代碼未必正確
C語言標準特別的規(guī)定某些行為是未定義的,編寫未定義行為的代碼,其
輸出結(jié)果由編譯器決定!C標準委員會定義未定義行為的原因如下:
?簡化標準,并給予實現(xiàn)一定的靈活性,比如不捕捉那些難以診斷的程
序錯誤;
?編譯器開發(fā)商可以通過未定義行為對語言進行擴展
C語言的未定義行為,使得C極度高效靈活并且給編譯器實現(xiàn)帶來了
方便,但這并不利于優(yōu)質(zhì)嵌入式C程序的編寫。因為許多C語言中看
起來有意義的東西都是未定義的,并且這也容易使你的代碼埋下隱
患,并且不利于跨編譯器移植。Java程序會極力避免未定義行為,并
用一系列手段進行運行時檢查,使用Java可以相對容易的寫出安全代
碼,但體積龐大效率低下。作為嵌入式程序員,我們需要了解這些未
定義行為,利用C語言的靈活性,寫出比Java更安全、效率更高的代
碼來。
3.3.1常見的未定義行為
1.自增自減在表達式中連續(xù)出現(xiàn)并作用于同一變量或者自增自減在表達
式中出現(xiàn)一次,包作用的變量多次出現(xiàn)
自增(++)和自減(-)這一動作發(fā)生在表達式的哪個時刻是由編譯器決
定的,比如:
1.r=1*a[i++]+2*a[i++]+3*a[i++];
不同的編譯器可能有著不同的匯編代碼,可能是先執(zhí)行i++再進行乘法和
加法運行,也可能是先進行加法和乘法運算,再執(zhí)行i++,因為這句代碼
在一個表達式中出現(xiàn)了連續(xù)的自增并作用于同一變量。更加隱蔽的是自增
自減在表達式中出現(xiàn)一次,但作用的變量多次出現(xiàn),比如:
1.a[i]=i++;”未定義行為*/
先執(zhí)行i++再賦值,還是先賦值再執(zhí)行i++是由編譯器決定的,而兩種不同
的執(zhí)行順序的結(jié)果差別是巨大的。
2.函數(shù)實參被求值的順序
函數(shù)如果有多個實參,這些實參的求值順序是由編譯器決定的,比如:
1.printf{*'%d%d\n",++n,power(2,n));/*未定義行為*/
是先執(zhí)行++n還是先執(zhí)行power(2,n)是由編譯器決定的。
3.有符號整數(shù)溢出
有符號整數(shù)溢出是未定義的行為,編譯器決定有符號整數(shù)溢出按照哪種方
式取值。比如下面代碼:
1.intvaluelzvalue2zsum
2.
3.//其它操作
4.suiu-valuel+value;/“sum可能發(fā)生流出*/
4.有符號數(shù)右移、移位的數(shù)量是負值或者大于操作數(shù)的位數(shù)
5.除數(shù)為零
6.malloc。、calloc。或realloc()分配零字節(jié)內(nèi)存
3.3.2如何避免C語言未定義行為
代碼中引入未定義行為會為代碼埋下隱患,防止代碼中出現(xiàn)未定義行為是
困難的,我們總能不經(jīng)意間就會在代碼中引入未定義行為。但是還是有一
些方法可以降低這種事件,總結(jié)如下:
?了解C語言未定義行為
標準C99附錄J.2”未定義行為”列舉了C99中的顯式未定義行為,通過查
看該文檔,了解那些行為是未定義的,并在編碼中時刻保持警惕;
?尋求工具幫助
編譯器警告信息以及PC-Lint等靜態(tài)檢查工具能夠發(fā)現(xiàn)很多未定義行為并
警告,要時刻關(guān)注這些工具反饋的信息;
?總結(jié)并使用一些編碼標準
1)避免構(gòu)造復雜的自增或者自減表達式,實際上,應該避免構(gòu)造所有復
雜表達式;
比如a[i]=i++;語句可以改為a[i]=i;i++;這兩句代碼。
2)只對無符號操作數(shù)使用位操作;
?必要的運行時檢查
檢查是否溢出、除數(shù)是否為零,申請的內(nèi)存數(shù)量是否為零等等,比如上面
的有符號整數(shù)溢出例子,可以按照如下方式編寫,以消除未定義特性:
1.intvaluel,value2zsum;
2.
3.//其它代碼
4.if((valuel>0&&value2>0&&valuel>(INTMAX-value2))II
5.(valueKO&&value2<0&&valuel<(INTMIN-value2)))
6.(
7.//處理錯誤
8.)
9.else
10.{
11.sum=valuel+value2;
12.)
上面的代碼是通用的,不依賴于任何CPU架構(gòu),但是代碼效率很低。如
果是有符號數(shù)使用補碼的CPU架構(gòu)(目前常見CPU絕大多數(shù)都是使用補
碼),還可以用下面的代碼來做溢出檢食:
intvaluel/value2,sum;
unsignedintusum=(unsignedint)valuel+value2;
if((usum入valuel)&(usum人value2)&INTMIN)
I
/*處理溢出情況*/
}
else
sum=valuel+value2;
使用的原理解釋一下,因為在加法運算中,操作數(shù)valuel和value2只有
符號相同時,才可能發(fā)生溢出,所以我們先將這兩個數(shù)轉(zhuǎn)換為無符號類
型,兩個數(shù)的和保存在變量usum中。如果發(fā)生溢出,則valuel、value2
和usum的最高位(符號位)一定不同,表達式(usum人valuel)&(usumA
value2)的最高位一定為1,這個表達式位與(&)上INT_MIN是為了將
最高位之外的其它位設置為0。
?了解你所用的編譯器對未定義行為的處理策略
很多引入了未定義行為的程序也能運行良好,這要歸功于編譯器處理未定
義行為的策略。不是你的代碼寫的正確,而是恰好編譯器處理策略跟你需
要的邏輯相同。了解編譯器的未定義行為處理策略,可以讓你更清楚的認
識到那些引入了未定義行為程序能夠運行良好是多么幸運的事,不然多換
幾個編譯器試試!
以KeilMDK為例,列舉常用的處理策略如下:
1)有符號量的右移是算術(shù)移位,即移位時要保證符號位不改變。
2)對于int類的值:超過31位的左移結(jié)果為零;無符號值或正的有符號
值超過31位的右移結(jié)果為零。負的有符號值移位結(jié)果為-1。
3)整型數(shù)除以零返回零
3.4了解你的編譯器
在嵌入式開發(fā)過程中,我們需要經(jīng)常和編譯器打交道,只有深入了解編譯
器,才能用好它,編寫更高效代碼,更靈活的操作硬件,實現(xiàn)一些高級功
能。下面以公司最常用的KeilMDK為例,來描述一下編譯器的細節(jié)。
3.4.1編譯器的一些小知識
1.默認情況下,char類型的數(shù)據(jù)項是無符號的,所以它的取值范圍是0、
255;
2.在所有的內(nèi)部和外部標識符中,大寫和小寫字符不同;
3.通常局部變量保存在寄存器中,但當局部變量太多放到棧里的時候,
它們總是字對齊的。
4.壓縮類型的自然對齊方式為lo使用關(guān)鍵字—packed來壓縮特定結(jié)
構(gòu),將所有有效類型的對齊邊界設置為1;
5.整數(shù)以二進制補碼形式表示;浮點量按IEEE格式存儲;
6.整數(shù)除法的余數(shù)的符號于被除數(shù)相同,由ISOC90標準得出;
7.如果整型值被截斷為短的有符號整型,則通過放棄適當數(shù)目的最高有
效位來得到結(jié)果,如果原始數(shù)是太大的正或負數(shù),對于新的類型,無
法保證結(jié)果的符號將于原始數(shù)相同。
8.整型數(shù)超界不引發(fā)異常:像unsignedchartest;test=1000:這類是不會報
錯的;
9.在嚴格C中,枚舉值必須被表示為整型。例如,必須在-2147483648
至IJ+2147483647的范圍內(nèi)。但MDK自動使用對象包含enum范圍的最
小整型來實現(xiàn)(二匕如char類型),除非使用編譯器命令-enum_is_int
來強制將enum的基礎類型設為至少和整型一樣寬。超出范圍的枚舉值
默認僅產(chǎn)生警告:#66:enumerationvalueisoutof"int"range;
10.對于結(jié)構(gòu)體填充,根據(jù)定義結(jié)構(gòu)的方式,keilMDK編譯器用以下方式
的一種來填充結(jié)構(gòu):
1>定義為static或者extern的結(jié)構(gòu)用零填充;
11>?;蚨焉系慕Y(jié)構(gòu),例如,用malloc?;蛘遖uto定義的結(jié)構(gòu),使用先前
存儲在那些存儲器位置的任何內(nèi)容進行填充。不能使用memcmp。來比較
以這種方式定義的填充結(jié)構(gòu)!
11.編譯器不對聲明為volatile類型的數(shù)據(jù)進行優(yōu)化;
12._nop():延時一個指令周期,編譯器絕不會優(yōu)化它。如果硬件支持
NOP指令,則該句被替換為NOP指令,如果硬件不支持NOP指令,
編譯器將它替換為一個等效于NOP的指令,具體指令由編譯器自己決
定;
13._align(n):指示編譯器在n字節(jié)邊界上對齊變量。對于局部變量,n
的值為1、2、4、8;
14.attribute((at(address))):可以使用此變量屬性指定變量的絕對地址;
15._inline:提示編譯器在合理的情況下內(nèi)聯(lián)編譯C或C++函數(shù);
3.4.2初始化的全局變量和靜態(tài)變量的初始值被放到了哪里?
我們程序中的一些全局變量和靜態(tài)變量在定義時進行了初始化,經(jīng)過編譯
器編譯后,這些初始值被存放在了代碼的哪里?我們舉個例子說明:
1.unsignedintg_unRunFlag=0xA5;
2.staticunsignedints_unCountFlag=0x5A;
我曾做過一個項目,項目中的一個設備需要在線編程,也就是通過協(xié)議,
將上位機發(fā)給設備的數(shù)據(jù)通過在應用編程(IAP)技術(shù)寫入到設備的內(nèi)部
Flash中。我將內(nèi)部Flash做了劃分,一小部分運行程序,大部分用來存儲
上位機發(fā)來的數(shù)據(jù)。隨著程序量的增加,在一次更新程序后發(fā)現(xiàn),在線編
程之后,設備運行正常,但是重啟設備后,運行出現(xiàn)了故障!經(jīng)過一系列
排查,發(fā)現(xiàn)故障的原因是一個全局變量的初值被改變了。這是件很不可思
議的事情,你在定義這個變量的時候指定了初始值,當你在第一次使用這
個變量時卻發(fā)現(xiàn)這個初值已經(jīng)被
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負責。
- 6. 下載文件中如有侵權(quán)或不適當內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- (新教材)2026年青島版八年級上冊數(shù)學 2.3 尺規(guī)作圖 課件
- 提升護理安全的策略與方法
- 護理倫理案例分析
- 大豐市小海中學高中化學檢測參考答案
- 2025年保險從業(yè)資格考試輔導協(xié)議
- 基于JVM的代碼分析技術(shù)
- 2025年AI視覺技術(shù)構(gòu)建無人售貨機健康管理場景
- 增強現(xiàn)實觸覺反饋
- 2026 年中職康復治療技術(shù)(康復評定量表應用)試題及答案
- 工廠消防知識考試及答案
- GB/T 4457.4-2002機械制圖圖樣畫法圖線
- GB/T 3805-2008特低電壓(ELV)限值
- GB/T 3651-2008金屬高溫導熱系數(shù)測量方法
- GB/T 17876-2010包裝容器塑料防盜瓶蓋
- GA/T 1567-2019城市道路交通隔離欄設置指南
- 最全《中國中鐵集團有限公司工程項目管理手冊》
- 連接器設計手冊要點
- 藥品注冊審評CDE組織機構(gòu)人員信息
- 營口水土保持規(guī)劃
- 魯迅《故鄉(xiāng)》優(yōu)秀PPT課件.ppt
- 魯迅《雪》ppt課件
評論
0/150
提交評論