日韩无码一级视频,久久久久久人妻一区精品,欧美va亚洲va日韩va,国产高清在线精品一区二区app电影,天堂影院一区二区三区四区

資深碼農(nóng)談:嵌入式C語(yǔ)言可靠性設(shè)計(jì)之我見(jiàn)

發(fā)布時(shí)間:2019-06-4 閱讀量:1424 來(lái)源: 21ic電子網(wǎng) 發(fā)布人: Cloris

設(shè)備的可靠性涉及多個(gè)方面:穩(wěn)定的硬件、優(yōu)秀的軟件架構(gòu)、嚴(yán)格的測(cè)試以及市場(chǎng)和時(shí)間的檢驗(yàn)等等。這里著重談一下對(duì)嵌入式軟件可靠性設(shè)計(jì)的一些理解,通過(guò)一定的技巧和方法提高軟件可靠性。這里所說(shuō)的嵌入式設(shè)備,是指使用單片機(jī)、ARM7、Cortex-M0,M3之類(lèi)為核心的測(cè)控或工控系統(tǒng)。

 

嵌入式軟件可靠性設(shè)計(jì)應(yīng)該從防錯(cuò)、判錯(cuò)和容錯(cuò)三方面進(jìn)行考慮. 此外,還需理解自己所使用的編譯器特性。   

   

良好的軟件架構(gòu)、清晰的代碼結(jié)構(gòu)、掌握硬件、深入理解C語(yǔ)言是防錯(cuò)的要點(diǎn),這里只談一下C語(yǔ)言。


“人的思維和經(jīng)驗(yàn)積累對(duì)軟件可靠性有很大影響"。C語(yǔ)言詭異且有種種陷阱和缺陷,需要程序員多年歷練才能達(dá)到較為完善的地步?!败浖馁|(zhì)量是由程序員的質(zhì)量以及他們相互之間的協(xié)作決定的”。因此,作者認(rèn)為防錯(cuò)的重點(diǎn)是要考慮人的因素。


“深入一門(mén)語(yǔ)言編程,不要浮于表面”。軟件的可靠性,與你理解的語(yǔ)言深度密切相關(guān),嵌入式C更是如此。除了語(yǔ)言,作者認(rèn)為嵌入式開(kāi)發(fā)還必須深入理解編譯器。


本節(jié)將對(duì)C語(yǔ)言的陷阱和缺陷做初步探討。


1.處處皆陷阱


最初開(kāi)始編程時(shí),除了英文標(biāo)點(diǎn)被誤寫(xiě)成中文標(biāo)點(diǎn)外,可能被大家普遍遇到的是將比較運(yùn)算符==誤寫(xiě)成賦值運(yùn)算符=,代碼如下所示:

           


if(x=5) { … }


這里本意是比較變量x是否等于常量5,但是誤將’==’寫(xiě)成了’=’,if語(yǔ)句恒為真。如果在邏輯判斷表達(dá)式中出現(xiàn)賦值運(yùn)算符,現(xiàn)在的大多數(shù)編譯器會(huì)給出警告信息。并非所有程序員都會(huì)注意到這類(lèi)警告,因此有經(jīng)驗(yàn)的程序員使用下面的代碼來(lái)避免此類(lèi)錯(cuò)誤:

        

if(5==x) { … }


將常量放在變量x的左邊,即使程序員誤將’==’寫(xiě)成了’=’,編譯器會(huì)產(chǎn)生一個(gè)任誰(shuí)也不能無(wú)視的語(yǔ)法錯(cuò)誤信息:不可給常量賦值!

 

+=與=+、-=與=-也是容易寫(xiě)混的。復(fù)合賦值運(yùn)算符(+=、*=等等)雖然可以使表達(dá)式更加簡(jiǎn)潔并有可能產(chǎn)生更高效的機(jī)器代碼,但某些復(fù)合賦值運(yùn)算符也會(huì)給程序帶來(lái)隱含Bug,如下所示代碼:

          

tmp=+1;


該代碼本意是想表達(dá)tmp=tmp+1,但是將復(fù)合賦值運(yùn)算符+=誤寫(xiě)成=+:將正整數(shù)常量1賦值給變量tmp。編譯器會(huì)欣然接受這類(lèi)代碼,連警告都不會(huì)產(chǎn)生。


如果你能在調(diào)試階段就發(fā)現(xiàn)這個(gè)Bug,你真應(yīng)該慶祝一下,否則這很可能會(huì)成為一個(gè)重大隱含Bug,且不易被察覺(jué)。

 

-=與=-也是同樣道理。與之類(lèi)似的還有邏輯與&&和位與&、邏輯或||和位或|、邏輯非!和位取反~。此外字母l和數(shù)字1、字母O和數(shù)字0也易混淆,這種情況可借助編譯器來(lái)糾正。

       

很多的軟件BUG自于輸入錯(cuò)誤。在Google上搜索的時(shí)候,有些結(jié)果列表項(xiàng)中帶有一條警告,表明Google認(rèn)為它帶有惡意代碼。如果你在2009年1月31日一大早使用Google搜索的話,你就會(huì)看到,在那天早晨55分鐘的時(shí)間內(nèi),Google的搜索結(jié)果標(biāo)明每個(gè)站點(diǎn)對(duì)你的PC都是有害的。這涉及到整個(gè)Internet上的所有站點(diǎn),包括Google自己的所有站點(diǎn)和服務(wù)。Google的惡意軟件檢測(cè)功能通過(guò)在一個(gè)已知攻擊者的列表上查找站點(diǎn),從而識(shí)別出危險(xiǎn)站點(diǎn)。在1月31日早晨,對(duì)這個(gè)列表的更新意外地包含了一條斜杠(“/”)。所有的URL都包含一條斜杠,并且,反惡意軟件功能把這條斜杠理解為所有的URL都是可疑的,因此,它愉快地對(duì)搜索結(jié)果中的每個(gè)站點(diǎn)都添加一條警告。很少見(jiàn)到如此簡(jiǎn)單的一個(gè)輸入錯(cuò)誤帶來(lái)的結(jié)果如此奇怪且影響如此廣泛,但程序就是這樣,容不得一絲疏忽。

 

