www.久久久久|狼友网站av天堂|精品国产无码a片|一级av色欲av|91在线播放视频|亚洲无码主播在线|国产精品草久在线|明星AV网站在线|污污内射久久一区|婷婷综合视频网站

當前位置:首頁 > 公眾號精選 > 嵌入式微處理器
[導讀]看完瞬間就懂了

前言


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

?

嵌入式軟件可靠性設計應該從防錯、判錯和容錯三方面進行考慮. 此外,還需理解自己所使用的編譯器特性。 ??


此文屬拋磚引玉。 ??


良好的軟件架構、清晰的代碼結構、掌握硬件、深入理解C語言是防錯的要點,這里只談一下C語言。


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


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


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


1.處處皆陷阱


最初開始編程時,除了英文標點被誤寫成中文標點外,可能被大家普遍遇到的是將比較運算符==誤寫成賦值運算符=,代碼如下所示:

? ? ? ? ??

if(x=5)?{?…?}


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

? ? ? ?

if(5==x)?{?…?}


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

?

+=與=+、-=與=-也是容易寫混的。復合賦值運算符(+=、*=等等)雖然可以使表達式更加簡潔并有可能產生更高效的機器代碼,但某些復合賦值運算符也會給程序帶來隱含Bug,如下所示代碼:

? ? ? ? ?

tmp=+1;


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


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

?

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

? ? ???

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

?

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

?

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

?

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


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


network?code()
{
???????switch(line)
????????{
??????????case??THING1:?????doit1();????????????
??????????break;
???????????case??THING2:?????if(x==STUFF)
????????????{????????????????????????????
????????????do_first_stuff();????????????????????????????if(y==OTHER_STUFF)???????????????????????????????????break;
?
?do_later_stuff();}?/*代碼的意圖是跳轉到這里…?…*/???????????????initialize_modes_pointer();
??break;
???default:?????????????????????
???processing();
}
/*…?…但事實上跳到了這里。*/
?use_modes_pointer();
?/*致使modes_pointer未初始化*/
?}


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

?

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


int?a=34,?b=034;


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


a[0]=106;??????/*十進制數(shù)106*/
a[1]=112;??????/*十進制數(shù)112*/
a[2]=052;??????/*實際為十進制數(shù)42,本意為十進制52*/


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


int?a=1;int?*p=(int*)0x00001000;a=a+1;p=p+1;


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


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


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

?

對于sizeof(),這里強調兩點,第一它是一個關鍵字,而不是函數(shù),并且它默認返回無符號整形數(shù)據(jù)(要記住是無符號);第二,使用sizeof獲取數(shù)組長度時,不要對指針應用sizeof操作符,比如下面的例子


void?ClearRAM(char?array[]){????
int?i?;????
for(i=0;i<sizeof(array)/sizeof(array[0]);i++)??//這里用法錯誤,array實際上是指針???????
{??????????????
array[i]=0x00;
?}
?}?
?int?main(void){???????
?char?Fle[20];???????
??ClearRAM(Fle);?//只能清除數(shù)組Fle中的前四個元素
??}


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

?

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


int?a=8,b=2,y;y=a+++--b;


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


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


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


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



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


2.玩具般的編譯器語義檢查


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


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


a.?????unsigned char?i;??????????????????? b.???unsigned chari;

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


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

??????

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


if(a>b);????//這里誤加了一個分號
a=b;??????//這句代碼一直被執(zhí)行


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


if(n<3)return????//這里少加了一個分號logrec.data=x[0];logrec.time=x[1];logrec.code=x[2];


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


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

??????

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


int?SensorData[30];?…for(i=30;i>0;i--){??SensorData[i]=…;??…}


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


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


int?SensorData[30];


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



extern?int?SensorData[];


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


for(i=30;i>0;i--){?SensorData[i]=…;?…}


這次,編譯器不會給出警告信息,因為編譯器壓根就不知道數(shù)組的元素個數(shù)。所以,當一個數(shù)組聲明為具有外部鏈接,它的大小應該顯式聲明。


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


char?*?func(char?SensorData[30]){??????????????
unsignedint?i;??????????????
for(i=30;i>0;
i--)
{?????????????????????
SensorData[i]=…;
?…??????????????
}
}


