抽象詮釋是那種會讓工程師關掉分頁的術語。它聽起來像是需要花一學期格論才能理解的東西。大多數開發者都以為它活在研究論文裡,而不是在提取要求中。

這個假設很昂貴。抽象詮釋只是一種在不執行程式碼的情況下證明程式碼性質的方法。建立在它之上的工具可以捕獲類型檢查器和程式碼檢查工具錯過的空指標解引用、記憶體洩漏和競態條件。好消息是:你不需要理解伽羅瓦連接就能使用它。你需要一個能運作的CI組態設定和大約二十分鐘。

抽象詮釋實際做什麼

其核心,抽象詮釋是一種自動化的證明技術。它執行你的程式,但使用的是近似值而不是真實值。

考慮一個變數x。在正常執行中,x可能儲存42。在抽象詮釋中,x可能儲存「正整數」。分析追蹤這些抽象值通過每一個可能的程式碼路徑。如果它能證明沒有任何路徑會導致空指標解引用,你就是安全的。如果它發現某個路徑上x在解引用位置可能為空,它就會報告一個潛在缺陷。

神奇之處在於這對迴圈和條件陳述式也有效。分析器在抽象狀態上計算不動點,從而可以對無界迭代進行推理,而無需實際永遠迭代下去。這就是抽象詮釋與在迴圈上掙扎的更簡單符號執行工具的區別。

Facebook的Infer是使用這項技術最易於上手的生產工具。它透過將程式碼編譯為中間表示,並在每個函式上執行組合式抽象詮釋來分析Java、C、C++和Objective-C。Infer按函式快取結果,因此增量建置很快。這正是讓它在CI中可行的秘訣。

為什麼你的程式碼檢查工具不夠

程式碼檢查工具看語法。類型檢查器看類型。抽象詮釋看跨路徑的行為。

程式碼檢查工具可以標記你忘了檢查空值。類型檢查器可以強制函式回傳Optional<T>。但兩者都無法可靠地捕獲你在第47行解引用了一個指標,而在一系列複雜的分支之後,某條路徑讓它未初始化。抽象詮釋追蹤該指標通過每個分支和合併點的可能狀態。

權衡是噪音。抽象詮釋會產生誤報。它可能報告一個你的商業邏輯保證永遠不會發生的空指標解引用。分析器不知道你的不變量。它只知道程式碼字面上允許什麼。

Infer的預設檢查器經過調整,可以將大多數程式碼庫的誤報率保持在10-15%左右。這比類型檢查器高,但它發現的缺陷往往是那些從程式碼審查和測試中溜走的。

將Infer新增到CI管道

你不需要從原始碼建構Infer。Facebook發布了Docker映像檔。以下是一個可以運作的GitHub Actions工作流程,用於分析Java專案:

# .github/workflows/infer.yml
name: Abstract Interpretation

on: [pull_request]

jobs:
  infer:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Run Infer
        uses: docker://ghcr.io/facebook/infer:main
        with:
          args: >
            infer run
            --make-command "mvn compile"
            --
            mvn compile

      - name: Upload report
        uses: actions/upload-artifact@v4
        with:
          name: infer-report
          path: infer-out/report.json

對於Node.js或Python專案,替換建置命令。Infer不原生分析JavaScript或Python,但你可以在那些專案通常依賴的C/C++擴充功能上執行它。如果你處於純受控語言環境中,你仍然可以從CodeQL或SonarQube等工具獲得類似的路徑敏感分析,儘管它們底層引擎不同。

對於C或C++專案,設定更簡單:

# .github/workflows/infer-cpp.yml
name: Infer C++ Analysis

on: [pull_request]

jobs:
  infer:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build with Infer
        uses: docker://ghcr.io/facebook/infer:main
        with:
          args: >
            infer run
            --make-command "make"
            --
            make

infer run --make-command模式在正常建置過程中攔截編譯器呼叫。Infer提取中間表示,分析它,並將結果寫入infer-out/。你的實際建置產物不受影響。

讀取輸出和調整誤報