數(shù)組常常也是引起程序不穩(wěn)定的重要因素,C語(yǔ)言數(shù)組的迷惑性與數(shù)組下標(biāo)從0開(kāi)始密不可分,你可以定義int a[30],但是你絕不可以使用數(shù)組元素a[30],除非你自己明確知道在做什么。

 

switch…case語(yǔ)句可以很方便的實(shí)現(xiàn)多分支結(jié)構(gòu),但要注意在合適的位置添加break關(guān)鍵字。程序員往往容易漏加break從而引起順序執(zhí)行多個(gè)case語(yǔ)句,這也許是C的一個(gè)缺陷之處。對(duì)于switch…case語(yǔ)句,從概率論上說(shuō),絕大多數(shù)程序一次只需執(zhí)行一個(gè)匹配的case語(yǔ)句,而每一個(gè)這樣的case語(yǔ)句后都必須跟一個(gè)break。去復(fù)雜化大概率事件,這多少有些不合常情。

 

break關(guān)鍵字用于跳出最近的那層循環(huán)語(yǔ)句或者switch語(yǔ)句,但程序員往往不夠重視這一點(diǎn)。



1990年1月15日,AT&T電話網(wǎng)絡(luò)位于紐約的一臺(tái)交換機(jī)當(dāng)機(jī)并且重啟,引起它鄰近交換機(jī)癱瘓,由此及彼,一個(gè)連著一個(gè),很快,114臺(tái)交換機(jī)每六秒當(dāng)機(jī)重啟一次,六萬(wàn)人九小時(shí)內(nèi)不能打長(zhǎng)途電話。當(dāng)時(shí)的解決方式:工程師重裝了以前的軟件版本。事后的事故調(diào)查發(fā)現(xiàn),這是break關(guān)鍵字誤用造成的。《C專(zhuān)家編程》提供了一個(gè)簡(jiǎn)化版的問(wèn)題源碼:


network code()

{

       switch(line) {

              case  THING1:

                     doit1();

              break;

              case  THING2:

                     if(x==STUFF) {

                            do_first_stuff();

                            if(y==OTHER_STUFF)

                                   break;

                            do_later_stuff();

                     } /*代碼的意圖是跳轉(zhuǎn)到這里… …*/

                     initialize_modes_pointer();

              break;

              default:

                     processing();

       }/*… …但事實(shí)上跳到了這里。*/

       use_modes_pointer();/*致使modes_pointer未初始化*/

}


那個(gè)程序員希望從if語(yǔ)句跳出,但他卻忘記了break關(guān)鍵字實(shí)際上跳出最近的那層循環(huán)語(yǔ)句或者switch語(yǔ)句?,F(xiàn)在它跳出了switch語(yǔ)句,執(zhí)行了use_modes_pointer()函數(shù)。但必要的初始化工作并未完成,為將來(lái)程序的失敗埋下了伏筆。

 

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


int a=34, b=034;


變量a和b相等嗎?答案是不相等的。我們知道,16進(jìn)制常量以’0x’為前綴,10進(jìn)制常量不需要前綴,那么8進(jìn)制呢?它與10進(jìn)制和16進(jìn)制表示方法都不相通,它以數(shù)字’0’為前綴,這多少有點(diǎn)奇葩:三種進(jìn)制的表示方法完全不相通。如果8進(jìn)制也像16進(jìn)制那樣以數(shù)字和字母表示前綴的話,或許更有利于減少軟件Bug,畢竟你使用8進(jìn)制的次數(shù)可能都不會(huì)有誤使用的次數(shù)多!下面展示一個(gè)誤用8進(jìn)制的例子,最后一個(gè)數(shù)組元素賦值錯(cuò)誤:


a[0]=106;              /*十進(jìn)制數(shù)106*/

a[1]=112;      /*十進(jìn)制數(shù)112*/

a[2]=052;              /*實(shí)際為十進(jìn)制數(shù)42,本意為十進(jìn)制52*/


指針的加減運(yùn)算是特殊的。下面的代碼運(yùn)行在32位ARM架構(gòu)上,執(zhí)行之后,a和p的值分別是多少?


int a=1;

int *p=(int*)0x00001000;

a=a+1;

p=p+1;


對(duì)于a的值很容判斷出結(jié)果為2,但是p的結(jié)果卻是0x00001004。指針p加1后,p的值增加了4,這是為什么呢?原因是指針做加減運(yùn)算時(shí)是以指針的數(shù)據(jù)類(lèi)型為單位。p+1實(shí)際上是p+1*sizeof(int)。不理解這一點(diǎn),在使用指針直接操作數(shù)據(jù)時(shí)極易犯錯(cuò)。比如下面對(duì)連續(xù)RAM初始化零操作代碼:


unsigned int *pRAMaddr;                   //定義地址指針變量

for(pRAMaddr=StartAddr;pRAMaddr<EndAddr;pRAMaddr+=4)

{

           *pRAMaddr=0x00000000;    //指定RAM地址清零

}