這個給SensorData[30]賦初值的語句,編譯器也是不給任何警告的。實際上,編譯器是將數(shù)組名Sensor隱含的轉化為指向數(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架構硬件異常。同事的一個設備用于接收無線傳感器的數(shù)據(jù),一次軟件升級后,發(fā)現(xiàn)接收設備工作一段時間后會死機。調試表明ARM7處理器發(fā)生了硬件異常,異常處理代碼是一段死循環(huán)(死機的直接原因)。接收設備有一個硬件模塊用于接收無線傳感器的整包數(shù)據(jù)并存在自己的硬件緩沖區(qū)中,當一幀數(shù)據(jù)接收完成后,使用外部中斷通知設備取數(shù)據(jù),外部中斷服務程序精簡后如下所示:


__irq?ExintHandler(void)
{??
unsignedchar?DataBuf[50];??
GetData(DataBug);????????//從硬件緩沖區(qū)取一幀數(shù)據(jù)
…
}


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


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

??????

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


unsigned?int?a;


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


編譯器會提示一個語法錯誤:變量’a’聲明類型不一致。但如果你在源文件定義變量:


volatile?unsigned?int?a,


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


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

???????

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


volatile?unsigned?int?TimerCount=0;


該變量用來在一個定時器服務程序中進行軟件計時:


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


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


extern?unsigned?int?TimerCount;???//這里漏掉了類型限定符volatile


在模塊B中,要使用TimerCount變量進行精確的軟件延時:


#include?“...A.h”???//首先包含模塊A的頭文件…TimerCount=0;while(TimerCount>=TIMER_VALUE);??????//延時一段時間…


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


? ? ??

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

???????

局部變量必須顯式初始化,除非你確定知道你要做什么。下面的代碼得到的溫度值跟預期會有很大差別,因為在使用局部變量sum時,并不能保證它的初值為0。編譯器會在第一次運行時清零堆棧區(qū)域,這加重了此類Bug的隱蔽性。


unsigned?intGetTempValue(void)
{??
unsigned?int?sum;???//定義局部變量,保存總值??
for(i=0;i<10;i++)???
{????
sum+=CollectTemp();???//函數(shù)CollectTemp可以得到當前的溫度值???
}???
return?(sum/10);
}


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



char?*?GetData(void)
{??
char?buffer[100];??//局部數(shù)組??
…??
return?buffer;
}

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


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


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


#define?READSDA?IO0PIN&(1<<11)??
//定義宏,讀IO口p0.11的端口狀態(tài)
//判斷端口p0.11是否為高電平?
if(READSDA==(1<<11))??
{?????
…
}


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


if(IO0PIN&(1<<11)?==(1<<11)){???…}


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


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

4.隱式轉換和強制轉換

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


a.當出現(xiàn)在表達式里時,有符號和無符號的char和short類型都將自動被轉換為int類型,在需要的情況下,將自動被轉換為unsigned int(在short和int具有相同大小時)。這稱為類型提升。提升在算數(shù)運算中通常不會有什么大的壞處,但如果位運算符 ~ 和 << 應用在基本類型為unsigned char或unsigned short 的操作數(shù),結果應該立即強制轉換為unsigned char或者unsigned short類型(取決于操作時使用的類型)。



uint8_t?port?=0x5aU;uint8_t??
result_8;result_8=?(~port)?>>?4;


假如我們不了解表達式里的類型提升,認為在運算過程中變量port一直是unsigned char類型的。我們來看一下運算過程:~port結果為0xa5,0xa5>>4結果為0x0a,這是我們期望的值。但實際上,result_8的結果卻是0xfa!在ARM結構下,int類型為32位。變量port在運算前被提升為int類型:~port結果為0xffffffa5,0xa5>>4結果為0x0ffffffa,賦值給變量result_8,發(fā)生類型截斷(這也是隱式的?。?,result_8=0xfa。經過這么詭異的隱式轉換,結果跟我們期望的值,已經大相徑庭!正確的表達式語句應該為:


?result_8=(unsigned?char)
?(~port)?>>?4;??/*強制轉換*/


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


uint16_t??u16a?=?40000;???/*?16位無符號變量*
/uint16_t??u16b=?30000;????/*16位無符號變量*
/uint32_t??u32x;????????/*32位無符號變量*
/uint32_t??u32y;u32x?=?u16a?+u16b;
/*?u32x?=?70000還是4464???*/u32y?=(uint32_t)
(u16a?+?u16b);???/*?u32y?=?70000?還是4464???*/


u32x和u32y的結果都是4464(70000%65536)!不要認為表達式中有一個高類別uint32_t類型變量,編譯器都會幫你把所有其他低類別都提升到uint32_t類型。正確的書寫方式:


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


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


uint16_t?u16a,u16b,u16c;uint32_t??u32x;u32x=?u16a?+?u16b?+?(uint32_t)u16c;/*錯誤寫法,u16a+?u16b仍可能溢出*/


c.在賦值語句里,計算的最后結果被轉換成將要被賦予值得那個變量的類型。這一過程可能導致類型提升也可能導致類型降級。降級可能會導致問題。比如將運算結果為321的值賦值給8位char類型變量。程序必須對運算時的數(shù)據(jù)溢出做合理的處理。


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


d.當作為函數(shù)的參數(shù)被傳遞時,char和short會被轉換為int,float會被轉換為double。


e.C語言支持強制類型轉換,如果你必須要進行強制類型轉換時,要確保你對類型轉換有足夠了解:


  • 并非所有強制類型轉換都是由風險的,把一個整數(shù)值轉換為一種具有相同符號的更寬類型時,是絕對安全的。

  • 精度高的類型強制轉換為精度低的類型時,通過丟棄適當數(shù)量的最高有效位來獲取結果,也就是說會發(fā)生數(shù)據(jù)截斷,并且可能改變數(shù)據(jù)的符號位。

  • ?精度低的類型強制轉換為精度高的類型時,如果兩種類型具有相同的符號,那么沒什么問題;需要注意的是負的有符號精度低類型強制轉換為無符號精度高類型時,會不直觀的執(zhí)行符號擴展,例如:


unsigned?int?bob;signed?char?fred?=?-1;
bob=(unsigned?int?)fred;????/*發(fā)生符號擴展,此時bob為0xFFFFFFFF*/


一些編程建議:


  • ?深入理解嵌入式C語言以及編譯器

  • ?細致、謹慎的編程

  • 使用好的風格和合理的設計

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

  • 打開編譯器所有警告開關

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

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

  • 檢查指針的合法性

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

  • 檢查所有返回值

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

  • 合理的使用括號

  • 謹慎的進行強制轉換

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


嵌入式ARM

掃描二維碼,關注更多精彩內容

本站聲明: 本文章由作者或相關機構授權發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點,本站亦不保證或承諾內容真實性等。需要轉載請聯(lián)系該專欄作者,如若文章內容侵犯您的權益,請及時聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或將催生出更大的獨角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關鍵字: 阿維塔 塞力斯 華為

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉型技術解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關鍵字: AWS AN BSP 數(shù)字化

倫敦2024年8月29日 /美通社/ -- 英國汽車技術公司SODA.Auto推出其旗艦產品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時1.5...

關鍵字: 汽車 人工智能 智能驅動 BSP

北京2024年8月28日 /美通社/ -- 越來越多用戶希望企業(yè)業(yè)務能7×24不間斷運行,同時企業(yè)卻面臨越來越多業(yè)務中斷的風險,如企業(yè)系統(tǒng)復雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務連續(xù)性,提升韌性,成...

關鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報道,騰訊和網易近期正在縮減他們對日本游戲市場的投資。

關鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國國際大數(shù)據(jù)產業(yè)博覽會開幕式在貴陽舉行,華為董事、質量流程IT總裁陶景文發(fā)表了演講。

關鍵字: 華為 12nm EDA 半導體

8月28日消息,在2024中國國際大數(shù)據(jù)產業(yè)博覽會上,華為常務董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語權最終是由生態(tài)的繁榮決定的。

關鍵字: 華為 12nm 手機 衛(wèi)星通信

要點: 有效應對環(huán)境變化,經營業(yè)績穩(wěn)中有升 落實提質增效舉措,毛利潤率延續(xù)升勢 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務引領增長 以科技創(chuàng)新為引領,提升企業(yè)核心競爭力 堅持高質量發(fā)展策略,塑強核心競爭優(yōu)勢...

關鍵字: 通信 BSP 電信運營商 數(shù)字經濟

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺與中國電影電視技術學會聯(lián)合牽頭組建的NVI技術創(chuàng)新聯(lián)盟在BIRTV2024超高清全產業(yè)鏈發(fā)展研討會上宣布正式成立。 活動現(xiàn)場 NVI技術創(chuàng)新聯(lián)...

關鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會上,軟通動力信息技術(集團)股份有限公司(以下簡稱"軟通動力")與長三角投資(上海)有限...

關鍵字: BSP 信息技術
關閉
關閉