Infer將發現結果輸出到infer-out/report.json和可人工閱讀的infer-out/report.txt。一個典型的發現如下所示:

src/parser.c:142: error: NULL_DEREFERENCE
  pointer `node` last assigned on line 138 could be null and is dereferenced at line 142, column 5

訊息告訴你變數、指派位置和解引用發生的位置。你可以在編輯器中追蹤路徑。

如果Infer太吵,你可以抑制特定檢查器或註解程式碼以跳過分析:

// src/parser.c
// infer-ignore: the parent check guarantees node is non-null here
node->value = parsed;

或全域停用特定檢查器:

infer run --make-command "make" --no-bufferoverrun --

buffer overflow 檢查器在具有複雜指標運算的程式碼上特別容易產生誤報。我通常在遺留C程式碼庫上停用它,保持空指標解引用和記憶體洩漏檢查器活動。這兩個發現的真正缺陷率證明了審查時間的價值。

建置時間權衡

抽象詮釋不是免費的。在中等規模C++專案上的完整Infer執行可能比正常建置長2-4倍。增量分析有幫助:在後續執行中,Infer只重新分析已更改的函式及其依賴項。實際上,這意味著10分鐘的建置在乾淨的CI執行中可能變成15-20分鐘,但在增量執行中只需3-5分鐘。

如果你的CI預算緊張,在提取要求上執行Infer,而不是在每次推送到main時。或每晚執行。它發現的缺陷通常值得延遲,但正確的頻率取決於你的團隊對CI時間的容忍度。

另一個選擇是在推送前本地執行Infer。相同的Docker映像檔適用於任何安裝了Docker的機器:

docker run --rm -v $(pwd):/workspace -w /workspace \
  ghcr.io/facebook/infer:main \
  infer run --make-command "make" --

Infer無法捕獲什麼

Infer是組合式的。它隔離分析函式,並使用摘要來建模呼叫者和被呼叫者。這讓它可擴展,但意味著需要分析完整呼叫圖的跨函式、路徑敏感缺陷可能會漏掉。

它也不會發現邏輯缺陷。如果你的程式碼安全地解引用了一個指標但使用了錯誤的值,Infer保持沉默。它是一個安全性檢查器,不是正確性神諭。

並發缺陷是有限的。Infer有一個競態條件檢查器,但它是實驗性的,產生的誤報足以讓大多數團隊關閉它。

下一步做什麼

從小處開始。選擇一個使用編譯語言的專案,添加上面的GitHub Actions工作流程。讓它在接下來的幾個提取要求上執行。與團隊一起審查發現結果,並為噪音建構抑制清單。

一週後,你會感覺到訊號是否值得CI時間。根據我的經驗,在現有C或Java程式碼庫上的第一次執行總會發現至少一個程式碼審查遺漏的空指標解引用。這通常足以證明保留它是值得的。

如果你想深入了解,Infer文件涵蓋了用OCaml編寫自訂檢查器。這就是博士學位派上用場的地方。對於其他一切,預設檢查器和Docker映像檔就足夠了。

FAQ

抽象詮釋簡單來說是什麼? 它是一種靜態分析技術,近似你的程式如何行為,以證明諸如「此指標永遠不為空」之類的屬性,而無需實際執行程式碼。

Infer免費使用嗎? 是的。Infer在MIT授權下開源,由Meta維護。

Infer與SonarQube相比如何? SonarQube根據語言混合使用模式匹配、汙染分析和一些更深入的分析。Infer專門建立在抽象詮釋之上,在C、C++、Java和Objective-C方面以SonarQube通常不具備的方式進行路徑敏感分析。

我可以在JavaScript或Python上執行Infer嗎? 不能直接執行。Infer分析編譯語言。對於JavaScript和Python,考慮CodeQL或具有嚴格規則的類型感知程式碼檢查工具如ESLint或Pyright。

Infer會顯著降低CI速度嗎? 完整分析需要2-4倍建置時間。提取要求上的增量分析快得多,通常只增加幾分鐘。