由于pRAMaddr是一個(gè)指針變量,所以pRAMaddr+=4代碼其實(shí)使pRAMaddr偏移了4*sizeof(int)=16個(gè)字節(jié),所以每執(zhí)行一次for循環(huán),會(huì)使變量pRAMaddr偏移16個(gè)字節(jié)空間,但只有4字節(jié)空間被初始化為零。其它的12字節(jié)數(shù)據(jù)的內(nèi)容,在大多數(shù)架構(gòu)處理器中都會(huì)是隨機(jī)數(shù)。

 

對(duì)于sizeof(),這里強(qiáng)調(diào)兩點(diǎn),第一它是一個(gè)關(guān)鍵字,而不是函數(shù),并且它默認(rèn)返回?zé)o符號(hào)整形數(shù)據(jù)(要記住是無(wú)符號(hào));第二,使用sizeof獲取數(shù)組長(zhǎng)度時(shí),不要對(duì)指針應(yīng)用sizeof操作符,比如下面的例子:


void ClearRAM(char array[])

{

    int i ;

    for(i=0;i<sizeof(array)/sizeof(array[0]);i++)             //這里用法錯(cuò)誤,array實(shí)際上是指針

       {

              array[i]=0x00;

       }

}

 

int main(void)

{

       char Fle[20];

      

       ClearRAM(Fle);                   //只能清除數(shù)組Fle中的前四個(gè)元素

}


我們知道,對(duì)于一個(gè)數(shù)組array[20],我們使用代碼sizeof(array)/sizeof(array[0])可以獲得數(shù)組的元素(這里為20),但數(shù)組名和指針往往是容易混淆的,而且有且只有一種情況下是可以當(dāng)做指針的,那就是數(shù)組名作為函數(shù)形參時(shí),數(shù)組名被認(rèn)為是指針。同時(shí),它不能再兼任數(shù)組名。注意只有這種情況下,數(shù)組名才可以當(dāng)做指針,但不幸的是這種情況下容易引發(fā)風(fēng)險(xiǎn)。在ClearRAM函數(shù)內(nèi),作為形參的array[]不再是數(shù)組名了,而成了指針。sizeof(array)相當(dāng)于求指針變量占用的字節(jié)數(shù),在32位系統(tǒng)下,該值為4,sizeof(array)/sizeof(array[0])的運(yùn)算結(jié)果也為4。所以在main函數(shù)中調(diào)用ClearRAM(Fle),也只能清除數(shù)組Fle中的前四個(gè)元素了。

 

增量運(yùn)算符++和減量運(yùn)算符--既可以做前綴也可以做后綴。前綴和后綴的區(qū)別在于值的增加或減少這一動(dòng)作發(fā)生的時(shí)間是不同的。作為前綴是先自加或自減然后做別的運(yùn)算,作為后綴時(shí),是先做運(yùn)算,之后再自加或自減。許多程序員對(duì)此認(rèn)識(shí)不夠,就容易埋下隱患。下面的例子可以很好的解釋前綴和后綴的區(qū)別。


int a=8,b=2,y;

y=a+++--b;


代碼執(zhí)行后,y的值是多少?


這個(gè)例子并非是挖空心思設(shè)計(jì)出來(lái)專(zhuān)門(mén)讓你絞盡腦汁的C難題(如果你覺(jué)得自己對(duì)C細(xì)節(jié)掌握很有信心,做一些C難題檢驗(yàn)一下是個(gè)不錯(cuò)的選擇。那么,《The C Puzzle Book》這本書(shū)一定不要錯(cuò)過(guò)。),你甚至可以將這個(gè)難懂的語(yǔ)句作為不友好代碼的反面例子。但是它也可以讓你更好的理解C語(yǔ)言。根據(jù)運(yùn)算符優(yōu)先級(jí)以及編譯器識(shí)別字符的貪心法原則,代碼y=a+++--b;可以寫(xiě)成更明確的形式:


y=(a++)+(--b);


當(dāng)賦值給變量y時(shí),a的值為8,b的值為1,所以變量y的值為9;賦值完成后,變量a自加,a的值變?yōu)?,千萬(wàn)不要以為y的值為10。這條賦值語(yǔ)句相當(dāng)于下面的兩條語(yǔ)句:

y=a+(--b);

a=a+1;


2.玩具般的編譯器語(yǔ)義檢查

 

為了更簡(jiǎn)單的設(shè)計(jì)編譯器,目前幾乎所有編譯器的語(yǔ)義檢查都比較弱小,加之為了獲得更快的執(zhí)行效率,C語(yǔ)言被設(shè)計(jì)的足夠靈活且?guī)缀醪贿M(jìn)行任何運(yùn)行時(shí)檢查,比如數(shù)組越界、指針是否合法、運(yùn)算結(jié)果是否溢出等等。


C語(yǔ)言足夠靈活,對(duì)于一個(gè)數(shù)組a[30],它允許使用像a[-1]這樣的形式來(lái)快速獲取數(shù)組首元素所在地址前面的數(shù)據(jù);允許將一個(gè)常數(shù)強(qiáng)制轉(zhuǎn)換為函數(shù)指針,使用代碼(*((void(*)())0))()來(lái)調(diào)用位于0地址的函數(shù)。C語(yǔ)言給了程序員足夠的自由,但也由程序員承擔(dān)濫用自由帶來(lái)的責(zé)任。下面的兩個(gè)例子都是死循環(huán),如果在不常用分支中出現(xiàn)類(lèi)似代碼,將會(huì)造成看似莫名其妙的死機(jī)或者重啟。


