[C 的那些眉角] switch 記得加 default — 防禦未來的自己

有一種 bug,不是今天的你造成的,
是三個月後的你造成的。

你現在寫了一個 switch
處理三種狀態,邏輯完全正確,
測試也都過了。

三個月後,需求改了,
新增了第四種狀態。
你在 enum 裡加了一個值,
但忘記去更新那個 switch

編譯過了,沒有警告,
程式跑起來,遇到第四種狀態的時候,
switch 什麼都不做,
靜靜地繼續往下執行。

症狀可能是某個功能沒有反應,
可能是某個變數沒有被更新,
可能要跑很久才會觸發那個狀態,
然後你花了半天才找到原因。

default 加一行,可以讓這種問題立刻現形。

閱讀全文

[C 的那些眉角]整數溢位比你想的更常見

有一個 bug,我在 code review 的時候無意間看到,
當下覺得「這不會有問題吧」,
但仔細算了一下,發現真的會出問題。

uint16_t a = 60000;
uint16_t b = 10000;
uint16_t sum = a + b; // 你覺得 sum 是多少?

答案不是 70000。

uint16_t 最大值是 65535,
70000 超過了,發生溢位,
sum 實際上是 70000 - 65536 = 4464

程式繼續跑,用著這個錯誤的值,
後續的計算全部都錯了,
但不會 crash,不會有任何警告。

這就是整數溢位的恐怖之處。

閱讀全文

[C 的那些眉角]陣列邊界檢查 — 緩衝區溢位的根源

緩衝區溢位(Buffer Overflow)是 C 語言最惡名昭彰的問題之一。

它不只是會讓程式 crash,
在某些情況下,它是駭客攻擊的入口。
歷史上很多嚴重的安全漏洞,
根源都是一個沒有做邊界檢查的陣列存取。

但在嵌入式開發,我更常遇到的不是安全問題,
而是「程式跑著跑著,某個全域變數的值莫名其妙被改掉」,
或是「UART 收到一個比預期長的封包,程式就 crash 了」。

追到最後,都是同一件事:
有人寫到了陣列邊界以外的地方。

閱讀全文

[C 的那些眉角]assert 是你的好朋友 — 但要用對地方

有一種 bug,你看了半天程式碼,
覺得「這裡不可能出問題」,
但它就是出問題了。

然後你加了一堆 printf
把每個變數的值都印出來,
才發現某個「不可能是 NULL」的指標,
在某個罕見的情況下真的是 NULL。

如果當初在那裡加了 assert
程式會在第一時間告訴你問題在哪,
而不是讓錯誤默默蔓延,
最後在完全不相關的地方 crash。

assert 就是做這件事的。

閱讀全文

[C 的那些眉角]Stack Overflow — 嵌入式的堆疊管理

一聽到 Stack Overflow 這個名字,
大部分工程師第一個想到的是那個問答網站。

但在嵌入式開發,Stack Overflow 是一個真實會發生的災難,
而且症狀往往讓你完全摸不著頭緒。

程式跑著跑著突然 reset,
某個全域變數的值莫名其妙被改掉,
函式回傳之後跳到奇怪的位址,
或是程式直接進入 HardFault Handler,
然後你盯著 register dump 發愁。

這些症狀背後,很多時候都是同一個原因:
Stack 被寫爆了。

閱讀全文

[C 的那些眉角]malloc 之後一定要檢查 — 記憶體配置的防禦性寫法

平常在嵌入式系統上用 malloc
寫完之後覺得很爽,動態配置記憶體,好像很厲害。

uint8_t *buf = malloc(1024);
memset(buf, 0, 1024);
// 開始用 buf...

有一次朋友看了一眼問我:「malloc 失敗怎麼辦?」

我說:「會失敗嗎?記憶體應該夠吧?」

他說:「嵌入式的 heap 就那麼大,你確定嗎?」

我 ........ 當然不是很確定。

閱讀全文

[C 的那些眉角]指標用完要歸零 — 懸空指標的恐怖故事

有一種 bug,我只要想到就頭皮發麻。

明明程式跑得好好的,突然在某個完全不相關的地方 crash,
或是資料莫名其妙被改掉,
或是在開發機上完全正常,到了產品上偶爾出問題。

很多時候,追到最後都是同一個兇手:

懸空指標(Dangling Pointer)。

閱讀全文

[C 的那些眉角]函式介面設計 — 呼叫時不易搞錯

話說有一種 bug 很特別。

不是邏輯寫錯,不是演算法有問題,
而是呼叫端用錯了你的函式

參數順序傳反了、單位搞錯了、忘記先初始化就呼叫、
buffer 大小傳錯了……

這種 bug 有時候很難發現,因為程式可能還是跑起來,
只是結果不對,或是偶爾 crash。

我以前遇到這種情況,第一反應是「呼叫端的問題,他用錯了」。

但後來慢慢體會到:如果很多人都用錯,問題通常在介面設計,不在使用者。

閱讀全文

[C 的那些眉角]參數傳遞的眉角 — `const` 用對了嗎?

const 這個關鍵字,我用了很久都只會這樣寫:

const int MAX_SIZE = 256;

然後在函式參數上偶爾加一下,
覺得「這樣比較專業」,但其實不太確定為什麼要加。

直到有一次,同事在 code review 留言:

「這個參數應該加 const,你這樣寫呼叫端不知道你會不會改它。」

我才開始認真研究 const 在參數傳遞上到底是什麼意思。

結果發現,const 跟指標放在一起,
光是位置不同,意思就完全不一樣。

閱讀全文

[C 的那些眉角]回傳值不要亂丟 — 錯誤處理的設計

剛開始寫 C 的時候,我的錯誤處理大概是這樣:

void init_device(void) {
    i2c_init();
    sensor_init();
    uart_init();
    // 完成,應該沒問題吧?
}

回傳 void,裡面每個函式的回傳值都不管。

反正在開發板上跑都正常,就這樣出貨了。

然後客戶回報說裝置偶爾會初始化失敗,
但 log 完全看不出來哪個步驟出問題,
因為根本沒有任何錯誤處理。

那次之後,我開始認真思考:錯誤處理不是可選的,是必須的。


最常見的壞習慣

1. 完全不處理回傳值

閱讀全文