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