a.     unsigned char i;                    b.   unsigned chari;

       for(i=0;i<256;i++)  {… }                 for(i=10;i>=0;i--) { … }


對(duì)于無(wú)符號(hào)char類(lèi)型,表示的范圍為0~255,所以無(wú)符號(hào)char類(lèi)型變量i永遠(yuǎn)小于256(第一個(gè)for循環(huán)無(wú)限執(zhí)行),永遠(yuǎn)大于等于0(第二個(gè)for循環(huán)無(wú)線執(zhí)行)。需要說(shuō)明的是,賦值代碼i=256是被C語(yǔ)言允許的,即使這個(gè)初值已經(jīng)超出了變量i可以表示的范圍。C語(yǔ)言會(huì)千方百計(jì)的為程序員創(chuàng)造出錯(cuò)的機(jī)會(huì),可見(jiàn)一斑。

      

假如你在if語(yǔ)句后誤加了一個(gè)分號(hào)改變了程序邏輯,編譯器也會(huì)很配合的幫忙掩蓋,甚至連警告都不提示。代碼如下:


if(a>b);          //這里誤加了一個(gè)分號(hào)

a=b;               //這句代碼一直被執(zhí)行


不但如此,編譯器還會(huì)忽略掉多余的空格符和換行符,就像下面的代碼也不會(huì)給出足夠提示:


if(n<3)

return    //這里少加了一個(gè)分號(hào)

logrec.data=x[0];

logrec.time=x[1];

logrec.code=x[2];


這段代碼的本意是n<3時(shí)程序直接返回,由于程序員的失誤,return少了一個(gè)結(jié)束分號(hào)。編譯器將它翻譯成返回表達(dá)式logrec.data=x[0]的結(jié)果,return后面即使是一個(gè)表達(dá)式也是C語(yǔ)言允許的。這樣當(dāng)n>=3時(shí),表達(dá)式logrec.data=x[0];就不會(huì)被執(zhí)行,給程序埋下了隱患。


可以毫不客氣的說(shuō),弱小的編譯器語(yǔ)義檢查在很大程度上縱容了不可靠代碼可以肆無(wú)忌憚的存在。

      

上文曾提到數(shù)組常常是引起程序不穩(wěn)定的重要因素,程序員往往不經(jīng)意間就會(huì)寫(xiě)數(shù)組越界。一位同事的代碼在硬件上運(yùn)行,一段時(shí)間后就會(huì)發(fā)現(xiàn)LCD顯示屏上的一個(gè)數(shù)字不正常的被改變。經(jīng)過(guò)一段時(shí)間的調(diào)試,問(wèn)題被定位到下面的一段代碼中:    


int SensorData[30];

 …for(i=30;i>0;i--)

{

  SensorData[i]=…;

  …

}


這里聲明了擁有30個(gè)元素的數(shù)組,不幸的是for循環(huán)代碼中誤用了本不存在的數(shù)組元素SensorData[30],但C語(yǔ)言卻默許這么使用,并欣然的按照代碼改變了數(shù)組元素SensorData[30]所在位置的值, SensorData[30]所在的位置原本是一個(gè)LCD顯示變量,這正是顯示屏上的那個(gè)值不正常被改變的原因。真慶幸這么輕而易舉的發(fā)現(xiàn)了這個(gè)Bug。


其實(shí)很多編譯器會(huì)對(duì)上述代碼產(chǎn)生一個(gè)警告:賦值超出數(shù)組界限。但并非所有程序員都對(duì)編譯器警告保持足夠敏感,況且,編譯器也并不能檢查出數(shù)組越界的所有情況。舉一個(gè)例子,你在模塊A中定義數(shù)組:


int SensorData[30];


在模塊B中引用該數(shù)組,但由于你引用代碼并不規(guī)范,這里沒(méi)有顯示聲明數(shù)組大小,但編譯器也允許這么做:


extern int SensorData[];


如果在模塊B中存在和上面一樣的代碼:


for(i=30;i>0;i--)

{

 SensorData[i]=…;

 …

}


這次,編譯器不會(huì)給出警告信息,因?yàn)榫幾g器壓根就不知道數(shù)組的元素個(gè)數(shù)。所以,當(dāng)一個(gè)數(shù)組聲明為具有外部鏈接,它的大小應(yīng)該顯式聲明。


再舉一個(gè)編譯器檢查不出數(shù)組越界的例子。函數(shù)func()的形參是一個(gè)數(shù)組形式,函數(shù)代碼簡(jiǎn)化如下所示:


char * func(char SensorData[30])

{

              unsignedint i;

              for(i=30;i>0;i--)

              {

                     SensorData[i]=…;

                     …

              }

}


這個(gè)給SensorData[30]賦初值的語(yǔ)句,編譯器也是不給任何警告的。實(shí)際上,編譯器是將數(shù)組名Sensor隱含的轉(zhuǎn)化為指向數(shù)組第一個(gè)元素的指針,函數(shù)體是使用指針的形式來(lái)訪問(wèn)數(shù)組的,它當(dāng)然也不會(huì)知道數(shù)組元素的個(gè)數(shù)了。造成這種局面的原因之一是C編譯器的作者們認(rèn)為指針代替數(shù)組可以提高程序效率,而且,還可以簡(jiǎn)化編譯器的復(fù)雜度。


指針和數(shù)組是容易給程序造成混亂的,我們有必要仔細(xì)的區(qū)分它們的不同。其實(shí)換一個(gè)角度想想,它們也是容易區(qū)分的:可以將數(shù)組名等同于指針的情況有且只有一處,就是上面例子提到的數(shù)組作為函數(shù)形參時(shí)。其它時(shí)候,數(shù)組名是數(shù)組名,指針是指針。

