0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

嵌入式開發(fā)中的C語言—編譯器介紹

單片機與嵌入式 ? 來源:STM32嵌入式開發(fā) ? 2023-02-25 16:48 ? 次閱讀

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(jìn)行任何運行時檢查,比如數(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)類似代碼,將會造成看似莫名其妙的死機或者重啟。

42a57f84-b06a-11ed-bfe3-dac502259ad0.png ? ?

對于無符號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語句后誤加了一個分號,可能會完全改變了程序邏輯。編譯器也會很配合的幫忙掩蓋,甚至連警告都不提示。代碼如下:

42bd14f0-b06a-11ed-bfe3-dac502259ad0.png ????

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

42d24afa-b06a-11ed-bfe3-dac502259ad0.png ????

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

2.3 難查的數(shù)組越界

上文曾提到數(shù)組常常是引起程序不穩(wěn)定的重要因素,程序員往往不經(jīng)意間就會寫數(shù)組越界。

一位同事的代碼在硬件上運行,一段時間后就會發(fā)現(xiàn)LCD顯示屏上的一個數(shù)字不正常的被改變。經(jīng)過一段時間的調(diào)試,問題被定位到下面的一段代碼中:

42e769f8-b06a-11ed-bfe3-dac502259ad0.png ????

這里聲明了擁有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ù)組:

42f74f76-b06a-11ed-bfe3-dac502259ad0.png ????

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

430c2f2c-b06a-11ed-bfe3-dac502259ad0.png ? ?

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

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

431f5b2e-b06a-11ed-bfe3-dac502259ad0.png ????

這個給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ù)完全接收后再進(jìn)行處理。即使定義的數(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ù)程序精簡后如下所示:

43395f24-b06a-11ed-bfe3-dac502259ad0.png ????

由于存在多個無線傳感器近乎同時發(fā)送數(shù)據(jù)的可能加之GetData()函數(shù)保護(hù)力度不夠,數(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限定符用來告訴編譯器,該對象的值無任何持久性,不要對它進(jìn)行任何優(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口都是使用寄存器中的值來進(jìn)行處理。因為讀寫寄存器是最快的,這樣可以優(yōu)化程序效率。與之類似的,中斷里的變量、多線程中的共享變量等都存在這樣的問題。

不使用volatile,可能造成運行邏輯錯誤,但是不必要的使用volatile會造成代碼效率低下(編譯器不優(yōu)化volatile限定的變量),因此清楚的知道何處該使用volatile限定符,是一個嵌入式程序員的必修內(nèi)容。

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

434be6e4-b06a-11ed-bfe3-dac502259ad0.png ????

并在頭文件中聲明該變量:

435d93bc-b06a-11ed-bfe3-dac502259ad0.png ????

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

436def1e-b06a-11ed-bfe3-dac502259ad0.png ?????

在頭文件中這樣聲明變量:

437e409e-b06a-11ed-bfe3-dac502259ad0.png ? ?

編譯器卻不會給出錯誤信息(有些編譯器僅給出一條警告)。當(dāng)你在另外一個模塊(該模塊包含聲明變量test的頭文件)使用變量test時,它已經(jīng)不再具有volatile限定,這樣很可能造成一些重大錯誤。比如下面的例子,注意該例子是為了說明volatile限定符而專門構(gòu)造出的,因為現(xiàn)實中的volatile使用Bug大都隱含,并且難以理解。

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

43912484-b06a-11ed-bfe3-dac502259ad0.png ? ?

實際上,這是一個死循環(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í)行過程。

43adc4fe-b06a-11ed-bfe3-dac502259ad0.png ????

為了更容易的理解編譯器如何處理volatile限定符,這里給出未使用volatile限定符和使用volatile限定符程序的反匯編代碼:

沒有使用關(guān)鍵字volatile,在keil MDK V4.54下編譯,默認(rèn)優(yōu)化級別,如下所示(注意最后兩行):

43e5fb8a-b06a-11ed-bfe3-dac502259ad0.png

使用關(guān)鍵字volatile,在keil MDK V4.54下編譯,默認(rèn)優(yōu)化級別,如下所示(注意最后三行):

43f6bc7c-b06a-11ed-bfe3-dac502259ad0.png ? ?

可以看到,如果沒有使用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ī)定的必須保護(hù)的寄存器以及局部變量,包括局部數(shù)組、結(jié)構(gòu)體、聯(lián)合體和C++的類。默認(rèn)情況下,堆棧的位置、初始值都是由編譯器設(shè)置,因此需要對編譯器的堆棧有一定了解。

從堆棧中分配的局部變量的初值是不確定的,因此需要運行時顯式初始化該變量。一旦離開局部變量的作用域,這個變量立即被釋放,其它代碼也就可以使用它,因此堆棧中的一個內(nèi)存位置可能對應(yīng)整個程序的多個變量。

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

441b87fa-b06a-11ed-bfe3-dac502259ad0.png ????

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

4441dfcc-b06a-11ed-bfe3-dac502259ad0.png

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…

4454feb8-b06a-11ed-bfe3-dac502259ad0.png ???

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)到源文件所在位置。

