volatile 是我學 C 語言以來,
花最久時間才真正搞懂的關鍵字。
不是因為語法複雜,
而是因為它的效果是「阻止編譯器做某些優化」,
在沒有優化的情況下,
加不加 volatile 看起來沒有差別,
讓人誤以為自己懂了。
直到有一天,開啟了 -O2 優化,
程式行為突然變了,
才發現原來自己一直用錯。
volatile 是我學 C 語言以來,
花最久時間才真正搞懂的關鍵字。
不是因為語法複雜,
而是因為它的效果是「阻止編譯器做某些優化」,
在沒有優化的情況下,
加不加 volatile 看起來沒有差別,
讓人誤以為自己懂了。
直到有一天,開啟了 -O2 優化,
程式行為突然變了,
才發現原來自己一直用錯。
有一種 bug,不是今天的你造成的,
是三個月後的你造成的。
你現在寫了一個 switch,
處理三種狀態,邏輯完全正確,
測試也都過了。
三個月後,需求改了,
新增了第四種狀態。
你在 enum 裡加了一個值,
但忘記去更新那個 switch。
編譯過了,沒有警告,
程式跑起來,遇到第四種狀態的時候,
switch 什麼都不做,
靜靜地繼續往下執行。
症狀可能是某個功能沒有反應,
可能是某個變數沒有被更新,
可能要跑很久才會觸發那個狀態,
然後你花了半天才找到原因。
default 加一行,可以讓這種問題立刻現形。
有一個 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,不會有任何警告。
這就是整數溢位的恐怖之處。
緩衝區溢位(Buffer Overflow)是 C 語言最惡名昭彰的問題之一。
它不只是會讓程式 crash,
在某些情況下,它是駭客攻擊的入口。
歷史上很多嚴重的安全漏洞,
根源都是一個沒有做邊界檢查的陣列存取。
但在嵌入式開發,我更常遇到的不是安全問題,
而是「程式跑著跑著,某個全域變數的值莫名其妙被改掉」,
或是「UART 收到一個比預期長的封包,程式就 crash 了」。
追到最後,都是同一件事:
有人寫到了陣列邊界以外的地方。
有一種 bug,你看了半天程式碼,
覺得「這裡不可能出問題」,
但它就是出問題了。
然後你加了一堆 printf,
把每個變數的值都印出來,
才發現某個「不可能是 NULL」的指標,
在某個罕見的情況下真的是 NULL。
如果當初在那裡加了 assert,
程式會在第一時間告訴你問題在哪,
而不是讓錯誤默默蔓延,
最後在完全不相關的地方 crash。
assert 就是做這件事的。
一聽到 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 跟指標放在一起,
光是位置不同,意思就完全不一樣。