下面的例子編譯器同樣檢查不出數(shù)組越界。


我們常常用數(shù)組來(lái)緩存通訊中的一幀數(shù)據(jù)。在通訊中斷中將接收的數(shù)據(jù)保存到數(shù)組中,直到一幀數(shù)據(jù)完全接收后再進(jìn)行處理。即使定義的數(shù)組長(zhǎng)度足夠長(zhǎng),接收數(shù)據(jù)的過(guò)程中也可能發(fā)生數(shù)組越界,特別是干擾嚴(yán)重時(shí)。這是由于外界的干擾破壞了數(shù)據(jù)幀的某些位,對(duì)一幀的數(shù)據(jù)長(zhǎng)度判斷錯(cuò)誤,接收的數(shù)據(jù)超出數(shù)組范圍,多余的數(shù)據(jù)改寫(xiě)與數(shù)組相鄰的變量,造成系統(tǒng)崩潰。由于中斷事件的異步性,這類(lèi)數(shù)組越界編譯器無(wú)法檢查到。


如果局部數(shù)組越界,可能引發(fā)ARM架構(gòu)硬件異常。同事的一個(gè)設(shè)備用于接收無(wú)線傳感器的數(shù)據(jù),一次軟件升級(jí)后,發(fā)現(xiàn)接收設(shè)備工作一段時(shí)間后會(huì)死機(jī)。調(diào)試表明ARM7處理器發(fā)生了硬件異常,異常處理代碼是一段死循環(huán)(死機(jī)的直接原因)。接收設(shè)備有一個(gè)硬件模塊用于接收無(wú)線傳感器的整包數(shù)據(jù)并存在自己的硬件緩沖區(qū)中,當(dāng)一幀數(shù)據(jù)接收完成后,使用外部中斷通知設(shè)備取數(shù)據(jù),外部中斷服務(wù)程序精簡(jiǎn)后如下所示:



__irq ExintHandler(void)

{

  unsignedchar DataBuf[50];

  GetData(DataBug);        //從硬件緩沖區(qū)取一幀數(shù)據(jù)

  …

}


由于存在多個(gè)無(wú)線傳感器近乎同時(shí)發(fā)送數(shù)據(jù)的可能加之GetData()函數(shù)保護(hù)力度不夠,數(shù)組DataBuf在取數(shù)據(jù)過(guò)程中發(fā)生越界。由于數(shù)組DataBuf為局部變量,被分配在堆棧中,同在此堆棧中的還有中斷發(fā)生時(shí)的運(yùn)行環(huán)境以及中斷返回地址。溢出的數(shù)據(jù)將這些數(shù)據(jù)破壞掉,中斷返回時(shí)PC指針可能變成一個(gè)不合法值,硬件異常由此產(chǎn)生。


如果我們精心設(shè)計(jì)溢出部分的數(shù)據(jù),化數(shù)據(jù)為指令,就可以利用數(shù)組越界來(lái)修改PC指針的值,使之指向我們希望執(zhí)行的代碼。1988年,第一個(gè)網(wǎng)絡(luò)蠕蟲(chóng)在一天之內(nèi)感染了2000到6000臺(tái)計(jì)算機(jī),這個(gè)蠕蟲(chóng)程序利用的正是一個(gè)標(biāo)準(zhǔn)輸入庫(kù)函數(shù)的數(shù)組越界Bug。起因是一個(gè)標(biāo)準(zhǔn)輸入輸出庫(kù)函數(shù)gets(),原來(lái)設(shè)計(jì)為從數(shù)據(jù)流中獲取一段文本,遺憾的是,gets()函數(shù)沒(méi)有規(guī)定輸入文本的長(zhǎng)度。gets()函數(shù)內(nèi)部定義了一個(gè)500字節(jié)的數(shù)組,攻擊者發(fā)送了大于500字節(jié)的數(shù)據(jù),利用溢出的數(shù)據(jù)修改了堆棧中的PC指針,從而獲取了系統(tǒng)權(quán)限。

      

一個(gè)程序模塊通常由兩個(gè)文件組成,源文件和頭文件。如果你在源文件定義變量:


unsigned int a;


并在頭文件中聲明該變量:extern unsigned long a;


編譯器會(huì)提示一個(gè)語(yǔ)法錯(cuò)誤:變量’a’聲明類(lèi)型不一致。但如果你在源文件定義變量:


volatile unsigned int a,


在頭文件中聲明變量:extern unsigned int a;     /*缺少volatile限定符*/


編譯器卻不會(huì)給出錯(cuò)誤信息(有些編譯器僅給出一條警告)。這里volatile屬于類(lèi)型限定符,另一個(gè)常見(jiàn)的類(lèi)型限定符是const關(guān)鍵字。限定符volatile在嵌入式軟件中至關(guān)重要,用來(lái)告訴編譯器不要優(yōu)化它修飾的變量。這里舉一個(gè)刻意構(gòu)造出的例子,因?yàn)楝F(xiàn)實(shí)中的volatile使用Bug大都隱含且難以理解。

       

在模塊A的源文件中,定義變量:


volatile unsigned int TimerCount=0;


該變量用來(lái)在一個(gè)定時(shí)器服務(wù)程序中進(jìn)行軟件計(jì)時(shí):


TimerCount++;                           //讀取IO端口1的值


在模塊A的頭文件中,聲明變量:



extern unsigned int TimerCount;   //這里漏掉了類(lèi)型限定符volatile


