一聽到 Stack Overflow 這個名字,
大部分工程師第一個想到的是那個問答網站。
但在嵌入式開發,Stack Overflow 是一個真實會發生的災難,
而且症狀往往讓你完全摸不著頭緒。
程式跑著跑著突然 reset,
某個全域變數的值莫名其妙被改掉,
函式回傳之後跳到奇怪的位址,
或是程式直接進入 HardFault Handler,
然後你盯著 register dump 發愁。
這些症狀背後,很多時候都是同一個原因:
Stack 被寫爆了。
韌體開發 x 踩坑心得 x 讓錢慢慢長大
一聽到 Stack Overflow 這個名字,
大部分工程師第一個想到的是那個問答網站。
但在嵌入式開發,Stack Overflow 是一個真實會發生的災難,
而且症狀往往讓你完全摸不著頭緒。
程式跑著跑著突然 reset,
某個全域變數的值莫名其妙被改掉,
函式回傳之後跳到奇怪的位址,
或是程式直接進入 HardFault Handler,
然後你盯著 register dump 發愁。
這些症狀背後,很多時候都是同一個原因:
Stack 被寫爆了。
平常在嵌入式系統上用 malloc,
寫完之後覺得很爽,動態配置記憶體,好像很厲害。
uint8_t *buf = malloc(1024);
memset(buf, 0, 1024);
// 開始用 buf...
有一次朋友看了一眼問我:「malloc 失敗怎麼辦?」
我說:「會失敗嗎?記憶體應該夠吧?」
他說:「嵌入式的 heap 就那麼大,你確定嗎?」
我 ........ 當然不是很確定。
有一種 bug,我只要想到就頭皮發麻。
明明程式跑得好好的,突然在某個完全不相關的地方 crash,
或是資料莫名其妙被改掉,
或是在開發機上完全正常,到了產品上偶爾出問題。
很多時候,追到最後都是同一個兇手:
懸空指標(Dangling Pointer)。
話說有一種 bug 很特別。
不是邏輯寫錯,不是演算法有問題,
而是呼叫端用錯了你的函式。
參數順序傳反了、單位搞錯了、忘記先初始化就呼叫、
buffer 大小傳錯了……
這種 bug 有時候很難發現,因為程式可能還是跑起來,
只是結果不對,或是偶爾 crash。
我以前遇到這種情況,第一反應是「呼叫端的問題,他用錯了」。
但後來慢慢體會到:如果很多人都用錯,問題通常在介面設計,不在使用者。
const 這個關鍵字,我用了很久都只會這樣寫:
const int MAX_SIZE = 256;
然後在函式參數上偶爾加一下,
覺得「這樣比較專業」,但其實不太確定為什麼要加。
直到有一次,同事在 code review 留言:
「這個參數應該加
const,你這樣寫呼叫端不知道你會不會改它。」
我才開始認真研究 const 在參數傳遞上到底是什麼意思。
結果發現,const 跟指標放在一起,
光是位置不同,意思就完全不一樣。
剛開始寫 C 的時候,我的錯誤處理大概是這樣:
void init_device(void) {
i2c_init();
sensor_init();
uart_init();
// 完成,應該沒問題吧?
}
回傳 void,裡面每個函式的回傳值都不管。
反正在開發板上跑都正常,就這樣出貨了。
然後客戶回報說裝置偶爾會初始化失敗,
但 log 完全看不出來哪個步驟出問題,
因為根本沒有任何錯誤處理。
那次之後,我開始認真思考:錯誤處理不是可選的,是必須的。
「一個函式只做一件事。」
這句話我很早就聽過,覺得自己懂了,然後繼續寫出這種東西:
int process_sensor_data(void) {
// 讀取感測器
uint8_t raw[8];
i2c_read(SENSOR_ADDR, raw, sizeof(raw));
// 解析資料
int16_t temp = (raw[0] << 8) | raw[1];
int16_t humidity = (raw[2] << 8) | raw[3];
// 換算單位
float temp_c = temp / 100.0f;
float humi_pct = humidity / 100.0f;
// 檢查是否超過閾值
if (temp_c > 85.0f || humi_pct > 95.0f) {
trigger_alarm();
}
// 存到全域變數
g_temperature = temp_c;
g_humidity = humi_pct;
// 發送到 server
mqtt_publish("sensor/data", temp_c, humi_pct);
return 0;
}
這個函式做了幾件事?
讀取、解析、換算、判斷、存值、發送。六件事。
當時覺得「這樣寫很方便,一個函式搞定所有事情」。
直到需要修改的時候才發現,牽一髮動全身,
改個閾值要找半天,加個錯誤處理不知道要加在哪裡,
單元測試根本不知道從哪裡下手。
剛學 C 的時候,老師說過:「變數要記得初始化。」
我點點頭,然後還是繼續寫:
int count;
char buf[64];
int *ptr;
反正當下程式跑起來好像也沒問題。
直到有一天,發現程式出現一個「偶發性」的奇怪行為。
有時候正常,有時候不正常,完全沒有規律。
#define 可以說是 C 語言裡最早學到的東西之一。
#define MAX_SIZE 256
#define PI 3.14159
看起來很無害,對吧?
但用久了才發現,#define 其實是個很容易出事的工具。
它不是變數、不是函式、不遵守 scope、不做型別檢查,
就是單純的文字替換。
而「單純的文字替換」,在某些情況下會產生你完全沒預期到的結果。
這篇記錄幾個我自己踩過、或在 code review 看過的坑。