編譯器語義檢查的弱小在很大程度上助長了不可靠代碼的廣泛存在。隨著時代的進(jì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ā)商可以通過未定義行為對語言進(jìn)行擴展 C語言的未定義行為,使得C極度高效靈活并且給編譯器實現(xiàn)帶來了方便,但這并不利于優(yōu)質(zhì)嵌入式C程序的編寫。因為許多 C 語言中看起來有意義的東西都是未定義的,并且這也容易使你的代碼埋下隱患,并且不利于跨編譯器移植。Java程序會極力避免未定義行為,并用一系列手段進(jìn)行運行時檢查,使用Java可以相對容易的寫出安全代碼,但體積龐大效率低下。作為嵌入式程序員,我們需要了解這些未定義行為,利用C語言的靈活性,寫出比Java更安全、效率更高的代碼來。

3.1常見的未定義行為

自增自減在表達(dá)式中連續(xù)出現(xiàn)并作用于同一變量或者自增自減在表達(dá)式中出現(xiàn)一次,但作用的變量多次出現(xiàn)

自增(++)和自減(--)這一動作發(fā)生在表達(dá)式的哪個時刻是由編譯器決定的,比如:

446ca770-b06a-11ed-bfe3-dac502259ad0.png

有符號數(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ù)雜的自增或者自減表達(dá)式,實際上,應(yīng)該避免構(gòu)造所有復(fù)雜表達(dá)式; 比如a[i] = i++;語句可以改為a[i] = i; i++;這兩句代碼。 2)只對無符號操作數(shù)使用位操作;

必要的運行時檢查

檢查是否溢出、除數(shù)是否為零,申請的內(nèi)存數(shù)量是否為零等等,比如上面的有符號整數(shù)溢出例子,可以按照如下方式編寫,以消除未定義特性:

448515e4-b06a-11ed-bfe3-dac502259ad0.png ? ?

使用的原理解釋一下,因為在加法運算中,操作數(shù)value1和value2只有符號相同時,才可能發(fā)生溢出,所以我們先將這兩個數(shù)轉(zhuǎn)換為無符號類型,兩個數(shù)的和保存在變量usum中。如果發(fā)生溢出,則value1、value2和usum的最高位(符號位)一定不同,表達(dá)式(usum ^ value1) & (usum ^ value2) 的最高位一定為1,這個表達(dá)式位與(&)上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ù)以二進(jìn)制補碼形式表示;浮點量按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)容進(jìn)行填充。不能使用memcmp()來比較以這種方式定義的填充結(jié)構(gòu)!

編譯器不對聲明為volatile類型的數(shù)據(jù)進(jìn)行優(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ìn)行了初始化,經(jīng)過編譯器編譯后,這些初始值被存放在了代碼的哪里?我們舉個例子說明:

449e2a2a-b06a-11ed-bfe3-dac502259ad0.png ? ?

我曾做過一個項目,項目中的一個設(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)試表明,進(jìn)入main函數(shù)的時候,該變量的初值已經(jīng)被改為一個恒定值。

要想知道為什么全局變量的初值被改變,就要了解這些初值編譯后被放到了二進(jìn)制文件的哪里。在此之前,需要先了解一點鏈接原理。

ARM映象文件各組成部分在存儲系統(tǒng)中的地址有兩種:一種是映象文件位于存儲器時(通俗的說就是存儲在Flash中的二進(jìn)制代碼)的地址,稱為加載地址;一種是映象文件運行時(通俗的說就是給板子上電,開始運行Flash中的程序了)的地址,稱為運行時地址。

賦初值的全局變量和靜態(tài)變量在程序還沒運行的時候,初值是被放在Flash中的,這個時候他們的地址稱為加載地址,當(dāng)程序運行后,這些初值會從Flash中拷貝到RAM中,這時候就是運行時地址了。

原來,對于在程序中賦初值的全局變量和靜態(tài)變量,程序編譯后,MDK將這些初值放到Flash中,位于緊靠在可執(zhí)行代碼的后面。在程序進(jìn)入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ù)選框,如下圖所示。

44b5cbf8-b06a-11ed-bfe3-dac502259ad0.png

4.4默認(rèn)情況下,棧被分配到RAM的哪個地方?

MDK中,我們只需要在配置文件中定義堆棧大小,編譯器會自動在RAM的空閑區(qū)域選擇一塊合適的地方來分配給我們定義的堆棧,這個地方位于RAM的那個地方呢?