在模塊B中,要使用TimerCount變量進(jìn)行精確的軟件延時(shí):


#include “...A.h”   //首先包含模塊A的頭文件

TimerCount=0;

while(TimerCount>=TIMER_VALUE);      //延時(shí)一段時(shí)間


實(shí)際上,這是一個(gè)死循環(huán)。由于模塊A頭文件中聲明變量TimerCount時(shí)漏掉了volatile限定符,在模塊B中,變量TimerCount是被當(dāng)作unsigned int類(lèi)型變量。由于寄存器速度遠(yuǎn)快于RAM,編譯器在使用非volatile限定變量時(shí)是先將變量從RAM中拷貝到寄存器中,如果同一個(gè)代碼塊再次用到該變量,就不再?gòu)腞AM中拷貝數(shù)據(jù)而是直接使用之前寄存器備份值。代碼while(TimerCount>=TIMER_VALUE)中,變量TimerCount僅第一次執(zhí)行時(shí)被使用,之后都是使用的寄存器備份值,而這個(gè)寄存器值一直為0,所以程序無(wú)限循環(huán)。下面的流程圖說(shuō)明了程序使用限定符volatile和不使用volatile的執(zhí)行過(guò)程。


V.jpg

       

ARM架構(gòu)下的編譯器會(huì)頻繁的使用堆棧,堆棧用于存儲(chǔ)函數(shù)的返回值、AAPCS規(guī)定的必須保護(hù)的寄存器以及局部變量,包括局部數(shù)組、結(jié)構(gòu)體、聯(lián)合體和C++的類(lèi)。從堆棧中分配的局部變量的初值是不確定的,因此需要運(yùn)行時(shí)顯式初始化該變量。一旦離開(kāi)局部變量的作用域,這個(gè)變量立即被釋放,其它代碼也就可以使用它,因此堆棧中的一個(gè)內(nèi)存位置可能對(duì)應(yīng)整個(gè)程序的多個(gè)變量。

       

局部變量必須顯式初始化,除非你確定知道你要做什么。下面的代碼得到的溫度值跟預(yù)期會(huì)有很大差別,因?yàn)樵谑褂镁植孔兞縮um時(shí),并不能保證它的初值為0。編譯器會(huì)在第一次運(yùn)行時(shí)清零堆棧區(qū)域,這加重了此類(lèi)Bug的隱蔽性。


unsigned intGetTempValue(void)

{

  unsigned int sum;                       //定義局部變量,保存總值

  for(i=0;i<10;i++)

   {

    sum+=CollectTemp();               //函數(shù)CollectTemp可以得到當(dāng)前的溫度值

   }

   return (sum/10);

}


由于一旦程序離開(kāi)局部變量的作用域即被釋放,所以下面代碼返回指向局部變量的指針是沒(méi)有實(shí)際意義的,該指針指向的區(qū)域可能會(huì)被其它程序使用,其值會(huì)被改變。



char * GetData(void)

{

  char buffer[100];                 //局部數(shù)組

  …

  return buffer;

}


讓人欣慰的是,現(xiàn)在越來(lái)越多的編譯器意識(shí)到了語(yǔ)義檢查的重要性,編譯器的語(yǔ)義檢查也越來(lái)越強(qiáng)大,比如著名的Keil MDK編譯器在其 V4.47或以上版本中增加了動(dòng)態(tài)語(yǔ)法檢查并加強(qiáng)了語(yǔ)義檢查,可以友好的提示更多警告信息。


3.不合理的優(yōu)先級(jí)


C語(yǔ)言有32個(gè)關(guān)鍵字,卻有34個(gè)運(yùn)算符。要記住所有運(yùn)算符的優(yōu)先級(jí)是困難的。不合理的#define會(huì)加重優(yōu)先級(jí)問(wèn)題,讓問(wèn)題變得更加隱蔽。


#define READSDA       IO0PIN&(1<<11)            //定義宏,讀IO口p0.11的端口狀態(tài)

//判斷端口p0.11是否為高電平 

if(READSDA==(1<<11))  

{  

   …


編譯器在編譯后將宏帶入,原if語(yǔ)句變?yōu)?


if(IO0PIN&(1<<11) ==(1<<11))

{

   …

}


運(yùn)算符'=='的優(yōu)先級(jí)是大于'&'的,代碼IO0PIN&(1<<11) ==(1<<11))等效為IO0PIN&0x00000001:判斷端口P0.0是否為高電平,這與原意相差甚遠(yuǎn)。


為了制造更多的軟件Bug,C語(yǔ)言的運(yùn)算符當(dāng)然不會(huì)只止步于數(shù)目繁多。在此基礎(chǔ)上,按照常規(guī)方式使用時(shí),可能引起誤會(huì)的運(yùn)算符更是比比皆是!如下表所示:


下圖.jpg


4.隱式轉(zhuǎn)換和強(qiáng)制轉(zhuǎn)換


這又是C語(yǔ)言的一大詭異之處,它造成的危害程度與數(shù)組和指針有的一拼。語(yǔ)句或表達(dá)式通常應(yīng)該只使用一種類(lèi)型的變量和常量。然而,如果你混合使用類(lèi)型,C使用一個(gè)規(guī)則集合來(lái)自動(dòng)完成類(lèi)型轉(zhuǎn)換。這可能很方便,但也很危險(xiǎn)。


