1 不能簡單的認(rèn)為是個工具
嵌入式程序開發(fā)跟硬件密切相關(guān),需要使用C語言來讀寫底層寄存器、存取數(shù)據(jù)、控制硬件等,C語言和硬件之間由編譯器來聯(lián)系,一些C標(biāo)準(zhǔn)不支持的硬件特性操作,由編譯器提供。
匯編可以很輕易的讀寫指定RAM地址、可以將代碼段放入指定的Flash地址、可以精確的設(shè)置變量在RAM中分布等等,所有這些操作,在深入了解編譯器后,也可以使用C語言實現(xiàn)。
C語言標(biāo)準(zhǔn)并非完美,有著數(shù)目繁多的未定義行為,這些未定義行為完全由編譯器自主決定,了解你所用的編譯器對這些未定義行為的處理,是必要的。
嵌入式編譯器對調(diào)試做了優(yōu)化,會提供一些工具,可以分析代碼性能,查看外設(shè)組件等,了解編譯器的這些特性有助于提高在線調(diào)試的效率。
此外,堆棧操作、代碼優(yōu)化、數(shù)據(jù)類型的范圍等等,都是要深入了解編譯器的理由。
如果之前你認(rèn)為編譯器只是個工具,能夠編譯就好。那么,是時候改變這種思想了。
2 不能依賴編譯器的語義檢查
編譯器的語義檢查很弱小,甚至還會“掩蓋”錯誤?,F(xiàn)代的編譯器設(shè)計是件浩瀚的工程,為了讓編譯器設(shè)計簡單一些,目前幾乎所有編譯器的語義檢查都比較弱小。為了獲得更快的執(zhí)行效率,C語言被設(shè)計的足夠靈活且?guī)缀醪贿M行任何運行時檢查,比如數(shù)組越界、指針是否合法、運算結(jié)果是否溢出等等。這就造成了很多編譯正確但執(zhí)行奇怪的程序。
C語言足夠靈活,對于一個數(shù)組test[30],它允許使用像test[-1]這樣的形式來快速獲取數(shù)組首元素所在地址前面的數(shù)據(jù);允許將一個常數(shù)強制轉(zhuǎn)換為函數(shù)指針,使用代碼(((void()())0))()來調(diào)用位于0地址的函數(shù)。C語言給了程序員足夠的自由,但也由程序員承擔(dān)濫用自由帶來的責(zé)任。
2.1莫名的死機
下面的兩個例子都是死循環(huán),如果在不常用分支中出現(xiàn)類似代碼,將會造成看似莫名其妙的死機或者重啟。
? ?
對于無符號char類型,表示的范圍為0~255,所以無符號char類型變量i永遠(yuǎn)小于256(第一個for循環(huán)無限執(zhí)行),永遠(yuǎn)大于等于0(第二個for循環(huán)無限執(zhí)行)。需要說明的是,賦值代碼i=256是被C語言允許的,即使這個初值已經(jīng)超出了變量i可以表示的范圍。C語言會千方百計的為程序員創(chuàng)造出錯的機會,可見一斑。
2.2不起眼的改變
假如你在if語句后誤加了一個分號,可能會完全改變了程序邏輯。編譯器也會很配合的幫忙掩蓋,甚至連警告都不提示。代碼如下:
????
不但如此,編譯器還會忽略掉多余的空格符和換行符,就像下面的代碼也不會給出足夠提示:
????
這段代碼的本意是n<3時程序直接返回,由于程序員的失誤,return少了一個結(jié)束分號。編譯器將它翻譯成返回表達式logrec.data=x[0]的結(jié)果,return后面即使是一個表達式也是C語言允許的。這樣當(dāng)n>=3時,表達式logrec.data=x[0];就不會被執(zhí)行,給程序埋下了隱患。
2.3 難查的數(shù)組越界
上文曾提到數(shù)組常常是引起程序不穩(wěn)定的重要因素,程序員往往不經(jīng)意間就會寫數(shù)組越界。
一位同事的代碼在硬件上運行,一段時間后就會發(fā)現(xiàn)LCD顯示屏上的一個數(shù)字不正常的被改變。經(jīng)過一段時間的調(diào)試,問題被定位到下面的一段代碼中:
????
這里聲明了擁有30個元素的數(shù)組,不幸的是for循環(huán)代碼中誤用了本不存在的數(shù)組元素SensorData[30],但C語言卻默許這么使用,并欣然的按照代碼改變了數(shù)組元素SensorData[30]所在位置的值, SensorData[30]所在的位置原本是一個LCD顯示變量,這正是顯示屏上的那個值不正常被改變的原因。真慶幸這么輕而易舉的發(fā)現(xiàn)了這個Bug。
其實很多編譯器會對上述代碼產(chǎn)生一個警告:賦值超出數(shù)組界限。但并非所有程序員都對編譯器警告保持足夠敏感,況且,編譯器也并不能檢查出數(shù)組越界的所有情況。比如下面的例子:
你在模塊A中定義數(shù)組:
????
在模塊B中引用該數(shù)組,但由于你引用代碼并不規(guī)范,這里沒有顯示聲明數(shù)組大小,但編譯器也允許這么做:
? ?
這次,編譯器不會給出警告信息,因為編譯器壓根就不知道數(shù)組的元素個數(shù)。所以,當(dāng)一個數(shù)組聲明為具有外部鏈接,它的大小應(yīng)該顯式聲明。
再舉一個編譯器檢查不出數(shù)組越界的例子。函數(shù)func()的形參是一個數(shù)組形式,函數(shù)代碼簡化如下所示:
????
這個給SensorData[30]賦初值的語句,編譯器也是不給任何警告的。實際上,編譯器是將數(shù)組名Sensor隱含的轉(zhuǎn)化為指向數(shù)組第一個元素的指針,函數(shù)體是使用指針的形式來訪問數(shù)組的,它當(dāng)然也不會知道數(shù)組元素的個數(shù)了。造成這種局面的原因之一是C編譯器的作者們認(rèn)為指針代替數(shù)組可以提高程序效率,而且,可以簡化編譯器的復(fù)雜度。
指針和數(shù)組是容易給程序造成混亂的,我們有必要仔細(xì)的區(qū)分它們的不同。其實換一個角度想想,它們也是容易區(qū)分的:可以將數(shù)組名等同于指針的情況有且只有一處,就是上面例子提到的數(shù)組作為函數(shù)形參時。其它時候,數(shù)組名是數(shù)組名,指針是指針。
下面的例子編譯器同樣檢查不出數(shù)組越界。
我們常常用數(shù)組來緩存通訊中的一幀數(shù)據(jù)。在通訊中斷中將接收的數(shù)據(jù)保存到數(shù)組中,直到一幀數(shù)據(jù)完全接收后再進行處理。即使定義的數(shù)組長度足夠長,接收數(shù)據(jù)的過程中也可能發(fā)生數(shù)組越界,特別是干擾嚴(yán)重時。
這是由于外界的干擾破壞了數(shù)據(jù)幀的某些位,對一幀的數(shù)據(jù)長度判斷錯誤,接收的數(shù)據(jù)超出數(shù)組范圍,多余的數(shù)據(jù)改寫與數(shù)組相鄰的變量,造成系統(tǒng)崩潰。由于中斷事件的異步性,這類數(shù)組越界編譯器無法檢查到。
如果局部數(shù)組越界,可能引發(fā)ARM架構(gòu)硬件異常。
同事的一個設(shè)備用于接收無線傳感器的數(shù)據(jù),一次軟件升級后,發(fā)現(xiàn)接收設(shè)備工作一段時間后會死機。調(diào)試表明ARM7處理器發(fā)生了硬件異常,異常處理代碼是一段死循環(huán)(死機的直接原因)。接收設(shè)備有一個硬件模塊用于接收無線傳感器的整包數(shù)據(jù)并存在自己的緩沖區(qū)中,當(dāng)硬件模塊接收數(shù)據(jù)完成后,使用外部中斷通知設(shè)備取數(shù)據(jù),外部中斷服務(wù)程序精簡后如下所示:
????
由于存在多個無線傳感器近乎同時發(fā)送數(shù)據(jù)的可能加之GetData()函數(shù)保護力度不夠,數(shù)組DataBuf在取數(shù)據(jù)過程中發(fā)生越界。由于數(shù)組DataBuf為局部變量,被分配在堆棧中,同在此堆棧中的還有中斷發(fā)生時的運行環(huán)境以及中斷返回地址。溢出的數(shù)據(jù)將這些數(shù)據(jù)破壞掉,中斷返回時PC指針可能變成一個不合法值,硬件異常由此產(chǎn)生。
如果我們精心設(shè)計溢出部分的數(shù)據(jù),化數(shù)據(jù)為指令,就可以利用數(shù)組越界來修改PC指針的值,使之指向我們希望執(zhí)行的代碼。
1988年,第一個網(wǎng)絡(luò)蠕蟲在一天之內(nèi)感染了2000到6000臺計算機,這個蠕蟲程序利用的正是一個標(biāo)準(zhǔn)輸入庫函數(shù)的數(shù)組越界Bug。起因是一個標(biāo)準(zhǔn)輸入輸出庫函數(shù)gets(),原來設(shè)計為從數(shù)據(jù)流中獲取一段文本,遺憾的是,gets()函數(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ù)仍然存在著。
2.4神奇的volatile
做嵌入式設(shè)備開發(fā),如果不對volatile修飾符具有足夠了解,實在是說不過去。volatile是C語言32個關(guān)鍵字中的一個,屬于類型限定符,常用的const關(guān)鍵字也屬于類型限定符。
volatile限定符用來告訴編譯器,該對象的值無任何持久性,不要對它進行任何優(yōu)化;它迫使編譯器每次需要該對象數(shù)據(jù)內(nèi)容時都必須讀該對象,而不是只讀一次數(shù)據(jù)并將它放在寄存器中以便后續(xù)訪問之用(這樣的優(yōu)化可以提高系統(tǒng)速度)。
這個特性在嵌入式應(yīng)用中很有用,比如你的IO口的數(shù)據(jù)不知道什么時候就會改變,這就要求編譯器每次都必須真正的讀取該IO端口。這里使用了詞語“真正的讀”,是因為由于編譯器的優(yōu)化,你的邏輯反應(yīng)到代碼上是對的,但是代碼經(jīng)過編譯器翻譯后,有可能與你的邏輯不符。
你的代碼邏輯可能是每次都會讀取IO端口數(shù)據(jù),但實際上編譯器將代碼翻譯成匯編時,可能只是讀一次IO端口數(shù)據(jù)并保存到寄存器中,接下來的多次讀IO口都是使用寄存器中的值來進行處理。因為讀寫寄存器是最快的,這樣可以優(yōu)化程序效率。與之類似的,中斷里的變量、多線程中的共享變量等都存在這樣的問題。
不使用volatile,可能造成運行邏輯錯誤,但是不必要的使用volatile會造成代碼效率低下(編譯器不優(yōu)化volatile限定的變量),因此清楚的知道何處該使用volatile限定符,是一個嵌入式程序員的必修內(nèi)容。
一個程序模塊通常由兩個文件組成,源文件和頭文件。如果你在源文件定義變量:
????
并在頭文件中聲明該變量:
????
編譯器會提示一個語法錯誤:變量’ test’聲明類型不一致。但如果你在源文件定義變量:
?????
在頭文件中這樣聲明變量:
? ?
編譯器卻不會給出錯誤信息(有些編譯器僅給出一條警告)。當(dāng)你在另外一個模塊(該模塊包含聲明變量test的頭文件)使用變量test時,它已經(jīng)不再具有volatile限定,這樣很可能造成一些重大錯誤。比如下面的例子,注意該例子是為了說明volatile限定符而專門構(gòu)造出的,因為現(xiàn)實中的volatile使用Bug大都隱含,并且難以理解。
在模塊A的源文件中,定義變量:
? ?
實際上,這是一個死循環(huán)。由于模塊A頭文件中聲明變量TimerCount時漏掉了volatile限定符,在模塊B中,變量TimerCount是被當(dāng)作unsigned int類型變量。由于寄存器速度遠(yuǎn)快于RAM,編譯器在使用非volatile限定變量時是先將變量從RAM中拷貝到寄存器中,如果同一個代碼塊再次用到該變量,就不再從RAM中拷貝數(shù)據(jù)而是直接使用之前寄存器備份值。
代碼while(TimerCount<=TIMER_VALUE)中,變量TimerCount僅第一次執(zhí)行時被使用,之后都是使用的寄存器備份值,而這個寄存器值一直為0,所以程序無限循環(huán)。下面的流程圖說明了程序使用限定符volatile和不使用volatile的執(zhí)行過程。
????
為了更容易的理解編譯器如何處理volatile限定符,這里給出未使用volatile限定符和使用volatile限定符程序的反匯編代碼:
沒有使用關(guān)鍵字volatile,在keil MDK V4.54下編譯,默認(rèn)優(yōu)化級別,如下所示(注意最后兩行):
使用關(guān)鍵字volatile,在keil MDK V4.54下編譯,默認(rèn)優(yōu)化級別,如下所示(注意最后三行):
? ?
可以看到,如果沒有使用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代碼的正確邏輯!
2.5局部變量
ARM架構(gòu)下的編譯器會頻繁的使用堆棧,堆棧用于存儲函數(shù)的返回值、AAPCS規(guī)定的必須保護的寄存器以及局部變量,包括局部數(shù)組、結(jié)構(gòu)體、聯(lián)合體和C++的類。默認(rèn)情況下,堆棧的位置、初始值都是由編譯器設(shè)置,因此需要對編譯器的堆棧有一定了解。
從堆棧中分配的局部變量的初值是不確定的,因此需要運行時顯式初始化該變量。一旦離開局部變量的作用域,這個變量立即被釋放,其它代碼也就可以使用它,因此堆棧中的一個內(nèi)存位置可能對應(yīng)整個程序的多個變量。
局部變量必須顯式初始化,除非你確定知道你要做什么。下面的代碼得到的溫度值跟預(yù)期會有很大差別,因為在使用局部變量sum時,并不能保證它的初值為0。編譯器會在第一次運行時清零堆棧區(qū)域,這加重了此類Bug的隱蔽性。
????
由于一旦程序離開局部變量的作用域即被釋放,所以下面代碼返回指向局部變量的指針是沒有實際意義的,該指針指向的區(qū)域可能會被其它程序使用,其值會被改變。
2.6使用外部工具
由于編譯器的語義檢查比較弱,我們可以使用第三方代碼分析工具,使用這些工具來發(fā)現(xiàn)潛在的問題,這里介紹其中比較著名的是PC-Lint。
PC-Lint由Gimpel Software公司開發(fā),可以檢查C代碼的語法和語義并給出潛在的BUG報告。PC-Lint可以顯著降低調(diào)試時間。
目前公司ARM7和Cortex-M3內(nèi)核多是使用Keil MDK編譯器來開發(fā)程序,通過簡單配置,PC-Lint可以被集成到MDK上,以便更方便的檢查代碼。MDK已經(jīng)提供了PC-Lint的配置模板,所以整個配置過程十分簡單,Keil MDK開發(fā)套件并不包含PC-Lint程序,在此之前,需要預(yù)先安裝可用的PC-Lint程序,配置過程如下:
點擊菜單Tools---Set-up PC-Lint…
???
PC-Lint Include Folders:該列表路徑下的文件才會被PC-Lint檢查,此外,這些路徑下的文件內(nèi)使用#include包含的文件也會被檢查;
Lint Executable:指定PC-Lint程序的路徑
Configuration File:指定配置文件的路徑,該配置文件由MDK編譯器提供。
菜單Tools---Lint 文件路徑.c/.h
檢查當(dāng)前文件。
菜單Tools---Lint All C-Source Files
檢查所有C源文件。
PC-Lint的輸出信息顯示在MDK編譯器的Build Output窗口中,雙擊其中的一條信息可以跳轉(zhuǎn)到源文件所在位置。
編譯器語義檢查的弱小在很大程度上助長了不可靠代碼的廣泛存在。隨著時代的進步,現(xiàn)在越來越多的編譯器開發(fā)商意識到了語義檢查的重要性,編譯器的語義檢查也越來越強大,比如公司使用的Keil MDK編譯器,雖然它的編輯器依然不盡人意,但在其V4.47及以上版本中增加了動態(tài)語法檢查并加強了語義檢查,可以友好的提示更多警告信息。
建議經(jīng)常關(guān)注編譯器官方網(wǎng)站并將編譯器升級到V4.47或以上版本,升級的另一個好處是這些版本的編輯器增加了標(biāo)識符自動補全功能,可以大大節(jié)省編碼的時間。
3 你覺得有意義的代碼未必正確
C語言標(biāo)準(zhǔn)特別的規(guī)定某些行為是未定義的,編寫未定義行為的代碼,其輸出結(jié)果由編譯器決定!C標(biāo)準(zhǔn)委員會定義未定義行為的原因如下:
簡化標(biāo)準(zhǔn),并給予實現(xiàn)一定的靈活性,比如不捕捉那些難以診斷的程序錯誤;
編譯器開發(fā)商可以通過未定義行為對語言進行擴展 C語言的未定義行為,使得C極度高效靈活并且給編譯器實現(xiàn)帶來了方便,但這并不利于優(yōu)質(zhì)嵌入式C程序的編寫。因為許多 C 語言中看起來有意義的東西都是未定義的,并且這也容易使你的代碼埋下隱患,并且不利于跨編譯器移植。Java程序會極力避免未定義行為,并用一系列手段進行運行時檢查,使用Java可以相對容易的寫出安全代碼,但體積龐大效率低下。作為嵌入式程序員,我們需要了解這些未定義行為,利用C語言的靈活性,寫出比Java更安全、效率更高的代碼來。
3.1常見的未定義行為
自增自減在表達式中連續(xù)出現(xiàn)并作用于同一變量或者自增自減在表達式中出現(xiàn)一次,但作用的變量多次出現(xiàn)
自增(++)和自減(--)這一動作發(fā)生在表達式的哪個時刻是由編譯器決定的,比如:
有符號數(shù)右移、移位的數(shù)量是負(fù)值或者大于操作數(shù)的位數(shù)
除數(shù)為零
malloc()、calloc()或realloc()分配零字節(jié)內(nèi)存
3.2如何避免C語言未定義行為
代碼中引入未定義行為會為代碼埋下隱患,防止代碼中出現(xiàn)未定義行為是困難的,我們總能不經(jīng)意間就會在代碼中引入未定義行為。但是還是有一些方法可以降低這種事件,總結(jié)如下:
了解C語言未定義行為
標(biāo)準(zhǔn)C99附錄J.2“未定義行為”列舉了C99中的顯式未定義行為,通過查看該文檔,了解那些行為是未定義的,并在編碼中時刻保持警惕;
尋求工具幫助
編譯器警告信息以及PC-Lint等靜態(tài)檢查工具能夠發(fā)現(xiàn)很多未定義行為并警告,要時刻關(guān)注這些工具反饋的信息;
總結(jié)并使用一些編碼標(biāo)準(zhǔn)
1)避免構(gòu)造復(fù)雜的自增或者自減表達式,實際上,應(yīng)該避免構(gòu)造所有復(fù)雜表達式; 比如a[i] = i++;語句可以改為a[i] = i; i++;這兩句代碼。 2)只對無符號操作數(shù)使用位操作;
必要的運行時檢查
檢查是否溢出、除數(shù)是否為零,申請的內(nèi)存數(shù)量是否為零等等,比如上面的有符號整數(shù)溢出例子,可以按照如下方式編寫,以消除未定義特性:
? ?
使用的原理解釋一下,因為在加法運算中,操作數(shù)value1和value2只有符號相同時,才可能發(fā)生溢出,所以我們先將這兩個數(shù)轉(zhuǎn)換為無符號類型,兩個數(shù)的和保存在變量usum中。如果發(fā)生溢出,則value1、value2和usum的最高位(符號位)一定不同,表達式(usum ^ value1) & (usum ^ value2) 的最高位一定為1,這個表達式位與(&)上INT_MIN是為了將最高位之外的其它位設(shè)置為0。
了解你所用的編譯器對未定義行為的處理策略
很多引入了未定義行為的程序也能運行良好,這要歸功于編譯器處理未定義行為的策略。不是你的代碼寫的正確,而是恰好編譯器處理策略跟你需要的邏輯相同。了解編譯器的未定義行為處理策略,可以讓你更清楚的認(rèn)識到那些引入了未定義行為程序能夠運行良好是多么幸運的事,不然多換幾個編譯器試試!
以Keil MDK為例,列舉常用的處理策略如下:
1) 有符號量的右移是算術(shù)移位,即移位時要保證符號位不改變。
2)對于int類的值:超過31位的左移結(jié)果為零;無符號值或正的有符號值超過31位的右移結(jié)果為零。負(fù)的有符號值移位結(jié)果為-1。
3)整型數(shù)除以零返回零
4 了解你的編譯器
在嵌入式開發(fā)過程中,我們需要經(jīng)常和編譯器打交道,只有深入了解編譯器,才能用好它,編寫更高效代碼,更靈活的操作硬件,實現(xiàn)一些高級功能。下面以公司最常用的Keil MDK為例,來描述一下編譯器的細(xì)節(jié)。
4.1編譯器的一些小知識
默認(rèn)情況下,char類型的數(shù)據(jù)項是無符號的,所以它的取值范圍是0~255;
在所有的內(nèi)部和外部標(biāo)識符中,大寫和小寫字符不同;
通常局部變量保存在寄存器中,但當(dāng)局部變量太多放到棧里的時候,它們總是字對齊的。
壓縮類型的自然對齊方式為1。使用關(guān)鍵字__packed來壓縮特定結(jié)構(gòu),將所有有效類型的對齊邊界設(shè)置為1;
整數(shù)以二進制補碼形式表示;浮點量按IEEE格式存儲;
整數(shù)除法的余數(shù)的符號于被除數(shù)相同,由ISO C90標(biāo)準(zhǔn)得出;
如果整型值被截斷為短的有符號整型,則通過放棄適當(dāng)數(shù)目的最高有效位來得到結(jié)果。如果原始數(shù)是太大的正或負(fù)數(shù),對于新的類型,無法保證結(jié)果的符號將于原始數(shù)相同。
整型數(shù)超界不引發(fā)異常;像unsigned char test; test=1000;這類是不會報錯的;
在嚴(yán)格C中,枚舉值必須被表示為整型。例如,必須在?2147483648 到+2147483647的范圍內(nèi)。但MDK自動使用對象包含enum范圍的最小整型來實現(xiàn)(比如char類型),除非使用編譯器命令??enum_is_int 來強制將enum的基礎(chǔ)類型設(shè)為至少和整型一樣寬。超出范圍的枚舉值默認(rèn)僅產(chǎn)生警告:#66:enumeration value is out of "int" range;
對于結(jié)構(gòu)體填充,根據(jù)定義結(jié)構(gòu)的方式,keil MDK編譯器用以下方式的一種來填充結(jié)構(gòu):
I> 定義為static或者extern的結(jié)構(gòu)用零填充; II> ?;蚨焉系慕Y(jié)構(gòu),例如,用malloc()或者auto定義的結(jié)構(gòu),使用先前存儲在那些存儲器位置的任何內(nèi)容進行填充。不能使用memcmp()來比較以這種方式定義的填充結(jié)構(gòu)!
編譯器不對聲明為volatile類型的數(shù)據(jù)進行優(yōu)化;
__nop():延時一個指令周期,編譯器絕不會優(yōu)化它。如果硬件支持NOP指令,則該句被替換為NOP指令,如果硬件不支持NOP指令,編譯器將它替換為一個等效于NOP的指令,具體指令由編譯器自己決定;
__align(n):指示編譯器在n 字節(jié)邊界上對齊變量。對于局部變量,n的值為1、2、4、8;
attribute((at(address))):可以使用此變量屬性指定變量的絕對地址;
__inline:提示編譯器在合理的情況下內(nèi)聯(lián)編譯C或C++ 函數(shù);
4.2初始化的全局變量和靜態(tài)變量的初始值被放到了哪里?
我們程序中的一些全局變量和靜態(tài)變量在定義時進行了初始化,經(jīng)過編譯器編譯后,這些初始值被存放在了代碼的哪里?我們舉個例子說明:
? ?
我曾做過一個項目,項目中的一個設(shè)備需要在線編程,也就是通過協(xié)議,將上位機發(fā)給設(shè)備的數(shù)據(jù)通過在應(yīng)用編程(IAP)技術(shù)寫入到設(shè)備的內(nèi)部Flash中。我將內(nèi)部Flash做了劃分,一小部分運行程序,大部分用來存儲上位機發(fā)來的數(shù)據(jù)。隨著程序量的增加,在一次更新程序后發(fā)現(xiàn),在線編程之后,設(shè)備運行正常,但是重啟設(shè)備后,運行出現(xiàn)了故障!經(jīng)過一系列排查,發(fā)現(xiàn)故障的原因是一個全局變量的初值被改變了。
這是件很不可思議的事情,你在定義這個變量的時候指定了初始值,當(dāng)你在第一次使用這個變量時卻發(fā)現(xiàn)這個初值已經(jīng)被改掉了!這中間沒有對這個變量做任何賦值操作,其它變量也沒有任何溢出,并且多次在線調(diào)試表明,進入main函數(shù)的時候,該變量的初值已經(jīng)被改為一個恒定值。
要想知道為什么全局變量的初值被改變,就要了解這些初值編譯后被放到了二進制文件的哪里。在此之前,需要先了解一點鏈接原理。
ARM映象文件各組成部分在存儲系統(tǒng)中的地址有兩種:一種是映象文件位于存儲器時(通俗的說就是存儲在Flash中的二進制代碼)的地址,稱為加載地址;一種是映象文件運行時(通俗的說就是給板子上電,開始運行Flash中的程序了)的地址,稱為運行時地址。
賦初值的全局變量和靜態(tài)變量在程序還沒運行的時候,初值是被放在Flash中的,這個時候他們的地址稱為加載地址,當(dāng)程序運行后,這些初值會從Flash中拷貝到RAM中,這時候就是運行時地址了。
原來,對于在程序中賦初值的全局變量和靜態(tài)變量,程序編譯后,MDK將這些初值放到Flash中,位于緊靠在可執(zhí)行代碼的后面。在程序進入main函數(shù)前,會運行一段庫代碼,將這部分?jǐn)?shù)據(jù)拷貝至相應(yīng)RAM位置。
由于我的設(shè)備程序量不斷增加,超過了為設(shè)備程序預(yù)留的Flash空間,在線編程時,將一部分存儲全局變量和靜態(tài)變量初值的Flash給重新編程了。在重啟設(shè)備前,初值已經(jīng)被拷貝到RAM中,所以這個時候程序運行是正常的,但重新上電后,這部分初值實際上是在線編程的數(shù)據(jù),自然與初值不同了。
4.3在C代碼中使用的變量,編譯器將他們分配到RAM的哪里?
我們會在代碼中使用各種變量,比如全局變量、靜態(tài)變量、局部變量,并且這些變量時由編譯器統(tǒng)一管理的,有時候我們需要知道變量用掉了多少RAM,以及這些變量在RAM中的具體位置。
這是一個經(jīng)常會遇到的事情,舉一個例子,程序中的一個變量在運行時總是不正常的被改變,那么有理由懷疑它臨近的變量或數(shù)組溢出了,溢出的數(shù)據(jù)更改了這個變量值。要排查掉這個可能性,就必須知道該變量被分配到RAM的哪里、這個位置附近是什么變量,以便針對性的做跟蹤。
其實MDK編譯器的輸出文件中有一個“工程名.map”文件,里面記錄了代碼、變量、堆棧的存儲位置,通過這個文件,可以查看使用的變量被分配到RAM的哪個位置。要生成這個文件,需要在Options for Targer窗口,Listing標(biāo)簽欄下,勾選Linker Listing前的復(fù)選框,如下圖所示。
4.4默認(rèn)情況下,棧被分配到RAM的哪個地方?
MDK中,我們只需要在配置文件中定義堆棧大小,編譯器會自動在RAM的空閑區(qū)域選擇一塊合適的地方來分配給我們定義的堆棧,這個地方位于RAM的那個地方呢?
通過查看MAP文件,原來MDK將堆棧放到程序使用到的RAM空間的后面,比如你的RAM空間從0x4000 0000開始,你的程序用掉了0x200字節(jié)RAM,那么堆??臻g就從0x4000 0200處開始。
使用了多少堆棧,是否溢出?
4.5 有多少RAM會被初始化?
在進入main()函數(shù)之前,MDK會把未初始化的RAM給清零的,我們的RAM可能很大,只使用了其中一小部分,MDK會不會把所有RAM都初始化呢?
答案是否定的,MDK只是把你的程序用到的RAM以及堆棧RAM給初始化,其它RAM的內(nèi)容是不管的。如果你要使用絕對地址訪問MDK未初始化的RAM,那就要小心翼翼的了,因為這些RAM上電時的內(nèi)容很可能是隨機的,每次上電都不同。
4.6 MDK編譯器如何設(shè)置非零初始化變量?
對于控制類產(chǎn)品,當(dāng)系統(tǒng)復(fù)位后(非上電復(fù)位),可能要求保持住復(fù)位前RAM中的數(shù)據(jù),用來快速恢復(fù)現(xiàn)場,或者不至于因瞬間復(fù)位而重啟現(xiàn)場設(shè)備。而keil mdk在默認(rèn)情況下,任何形式的復(fù)位都會將RAM區(qū)的非初始化變量數(shù)據(jù)清零。
MDK編譯程序生成的可執(zhí)行文件中,每個輸出段都最多有三個屬性:RO屬性、RW屬性和ZI屬性。對于一個全局變量或靜態(tài)變量,用const修飾符修飾的變量最可能放在RO屬性區(qū),初始化的變量會放在RW屬性區(qū),那么剩下的變量就要放到ZI屬性區(qū)了。
默認(rèn)情況下,ZI屬性區(qū)的數(shù)據(jù)在每次復(fù)位后,程序執(zhí)行main函數(shù)內(nèi)的代碼之前,由編譯器“自作主張”的初始化為零。所以我們要在C代碼中設(shè)置一些變量在復(fù)位后不被零初始化,那一定不能任由編譯器“胡作非為”,我們要用一些規(guī)則,約束一下編譯器。
分散加載文件對于連接器來說至關(guān)重要,在分散加載文件中,使用UNINIT來修飾一個執(zhí)行節(jié),可以避免編譯器對該區(qū)節(jié)的ZI數(shù)據(jù)進行零初始化。這是要解決非零初始化變量的關(guān)鍵。
因此我們可以定義一個UNINIT修飾的數(shù)據(jù)節(jié),然后將希望非零初始化的變量放入這個區(qū)域中。于是,就有了第一種方法:
修改分散加載文件,增加一個名為MYRAM的執(zhí)行節(jié),該執(zhí)行節(jié)起始地址為0x1000A000,長度為0x2000字節(jié)(8KB),由UNINIT修飾:
br變量屬性修飾符__attribute__((section(“name”),zero_init))用于將變量強制定義到name屬性數(shù)據(jù)節(jié)中,zero_init表示將未初始化的變量放到ZI數(shù)據(jù)節(jié)中。因為“NO_INIT”這顯性命名的自定義節(jié),具有UNINIT屬性。
將一個模塊內(nèi)的非初始化變量都非零初始化
假如該模塊名字為test.c,修改分散加載文件如下所示:
? ?
在該模塊定義時變量時使用如下方法:
這里,變量屬性修飾符__attribute__((zero_init))用于將未初始化的變量放到ZI數(shù)據(jù)節(jié)中變量,其實MDK默認(rèn)情況下,未初始化的變量就是放在ZI數(shù)據(jù)區(qū)的。
審核編輯:劉清
-
嵌入式
+關(guān)注
關(guān)注
5082文章
19104瀏覽量
304807 -
RAM
+關(guān)注
關(guān)注
8文章
1368瀏覽量
114641 -
C語言
+關(guān)注
關(guān)注
180文章
7604瀏覽量
136688 -
編譯器
+關(guān)注
關(guān)注
1文章
1623瀏覽量
49108 -
Flash模塊
+關(guān)注
關(guān)注
0文章
4瀏覽量
6086
原文標(biāo)題:嵌入式開發(fā)中的C語言2??——編譯器
文章出處:【微信號:單片機與嵌入式,微信公眾號:單片機與嵌入式】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論