記得剛出社會沒多久,有一件事讓我覺得很奇怪。
公司裡的工程師們,不管資歷深淺,午休時間幾乎人手一個看盤 App,偶爾三三兩兩討論一下某支股票。我那時候心想,大家不是來寫程式的嗎?幹嘛盯著那個。
後來有個前輩點醒了我:「你以後會需要懂一點股票,不是要靠它發財,是為了讓自己在職涯裡做出更好的決定。」
當時聽完覺得有點玄,現在回頭看,還真的是這樣。
看懂公司,才知道你在哪條船上
科技業的工作環境有個特性:公司的命運跟你的命運是綁在一起的。
記得剛出社會沒多久,有一件事讓我覺得很奇怪。
公司裡的工程師們,不管資歷深淺,午休時間幾乎人手一個看盤 App,偶爾三三兩兩討論一下某支股票。我那時候心想,大家不是來寫程式的嗎?幹嘛盯著那個。
後來有個前輩點醒了我:「你以後會需要懂一點股票,不是要靠它發財,是為了讓自己在職涯裡做出更好的決定。」
當時聽完覺得有點玄,現在回頭看,還真的是這樣。
科技業的工作環境有個特性:公司的命運跟你的命運是綁在一起的。
嵌入式開發幾乎離不開 bit 操作。
設定硬體暫存器、解析通訊協定封包、
控制 GPIO、讀取狀態旗標,
到處都是對特定 bit 的讀寫。
我剛開始寫嵌入式的時候,
程式碼長這樣:
// 設定 UART 控制暫存器
UART_CTRL |= 0x01; // 啟用 TX
UART_CTRL |= 0x02; // 啟用 RX
UART_CTRL &= ~0x04; // 關閉 loopback
UART_CTRL |= (0x03 << 4); // 設定 baud rate 為 115200
當下寫的時候覺得沒問題,
因為我知道每個數字代表什麼。
三個月後回來看,
完全不知道 0x01、0x02、0x04 是什麼意思,
要翻 datasheet 才能看懂。
這就是 magic number 的問題。
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)。