a.當(dāng)出現(xiàn)在表達(dá)式里時(shí),有符號(hào)和無(wú)符號(hào)的char和short類(lèi)型都將自動(dòng)被轉(zhuǎn)換為int類(lèi)型,在需要的情況下,將自動(dòng)被轉(zhuǎn)換為unsigned int(在short和int具有相同大小時(shí))。這稱(chēng)為類(lèi)型提升。提升在算數(shù)運(yùn)算中通常不會(huì)有什么大的壞處,但如果位運(yùn)算符 ~ 和 << 應(yīng)用在基本類(lèi)型為unsigned char或unsigned short 的操作數(shù),結(jié)果應(yīng)該立即強(qiáng)制轉(zhuǎn)換為unsigned char或者unsigned short類(lèi)型(取決于操作時(shí)使用的類(lèi)型)。

uint8_t  port =0x5aU;

uint8_t  result_8;

result_8= (~port) >> 4;


假如我們不了解表達(dá)式里的類(lèi)型提升,認(rèn)為在運(yùn)算過(guò)程中變量port一直是unsigned char類(lèi)型的。我們來(lái)看一下運(yùn)算過(guò)程:~port結(jié)果為0xa5,0xa5>>4結(jié)果為0x0a,這是我們期望的值。但實(shí)際上,result_8的結(jié)果卻是0xfa!在ARM結(jié)構(gòu)下,int類(lèi)型為32位。變量port在運(yùn)算前被提升為int類(lèi)型:~port結(jié)果為0xffffffa5,0xa5>>4結(jié)果為0x0ffffffa,賦值給變量result_8,發(fā)生類(lèi)型截?cái)啵ㄟ@也是隱式的?。?,result_8=0xfa。經(jīng)過(guò)這么詭異的隱式轉(zhuǎn)換,結(jié)果跟我們期望的值,已經(jīng)大相徑庭!正確的表達(dá)式語(yǔ)句應(yīng)該為:


 result_8=(unsigned char) (~port) >> 4;             /*強(qiáng)制轉(zhuǎn)換*/


b.在包含兩種數(shù)據(jù)類(lèi)型的任何運(yùn)算里,兩個(gè)值都會(huì)被轉(zhuǎn)換成兩種類(lèi)型里較高的級(jí)別。類(lèi)型級(jí)別從高到低的順序是long double、double、float、unsigned long long、long long、unsigned long、long、unsigned int、int。這種類(lèi)型提升通常都是件好事,但往往有很多程序員不能真正理解這句話,從而做一些想當(dāng)然的事情,比如下面的例子,int類(lèi)型表示16位。

uint16_t  u16a = 40000;            /* 16位無(wú)符號(hào)變量*/

uint16_t  u16b= 30000;          /*16位無(wú)符號(hào)變量*/

uint32_t  u32x;                        /*32位無(wú)符號(hào)變量 */

uint32_t  u32y;

u32x = u16a +u16b;                /* u32x = 70000還是4464 ? */

u32y =(uint32_t)(u16a + u16b);   /* u32y = 70000 還是4464 ? */


u32x和u32y的結(jié)果都是4464(70000%65536)!不要認(rèn)為表達(dá)式中有一個(gè)高類(lèi)別uint32_t類(lèi)型變量,編譯器都會(huì)幫你把所有其他低類(lèi)別都提升到uint32_t類(lèi)型。正確的書(shū)寫(xiě)方式:

u32x = (uint32_t)u16a +(uint32_t)u16b;或者:

u32x = (uint32_t)u16a + u16b;


后一種寫(xiě)法在本表達(dá)式中是正確的,但是在其它表達(dá)式中不一定正確,比如:

uint16_t u16a,u16b,u16c;

uint32_t  u32x;

u32x= u16a + u16b + (uint32_t)u16c;/*錯(cuò)誤寫(xiě)法,u16a+ u16b仍可能溢出*/


c.在賦值語(yǔ)句里,計(jì)算的最后結(jié)果被轉(zhuǎn)換成將要被賦予值得那個(gè)變量的類(lèi)型。這一過(guò)程可能導(dǎo)致類(lèi)型提升也可能導(dǎo)致類(lèi)型降級(jí)。降級(jí)可能會(huì)導(dǎo)致問(wèn)題。比如將運(yùn)算結(jié)果為321的值賦值給8位char類(lèi)型變量。程序必須對(duì)運(yùn)算時(shí)的數(shù)據(jù)溢出做合理的處理。


很多其他語(yǔ)言,像Pascal語(yǔ)言(好笑的是C語(yǔ)言設(shè)計(jì)者之一曾撰文狠狠批評(píng)過(guò)Pascal語(yǔ)言),都不允許混合使用類(lèi)型,但C語(yǔ)言不會(huì)限制你的自由,即便這經(jīng)常引起B(yǎng)ug。


d.當(dāng)作為函數(shù)的參數(shù)被傳遞時(shí),char和short會(huì)被轉(zhuǎn)換為int,float會(huì)被轉(zhuǎn)換為double。


e.C語(yǔ)言支持強(qiáng)制類(lèi)型轉(zhuǎn)換,如果你必須要進(jìn)行強(qiáng)制類(lèi)型轉(zhuǎn)換時(shí),要確保你對(duì)類(lèi)型轉(zhuǎn)換有足夠了解:


并非所有強(qiáng)制類(lèi)型轉(zhuǎn)換都是由風(fēng)險(xiǎn)的,把一個(gè)整數(shù)值轉(zhuǎn)換為一種具有相同符號(hào)的更寬類(lèi)型時(shí),是絕對(duì)安全的。

精度高的類(lèi)型強(qiáng)制轉(zhuǎn)換為精度低的類(lèi)型時(shí),通過(guò)丟棄適當(dāng)數(shù)量的最高有效位來(lái)獲取結(jié)果,也就是說(shuō)會(huì)發(fā)生數(shù)據(jù)截?cái)啵⑶铱赡芨淖償?shù)據(jù)的符號(hào)位。

 精度低的類(lèi)型強(qiáng)制轉(zhuǎn)換為精度高的類(lèi)型時(shí),如果兩種類(lèi)型具有相同的符號(hào),那么沒(méi)什么問(wèn)題;需要注意的是負(fù)的有符號(hào)精度低類(lèi)型強(qiáng)制轉(zhuǎn)換為無(wú)符號(hào)精度高類(lèi)型時(shí),會(huì)不直觀的執(zhí)行符號(hào)擴(kuò)展,例如:

unsigned int bob;

signed char fred = -1;

bob=(unsigned int )fred;              /*發(fā)生符號(hào)擴(kuò)展,此時(shí)bob為0xFFFFFFFF*/



一些編程建議:


 深入理解嵌入式C語(yǔ)言以及編譯器

 細(xì)致、謹(jǐn)慎的編程

使用好的風(fēng)格和合理的設(shè)計(jì)

不要倉(cāng)促編寫(xiě)代碼,寫(xiě)每一行的代碼時(shí)都要三思而后行:可能會(huì)出現(xiàn)什么樣的錯(cuò)誤?是否考慮了所有的邏輯分支?

打開(kāi)編譯器所有警告開(kāi)關(guān)

使用靜態(tài)分析工具分析代碼

安全的讀寫(xiě)數(shù)據(jù)(檢查所有數(shù)組邊界…)

檢查指針的合法性

檢查函數(shù)入口參數(shù)合法性

檢查所有返回值

在聲明變量位置初始化所有變量

合理的使用括號(hào)

謹(jǐn)慎的進(jìn)行強(qiáng)制轉(zhuǎn)換

使用好的診斷信息日志和工具


相關(guān)資訊
探索體外除顫器中電容器的關(guān)鍵作用

除顫器的設(shè)計(jì)旨在通過(guò)向心臟施加受控的電擊,即向心肌輸送電流,以治療心律失常癥狀,并促使心臟恢復(fù)正常跳動(dòng)。在這一關(guān)鍵的救生過(guò)程中,電容器扮演著舉足輕重的角色。在今天的文章中,我們將為您詳細(xì)闡述除顫器電路的基本構(gòu)成元素,并深入分析電容器選型在除顫器系統(tǒng)設(shè)計(jì)中所起到的關(guān)鍵作用。

提高熱電偶測(cè)溫電路性能的設(shè)計(jì)小妙招

在工業(yè)生產(chǎn)過(guò)程中,溫度是需要測(cè)量和控制的重要參數(shù)之一。在溫度測(cè)量中,熱電偶的應(yīng)用極為廣泛,它具有結(jié)構(gòu)簡(jiǎn)單、制造方便、測(cè)量范圍廣、精度高、慣性小和輸出信號(hào)便于遠(yuǎn)傳等許多優(yōu)點(diǎn)。另外,由于熱電偶是一種無(wú)源傳感器,測(cè)量時(shí)不需外加電源,使用十分方便,所以常被用作測(cè)量爐子、管道內(nèi)的氣體或液體的溫度及固體的表面溫度。

你對(duì)電機(jī)驅(qū)動(dòng)的所有要求這顆芯片都能滿足

日前,拓爾微推出一顆適用于按摩椅、掃地機(jī)、吸塵器等大電流智能市場(chǎng)應(yīng)用的直流有刷馬達(dá)驅(qū)動(dòng),這可馬達(dá)驅(qū)動(dòng)峰值電流高達(dá)10A,功耗小,滿足大部分電機(jī)驅(qū)動(dòng)的所有要求。除此之外,拓爾微還有全橋驅(qū)動(dòng)、柵極驅(qū)動(dòng)、低邊驅(qū)動(dòng)、DC/DC、音頻功放、充電協(xié)議、霍爾開(kāi)關(guān)等系列產(chǎn)品可供選型,應(yīng)用在按摩椅多個(gè)關(guān)鍵部件,為客戶(hù)提供更全面的產(chǎn)品選型支持和一站式服務(wù)。

橋式電路技術(shù)特點(diǎn)與分析方案介紹

橋式電路基于基爾霍夫定律和歐姆定律的原理,通過(guò)電流和電壓的比較來(lái)確定未知元件的值

Transphorm 最新技術(shù)白皮書(shū): 常閉耗盡型 (D-Mode)與增強(qiáng)型 (E-Mode) 氮化鎵晶體管的優(yōu)勢(shì)對(duì)比

氮化鎵功率半導(dǎo)體器件的先鋒企業(yè) Transphorm說(shuō)明了如何利用其N(xiāo)ormally-Off D-Mode平臺(tái)設(shè)計(jì)充分發(fā)揮氮化鎵晶體管的優(yōu)勢(shì),而E-Mode設(shè)計(jì)卻必須在性能上做出妥協(xié)

孙吴县| 年辖:市辖区| 习水县| 图们市| 西贡区| 黔西县| 慈溪市| 册亨县| 加查县| 荆门市| 长治县| 长春市| 横峰县| 平度市| 宜阳县| 吉隆县| 庆元县| 宁安市| 赤壁市| 历史| 巴楚县| 镇原县| 双桥区| 普定县| 旺苍县| 中阳县| 奎屯市| 开原市| 平武县| 收藏| 金阳县| 沛县| 三门县| 灯塔市| 朝阳县| 光泽县| 铜山县| 红原县| 长汀县| 灵武市| 贵南县|