通過查看MAP文件,原來MDK將堆棧放到程序使用到的RAM空間的后面,比如你的RAM空間從0x4000 0000開始,你的程序用掉了0x200字節(jié)RAM,那么堆??臻g就從0x4000 0200處開始。

使用了多少堆棧,是否溢出?

4.5 有多少RAM會被初始化?

在進(jìn)入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ù)進(jìn)行零初始化。這是要解決非零初始化變量的關(guān)鍵。

因此我們可以定義一個UNINIT修飾的數(shù)據(jù)節(jié),然后將希望非零初始化的變量放入這個區(qū)域中。于是,就有了第一種方法:

修改分散加載文件,增加一個名為MYRAM的執(zhí)行節(jié),該執(zhí)行節(jié)起始地址為0x1000A000,長度為0x2000字節(jié)(8KB),由UNINIT修飾:

44d40e7e-b06a-11ed-bfe3-dac502259ad0.png

br
變量屬性修飾符__attribute__((section(“name”),zero_init))用于將變量強制定義到name屬性數(shù)據(jù)節(jié)中,zero_init表示將未初始化的變量放到ZI數(shù)據(jù)節(jié)中。因為“NO_INIT”這顯性命名的自定義節(jié),具有UNINIT屬性。

將一個模塊內(nèi)的非初始化變量都非零初始化

假如該模塊名字為test.c,修改分散加載文件如下所示:

450893d8-b06a-11ed-bfe3-dac502259ad0.png ? ?

在該模塊定義時變量時使用如下方法:

這里,變量屬性修飾符__attribute__((zero_init))用于將未初始化的變量放到ZI數(shù)據(jù)節(jié)中變量,其實MDK默認(rèn)情況下,未初始化的變量就是放在ZI數(shù)據(jù)區(qū)的。





審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 嵌入式
    +關(guān)注

    關(guān)注

    5045

    文章

    18816

    瀏覽量

    298475
  • RAM
    RAM
    +關(guān)注

    關(guān)注

    8

    文章

    1344

    瀏覽量

    114214
  • C語言
    +關(guān)注

    關(guān)注

    180

    文章

    7575

    瀏覽量

    134034
  • 編譯器
    +關(guān)注

    關(guān)注

    1

    文章

    1602

    瀏覽量

    48895
  • Flash模塊
    +關(guān)注

    關(guān)注

    0

    文章

    4

    瀏覽量

    6076

原文標(biāo)題:嵌入式開發(fā)中的C語言2??——編譯器

