在嵌入式軟件中發(fā)現(xiàn)并消除潛在的bug是一件困難的事情。要從觀察到的崩潰、掛起或其他計(jì)劃外運(yùn)行時(shí)行為追溯到根本原因,通常需要付出巨大的努力和昂貴的工具。嵌入式開發(fā)工程師們常常放棄尋找罕見異常的原因——因?yàn)檫@些異常無法在實(shí)驗(yàn)室中輕易重現(xiàn)——而將其視為“用戶錯(cuò)誤”或“小故障”,然而,機(jī)器中的這些潛在危機(jī)仍然一直存在。
因此,這里有一個(gè)關(guān)于難以重現(xiàn)的固件錯(cuò)誤最常見的根本原因的指南。
1. 堆碎片
嵌入式軟件開發(fā)人員并未廣泛使用動(dòng)態(tài)內(nèi)存分配——這是有充分理由的,其中之一是堆碎片的問題。
通過 C 的 malloc() 標(biāo)準(zhǔn)庫(kù)例程或 C++ 的 new 關(guān)鍵字創(chuàng)建的所有數(shù)據(jù)結(jié)構(gòu)都存在于堆中。堆是 RAM
中預(yù)先確定的最大大小的特定區(qū)域。最初,堆中的每個(gè)分配都會(huì)將剩余的“可用”空間減少相同的字節(jié)數(shù)。
不再需要的數(shù)據(jù)結(jié)構(gòu)的存儲(chǔ)可以通過調(diào)用 free() 或使用 delete
關(guān)鍵字返回到堆中。從理論上講,這使得該存儲(chǔ)空間可在后續(xù)分配期間重復(fù)使用。但是分配和刪除的順序通常至少是偽隨機(jī)的——導(dǎo)致堆變成一堆更小的碎片。
2. 堆棧溢出
每個(gè)程序員都知道堆棧溢出是一件非常糟糕的事情?。 但是,每個(gè)堆棧溢出的影響各不相同。
損害的性質(zhì)和不當(dāng)行為的時(shí)間完全取決于破壞了哪些數(shù)據(jù)或指令以及如何使用它們。重要的是,堆棧溢出與其對(duì)系統(tǒng)的負(fù)面影響之間的時(shí)間長(zhǎng)度取決于使用破壞位之前的時(shí)間長(zhǎng)度。
不幸的是,在嵌入式開發(fā)中,堆棧溢出對(duì)嵌入式系統(tǒng)的影響遠(yuǎn)遠(yuǎn)超過對(duì)臺(tái)式計(jì)算機(jī)的影響。這有幾個(gè)原因,包括:
嵌入式系統(tǒng)通常只能依靠少量的 RAM;
通常沒有可依賴的虛擬內(nèi)存(因?yàn)闆]有磁盤);
基于 RTOS
任務(wù)的固件設(shè)計(jì)利用多個(gè)堆棧(每個(gè)任務(wù)一個(gè)),每個(gè)堆棧的大小都必須足夠大,以確保不會(huì)出現(xiàn)唯一的最壞情況堆棧深度;
中斷處理程序可能會(huì)嘗試使用這些相同的堆棧。
3. 缺少“volatile”關(guān)鍵字
未能使用 C
的“volatile”關(guān)鍵字標(biāo)記某些類型的變量,可能會(huì)導(dǎo)致系統(tǒng)出現(xiàn)許多癥狀,這些癥狀只有在編譯器的優(yōu)化器設(shè)置為低級(jí)別或禁用時(shí)才能正常工作。 volatile
限定符在變量聲明期間使用,其目的是防止優(yōu)化該變量的讀取和寫入。
請(qǐng)注意,除了確保對(duì)給定變量進(jìn)行所有讀取和寫入之外,使用 volatile 還會(huì)通過添加額外的“序列點(diǎn)”來限制編譯器。對(duì)多個(gè)
volatile 的訪問必須按照它們?cè)诖a中的寫入順序執(zhí)行。
4. 比賽條件
競(jìng)爭(zhēng)條件是指兩個(gè)或多個(gè)執(zhí)行線程(可以是 RTOS 任務(wù)或 main() 加
ISR)的組合結(jié)果根據(jù)每個(gè)指令交錯(cuò)的精確順序而變化的任何情況。
例如,假設(shè)嵌入式開發(fā)人員有兩個(gè)執(zhí)行線程,其中一個(gè)定期遞增全局變量 (g_counter += 1;),另一個(gè)偶爾重置它
(g_counter =
0;)。如果增量不能始終以原子方式執(zhí)行(即,在單個(gè)指令周期中),則此處存在競(jìng)爭(zhēng)條件。計(jì)數(shù)器變量的兩次更新之間的沖突可能永遠(yuǎn)不會(huì)或很少發(fā)生。但是當(dāng)它這樣做時(shí),計(jì)數(shù)器實(shí)際上不會(huì)在內(nèi)存中重置。這種影響可能會(huì)對(duì)系統(tǒng)產(chǎn)生嚴(yán)重后果,盡管可能要等到實(shí)際碰撞后很長(zhǎng)時(shí)間才會(huì)發(fā)生。
最佳實(shí)踐:可以通過圍繞必須以適當(dāng)?shù)膿屨枷拗菩袨閷?duì)原子執(zhí)行的代碼的“關(guān)鍵部分”來防止競(jìng)爭(zhēng)條件。為了防止涉及 ISR
的競(jìng)爭(zhēng)條件,必須在其他代碼的關(guān)鍵部分期間至少禁用一個(gè)中斷信號(hào)。在 RTOS
任務(wù)之間競(jìng)爭(zhēng)的情況下,最佳實(shí)踐是創(chuàng)建特定于該共享對(duì)象的互斥鎖,每個(gè)任務(wù)必須在進(jìn)入臨界區(qū)之前獲取該互斥鎖。請(qǐng)注意,依靠特定 CPU
的功能來確保原子性并不是一個(gè)好主意,因?yàn)檫@只會(huì)防止競(jìng)爭(zhēng)條件,直到更改編譯器或 CPU。
5. 不可重入函數(shù)
從技術(shù)上講,不可重入函數(shù)的問題是競(jìng)爭(zhēng)條件問題的一個(gè)特例。
出于這個(gè)原因,由不可重入函數(shù)引起的運(yùn)行時(shí)錯(cuò)誤是相似的,也不會(huì)以可重現(xiàn)的方式發(fā)生——這使得它們同樣難以調(diào)試。
不幸的是,與其他類型的競(jìng)爭(zhēng)條件相比,不可重入函數(shù)在代碼審查中也更難發(fā)現(xiàn)。
使函數(shù)可重入的關(guān)鍵是暫停對(duì)外圍寄存器、全局變量(包括靜態(tài)局部變量)、持久堆對(duì)象和共享內(nèi)存區(qū)域的所有訪問的搶占。嵌入式開發(fā)人員可以通過禁用一個(gè)或多個(gè)中斷或通過獲取和釋放互斥鎖來完成,共享數(shù)據(jù)類型的細(xì)節(jié)通常決定了最佳解決方案。