最近我看到很多關於 Fil-C 的討論,它自稱是一種記憶體安全的 C/C++ 實作方式。你可以深入了解其實現細節,但對於第一次接觸的人,我認為展示一個簡化版本會更有價值,因為理解簡化版本後,再理解正式版本的心智負擔會小很多。

真正的 Fil-C 是透過編譯器階段重寫 LLVM IR,而簡化模型則是自動重寫 C/C++ 原始碼:將不安全的程式碼轉成安全程式碼。第一個重寫是,在每個函式中,每個指標型態的區域變數都會多出一個對應的 AllocationRecord* 型態區域變數,例如:

AllocationRecord 大致上是這樣的結構:

對指標型態的區域變數進行簡單操作時,也會同時移動對應的 AllocationRecord* :

當指標被傳入或傳出函式時,程式碼會被重寫,除了原本的指標外,也會包含 AllocationRecord*。對特定標準函式庫函式的呼叫也會被改寫成呼叫 Fil-C 版本。綜合起來,我們得到:

filc_malloc 的(簡化)實作其實會執行三次不同的配置,而非只配置請求的大小:

當指標變數被解參考時,會使用對應的 AllocationRecord* 來執行邊界檢查:

當被存取或載入的值本身也是指標時,情況會更複雜。如前所述,指標型態的區域變數會由編譯器插入對應的 AllocationRecord* 變數,因為編譯器對所有區域變數擁有完全控制權與可見性。一旦指標存在於堆積區而非僅在區域變數中,情況就變得困難,這時 invisible_bytes 就派上用場:如果在 visible_bytes + i 有一個指標,那它的 AllocationRecord* 就在 invisible_bytes + i。換句話說,invisible_bytes 是一個元素型態為 AllocationRecord* 的陣列。為了確保對此陣列的合理存取,i 必須是 sizeof(AllocationRecord*) 的倍數。這部分的額外邏輯以綠色標示:

我們還沒看到的是 filc_free,其大致執行如下:

細心的讀者會注意到 filc_malloc 做了三次配置,但 filc_free 只釋放了其中兩次:AllocationRecord 物件並未被 filc_free 釋放。這個缺口由垃圾回收器(GC)補足。沒錯,這是帶有 GC 的 C/C++。正式版的 Fil-C 採用平行並行增量式回收器,但簡單模型用停頓世界的回收器就足夠。回收器會追蹤 AllocationRecord 物件,釋放所有不可達的物件。它還做兩件事:

第一點表示使用 Fil-C 時,忘記呼叫 free 不再會造成記憶體洩漏:記憶體會被 GC 自動釋放。但這不代表呼叫 free 沒用,因為它能讓記憶體比 GC 預期的時間更早被釋放。第二點表示呼叫 free 後,對應的 AllocationRecord 最終會變成不可達,進而被釋放。

有了 GC 後,使用它的誘因增加。其中一個用途是讓取得區域變數地址變得安全,即使該指標在區域變數生命週期結束後仍被使用。如果編譯器發現區域變數的地址被取得,且無法證明該地址不會逃逸區域變數生命週期,Fil-C 轉換會將該區域變數改為透過 malloc 在堆積區配置,而非在堆疊上配置。這種情況下不需要插入對應的 free,因為 GC 會處理。

最後我要強調的是 Fil-C 版本的 memmove。這個 C 標準函式庫函式會操作任意記憶體,編譯器無法知道該記憶體中可能存在的指標。為解決此問題,採用一個合理的啟發式方法:任意記憶體中的指標必須完全位於該任意記憶體範圍內,且必須正確對齊。這導致一個有趣的結果:memmove 八個對齊的位元組與分別 memmove 八個 1 位元組的行為不同,前者會同時 memmove 對應範圍的 invisible_bytes,後者則不會。

以上就是簡化模型的全部內容。正式版本還包含一些額外的複雜性,例如:

有了基礎理解後,我想以一個問題作結:你什麼時候會想使用 Fil-C?就我個人而言,我的答案是: