記憶體消毒器(MemorySanitizer, MSan)
/ MSan -> EM-san /
在 C 裡,你從 malloc() 拿到的記憶體、或一個未初始化的區域變數,裝著之前留在那裡的任何垃圾——它不會被歸零。在你寫進任何有意義的東西之前就讀這樣的值是一個臭蟲:你的程式對著殘留的垃圾動作,更糟的是,那垃圾可能在某次執行碰巧看起來合理、下一次卻是災難。記憶體消毒器是一個專注於一件事的工具:逮到對未初始化記憶體的使用——程式行為開始取決於一個從未被設定過的值的那一刻。
具體來說,MSan 是 clang 的一種消毒器,用 -fsanitize=memory(加 -g)啟用。編譯器以一些位元為每個值做影子、追蹤它是否已初始化,並把那個「被汙染」的狀態透過複製與運算傳播下去。關鍵在於,MSan 不會在你一讀未初始化記憶體的瞬間就抱怨——它是在一個未初始化的值真正影響到可觀察的行為時才抱怨,例如用它做分支判斷、把它當陣列索引、或把它傳給一個系統呼叫。在那一刻它印出帶著那次使用的堆疊追蹤的報告,並很有用地指出那塊未初始化記憶體源自哪裡。這正是 memcheck 與 ASan 留下的空缺:ASan 預設不標出未初始化的讀取,所以 MSan 補上它。
為何重要:未初始化讀取的臭蟲之所以陰險,正因為它們有時候「能動」——殘留的位元組碰巧是零、或碰巧在範圍內——所以程式通過測試、卻在實地失敗。MSan 把那場樂透變成一個確定、有位置的錯誤。誠實的提醒與一個真實的實務代價:MSan 只有 clang 有,而且它要求所有程式碼都被插樁,包括 C 函式庫與每一個相依套件——任何未插樁、卻會寫進你記憶體的函式庫都會產生假陽性,所以實務上你需要一個被插樁過的 libc++/標準函式庫。這使得 MSan 比 ASan 或 UBSan 更難建置,這也是為什麼團隊往往是在追一個未初始化資料的臭蟲時才特意動用它,而非總是開著。
$ clang -g -fsanitize=memory uninit.c -o a 對一段做 int x; if (x > 0) ... (在指派之前就讀 x)的程式碼,會印出「use-of-uninitialized-value」連同那個分支的行號與 x 宣告的地方——而一個普通的建構可能每次依堆疊裡的垃圾不同而跑出不同結果。
MSan 標出一個取決於未初始化變數的分支,並附上它的源頭。
MSan 回報的是對一個未初始化值的使用、而非它單純的存在,而且它要求所有程式碼(包括 libc)都被插樁、否則會冒出假陽性——所以它只有 clang 有、也比 ASan/UBSan 更難部署。記得 malloc() 不會把記憶體歸零(calloc() 會),這正是這些臭蟲存在的原因。