文章出處:【微信號:單片機與嵌入式,微信公眾號:單片機與嵌入式】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    嵌入式開發(fā)的交叉編譯詳解

    嵌入式開發(fā),經(jīng)常會遇到目標(biāo)平臺資源貧乏,無法運行需要的編譯器。亦或是目標(biāo)平臺上不允許或不能夠安裝需要的編譯器。這時候就需要使用交叉編譯
    的頭像 發(fā)表于 12-01 13:24 ?945次閱讀
    <b class='flag-5'>嵌入式開發(fā)</b><b class='flag-5'>中</b>的交叉<b class='flag-5'>編譯</b>詳解

    嵌入式開發(fā)要學(xué)什么?

    你受益匪淺?! ?.安裝一個Linux的發(fā)行版本,能夠熟悉使用Linux,掌握Linux下的目錄結(jié)構(gòu)、基本命令、編輯VI、編譯器GCC、調(diào)試GDB和Make項目管理工具以及嵌入式開發(fā)
    發(fā)表于 09-06 16:21

    嵌入式開發(fā)為什么選擇C語言?

    1、嵌入式開發(fā)為什么選擇C語言?(面試題?。。。?b class='flag-5'>嵌入式開發(fā)操作系統(tǒng)是核心,需要移植,并在上層和底層做
    發(fā)表于 12-15 07:45

    嵌入式C_C++語言精華

    介紹了在嵌入式開發(fā)的過程,c語言C++語言的施用
    發(fā)表于 03-17 09:54 ?2次下載

    嵌入式軟件開發(fā)語言 嵌入式C編程

    在我們初學(xué)嵌入式開發(fā)的時候,總會出現(xiàn)一個問題。那就是C語言嵌入式C編程有什么區(qū)別?而嵌入式工程
    發(fā)表于 12-28 16:52 ?1647次閱讀

    嵌入式開發(fā)語言有哪些_最全面嵌入式開發(fā)語言概述

    嵌入式開發(fā)語言有哪些?嵌入式開發(fā)的入門門檻還是比較高的,不僅要懂較底層軟件,對軟件專業(yè)水平要求較高,而且必須懂得硬件的工作原理,嵌入式系統(tǒng)應(yīng)用越來越廣泛,目前,在
    發(fā)表于 01-29 14:47 ?9674次閱讀
    <b class='flag-5'>嵌入式開發(fā)</b><b class='flag-5'>語言</b>有哪些_最全面<b class='flag-5'>嵌入式開發(fā)</b><b class='flag-5'>語言</b>概述

    嵌入式C實現(xiàn)延時程序的不同變量的區(qū)別 幾種Linux嵌入式開發(fā)環(huán)境的簡單介紹

    嵌入式C實現(xiàn)延時程序的不同變量的區(qū)別 幾種Linux嵌入式開發(fā)環(huán)境的簡單介紹 ARM嵌入式開發(fā)基礎(chǔ) 對話微軟MVP:走進(jìn)
    發(fā)表于 04-14 07:24 ?1568次閱讀
    <b class='flag-5'>嵌入式</b><b class='flag-5'>C</b>實現(xiàn)延時程序的不同變量的區(qū)別 幾種Linux<b class='flag-5'>嵌入式開發(fā)</b>環(huán)境的簡單<b class='flag-5'>介紹</b>

    嵌入式開發(fā)通常采用哪種編程語言

    目前在嵌入式開發(fā)領(lǐng)域比較常見的編程語言C,另外C++、Python、JavaScript等語言也可以進(jìn)行
    發(fā)表于 06-18 16:59 ?1.6w次閱讀

    嵌入式linux c語言,嵌入式LinuxC語言開發(fā)工具.pdf

    2 章 嵌入式Linux C 語言開發(fā)工具本章目標(biāo)任何應(yīng)用程序的開發(fā)都離不開編輯
    發(fā)表于 11-01 17:38 ?12次下載
    <b class='flag-5'>嵌入式</b>linux <b class='flag-5'>c</b><b class='flag-5'>語言</b>,<b class='flag-5'>嵌入式</b>LinuxC<b class='flag-5'>語言</b><b class='flag-5'>開發(fā)</b>工具.pdf

    什么是嵌入式開發(fā)?為什么用C語言作為開發(fā)語言?

    內(nèi)部做開發(fā)的,而操作系統(tǒng)所有的內(nèi)核都是C語言所編寫的,所以說在嵌入式開發(fā)的過程也選擇C
    發(fā)表于 11-02 18:50 ?12次下載
    什么是<b class='flag-5'>嵌入式開發(fā)</b>?為什么用<b class='flag-5'>C</b><b class='flag-5'>語言</b>作為<b class='flag-5'>開發(fā)</b><b class='flag-5'>語言</b>?

    嵌入式開發(fā)為什么選擇C語言作為開發(fā)語言

    了解嵌入式開發(fā)的朋友們都非常的清楚其核心的開發(fā)語言C語言,C
    發(fā)表于 11-03 09:21 ?17次下載
    <b class='flag-5'>嵌入式開發(fā)</b>為什么選擇<b class='flag-5'>C</b><b class='flag-5'>語言</b>作為<b class='flag-5'>開發(fā)</b><b class='flag-5'>語言</b>?

    嵌入式開發(fā)為什么選擇C語言?它有哪些特點?

    眾所周知,C語言嵌入式開發(fā)占據(jù)著十分重要的地位,為什么嵌入式開發(fā)要選擇C
    的頭像 發(fā)表于 01-04 09:56 ?1099次閱讀
    <b class='flag-5'>嵌入式開發(fā)</b><b class='flag-5'>中</b>為什么選擇<b class='flag-5'>C</b><b class='flag-5'>語言</b>?它有哪些特點?

    嵌入式開發(fā)C語言編譯器設(shè)置

    編譯器的語義檢查很弱小,甚至還會“掩蓋”錯誤?,F(xiàn)代的編譯器設(shè)計是件浩瀚的工程,為了讓編譯器設(shè)計簡單一些,目前幾乎所有編譯器的語義檢查都比較弱小。為了獲得更快的執(zhí)行效率,
    發(fā)表于 10-11 12:43 ?571次閱讀

    c語言嵌入式開發(fā)

    電子發(fā)燒友網(wǎng)站提供《c語言嵌入式開發(fā).zip》資料免費下載
    發(fā)表于 11-17 14:11 ?2次下載
    <b class='flag-5'>c</b><b class='flag-5'>語言</b><b class='flag-5'>嵌入式開發(fā)</b>

    C語言嵌入式開發(fā)的關(guān)鍵編譯器角色

    嵌入式程序開發(fā)跟硬件密切相關(guān),需要使用C語言來讀寫底層寄存、存取數(shù)據(jù)、控制硬件等,C
    發(fā)表于 04-26 14:53 ?407次閱讀
    <b class='flag-5'>C</b><b class='flag-5'>語言</b>:<b class='flag-5'>嵌入式開發(fā)</b><b class='flag-5'>中</b>的關(guān)鍵<b class='flag-5'>編譯器</b>角色