天天看點

Hades:移動端靜态分析架構

隻有通過别人的眼睛,才能真正地了解自己 ——《雲圖》
Hades:移動端靜态分析架構

背景

作為全球最大的網際網路 + 生活服務平台,美團點評近年來在業務上取得了飛速的發展。為支援業務的快速發展,移動研發團隊規模也逐漸從零星的小作坊式營運,演變為千人級研發軍團協同作戰。

在公司蓬勃發展的大背景下,移動項目架構也有了全新的演進方向:需要支援高效的內建政策,支援研發流程自動化等等,最終提升研發效能,加速産品疊代和傳遞能力。

雖然高效的研發傳遞體系幫助 App 項目縮短了疊代周期,但井噴式的子產品發版和頻繁的項目內建,使得純人工的項目維護和品質保證變得“獨木難支”。

Hades:移動端靜态分析架構

上圖漫畫中,列舉了大型項目在持續優化和維護過程中較為常見的幾類需求。這些需求主要包括以下幾個方面:

  1. 在 CI 流程中加入靜态準入檢查,避免繁瑣的人工 Review 以及減少人工 Review 可能帶來的失誤。
  2. 為了推進項目的優化過程,需要方法數監控、宏定義分析等代碼分析報表和監控。
  3. 零 PV 報表、依賴分析和頭檔案引用規範、無用代碼分析等項目優化方案。

不難發現,這些需求的本質是:借助代碼靜态分析能力,提升項目可持續發展所需要的自動化水準。針對 C/Objective-C 主流的靜态分析開源項目包括:Static Analyzer、Infer、OCLint 等。但是,這些分析工具對我們而言存在一些問題:

  • 開發成本高,收益有限,研發參與積極性不夠。
  • 針對局部代碼分析,跨編譯單元以及全局性分析較難。
  • 增量分析困難,CI 靜态檢查效率低下。
  • 工具性較強,大部分隻作代碼規範檢查,應用範疇局限。
  • 接入和維護成本高,難以平台化。

針對以上背景和現有方案的不足,我們決定自研基于語義的靜态分析架構。

Hades 項目簡介

大衆點評靜态分析架構 Hades,取名源于

古希臘神話中的冥王

。冥王 Hades 公正無私,能夠審視靈魂的是非善惡。

Hades 架構支援語義分析能力,我們希望這種能力不僅僅能夠去實作一個傳統的 Lint 工具,而且能成為創造更多能力的基礎,可以幫助我們更輕松地審視代碼,了解把控大型項目。

Hades 方案選型

文本處理方式

首先,最簡單的靜态分析是字元比對和文本處理。這種方式雖然實作簡單,但是存在能力上限,也不可能在語義了解上有足夠的把控力。另外,以正則比對為核心建立的工具棧難以得到持續優化。為了分析項目的依賴關系,我們需要判斷代碼中的符号含義以及符号間關系(如包含哪些類,類中有哪些方法等),分析過程的正規表達式如下圖所示。

Hades:移動端靜态分析架構

由此可見,繁瑣的文本比對不僅可讀性差,也存在容易分析出錯的問題。

基于編譯器的靜态分析方案

我們需求的本質是對代碼進行分析,而在源代碼編譯過程中,文法分析器會建立出抽象文法樹(Abstract Syntax Tree 縮寫為 AST)。AST 是源代碼的抽象文法結構的樹狀表現形式,樹上的每個節點都表示源碼的一種結構。

Hades:移動端靜态分析架構

以上圖為例,代碼塊區域是用 Objective-C 和 TypeScript 編寫的一個簡單條件語句源碼,下面是其對應的抽象文法結構表達。這種樹狀的結構表達,省略了一些細節(比如:沒有生成括号節點),從圖中的這種映射關系中我們也可以發現:

  • 源碼的文法結構是可以通過明确的資料結構表示的。
  • 大多數程式設計語言都可以用相似的 AST 表達的。

對于 C/Objective-C 而言,主流編譯器是 Clang/LLVM(Low Level Virtual Machine)的,它是一個開源的編譯器架構,并被成功應用到多個應用領域。Clang(發音為/klæŋ/,不是C浪)是 LLVM的一個編譯器前端,它目前支援 C, C++, Objective-C 等程式設計語言。Clang 會對源程式進行詞法分析和語義分析,将分析結果轉換為 AST。現有方案中不少 Lint 工具便是基于 Clang 的,Clang 包含了以下特點:

  • 編譯速度快:Clang 的編譯速度遠快于 GCC。
  • 占用記憶體小:Clang 生成的 AST 所占用的記憶體是 GCC 的五分之一左右。
  • 子產品化設計:Clang 采用基于庫的子產品化設計,易于 IDE 內建及其他用途的重用。

是以,借助 Clang 的子產品化設計和高效編譯等諸多優點,Hades 也将更容易開發和更新維護。Clang 對源碼強有力的分析能力也是主流靜态分析工具的不二之選。

Clang AST 初識

Clang 項目非常龐大。僅僅是 Clang AST 相關代碼就超過 10W+ 行代碼。如何利用 Clang 實作 AST 分析工作,這裡可以參考官網提供的文檔 Choosing the Right Interface for Your Application ,以下是三種方式:

  • LibClang

    提供 C 語言的穩定接口,支援Python Binding。AST 并不完整,不能完全掌控 Clang AST。

  • Clang Plugins

    提供 C++ 接口,更新快,不能保留上下文資訊。插件的存在形式是一個動态連結庫,不能在建構環境外獨立存在。

  • LibTooling

    提供 C++ 接口,更新快,可以通過标準的 main() 函數作為入口,可獨立運作,能夠完全掌控 AST,相比 Plugin 更容易設定。

這裡我們選擇可獨立運作并且能完全掌控 AST 的 LibTooling 作為 Hades 的基礎。

在使用 Clang 的學習過程中,基本的概念便是表示 AST 的節點類型,這裡重要的幾點是:

  • ASTContext。

ASTContext 是編譯執行個體用來儲存 AST 相關資訊的一種結構,也包含了編譯期間的符号表。我們可以通過

TranslationUnitDecl * getTranslationUnitDecl():

方法得到整個翻譯單元的 AST 的入口節點。

  • 節點類型。

AST 通過三組核心類建構:Decl (declarations)、Stmt (statements)、Type (types)。其它節點類型并不會從公共基類繼承,是以,沒有用于通路樹中所有節點的通用接口。

  • 周遊方式。

為了分析 AST,我們需要周遊文法樹。Clang 提供了兩種方式:RecursiveASTVisitor 和 ASTMatcher。RecursiveASTVisitor 能夠讓我們以深度優先的方式周遊 Clang AST 節點。我們可以通過擴充類并實作所需的 VisitXXX 方法來通路特定節點。

ASTMatcher API 提供了一種域特定語言(DSL)來建構基于 Clang AST 的謂詞,它能高效地比對到我們感興趣的節點。

除了這兩種方式外,LibClang 也提供了 Cursors 來周遊 AST。更多細節内容可以前往 :clang.llvm.org 。

常用開源工具的不足

通過上一章節的介紹,我們大緻了解了 Clang 的基本特點。 但是在實踐開發過程中發現:通過 Clang API 去周遊和分析 AST 的源碼樹形結構較為複雜。現有靜态分析方案(如:OCLint),大多是直接給出封裝好的 Lint 工具,擴充方面也是提供腳手架生成 Rule 檔案,然後在 Rule 中編寫通路特定 AST 節點的方法(例如:VisitObjCMethodDecl 方法用來通路 Objective-C 的方法定義)。

是以,現有方案大多數隻提供了直接通路 AST 的方式,而且這種方式較為“局部”。每實作一個實際需求需要耗費大量精力去了解如何從 AST 分析映射到源碼的語義邏輯。

但是,Code Review 時我們并不會将目标代碼轉換為 AST 然後再去分析代碼的語義如何,更多的是直接了解代碼的具體邏輯和調用關系。AST 樹狀結構分析的複雜性容易帶來了解上的差異鴻溝。是以,這也不利于調動業務研發團隊的積極性,很多基于源碼分析工作也難以落地。

Hades 核心實作

為了讓分析過程更清晰,我們需要在 AST 的基礎之上再進行一次抽象。本章節主要内容包含:Hades 的整體架構、為什麼要定義語義模型、定義什麼樣的語義模型、如何輸出語義模型以及模型的序列化和持久化。

Hades 總體架構

按照 Hades 的架構目标進行基礎方案選型以後,我們來看下 Hades 的整體技術架構,可以用下圖所示的四層架構表示:

Hades:移動端靜态分析架構

下面簡述下這幾層的不同職責:

編譯器架構層。Clang 的諸多優勢前文已經提到,這也是 Hades 的基礎依賴。

Hades 核心層。在編譯器架構層,我們借助 Clang 得到了代碼的抽象文法結構表示 AST。而 Hades 核心層的職責便是将 AST 解析成人們更容易了解的,更高層級的語義模型。

Hades 接口封裝層。抽象出的模型,能夠像 Clang 提供豐富 AST 通路接口那樣,為開發者提供豐富的模型通路接口。

靜态分析應用。通過 Hades 接口封裝,我們無需清楚底層模型是如何生成的,在這一層我們可以制作 Lint 或者其它監控、分析工具。

為什麼 Hades 的架構設計是這樣的呢?下面我們将一一道來。

為何要定義語義模型 ?

首先,正如「常用開源工具的不足」章節所述,大多現有方案是直接通過編譯器前端提供的接口實作對 AST 的操作,進而達到靜态分析的目的。

當然,除了現有方案的不足以外,在業務研發過程中出現的 Case ,其原因大多數并不是違反了現有的 Lint 工具中所定義的基本文法規範,這些規則分析的往往是“常識”類問題。在靜态分析中,更多的是對象的錯誤方法調用和非法的繼承/複寫關系等問題,即便具備良好的編碼規範也會疏忽。這裡乍一看沒太大差別,但是從着重點來說,Hades 的設計理念上會存在本質差別。

Hades:移動端靜态分析架構

如上圖所示,現有方案如 OCLint 或者 Clang Static Analyser 等,其核心原理是在編譯器将源碼生成 AST 時,通過分析節點和節點間的關系,進而達到靜态分析的目的。這種方式不利于跨編譯單元分析,自然對項目級别的了解分析存在局限性。

是以,這裡可以借助 AST 針對每個編譯單元建立更直覺的、更容易了解的結構化表達。我們将這個更高層級的語義表達稱為 HadesModel。

定義什麼樣的語義模型 ?

建立 HadesModel 以後的靜态分析中,我們的着重點變化如下圖所示:

Hades:移動端靜态分析架構

下面我們可以簡單描述需要設計的 HadesModel 的基本特點:

  • HadesModel 可以結構化表達源碼的語義。它能夠表達一個編譯單元定義了哪些接口聲明、實作了哪些類/類别的方法、定義和展開了哪些宏定義、對象的方法調用和函數使用情況等等。
  • HadesModel 使我們不需要了解 Clang 編譯器以及 AST 如何表達源碼。
  • HadesModel 以一個完整的編譯單元為機關,支援 JSON 格式表達。
  • 對于 Objective-C ,分析過程不必強依賴于 xcodebuild 編譯建構過程。

通過以上幾點特征描述,我們得到了 HadesModel 更清晰的表述:

HadesModel 是基于 AST 的更高層級語義表達,它能夠序列化為 JSON 格式并描述完整的編譯單元,這種結構化資訊使得靜态分析能更接近于開發者閱讀了解源碼的思維習慣。

在介紹完 HadesModel 的基本目标後,我們用下面一段簡單的 Objective-C 代碼為例來明确 HadesModel 的具體表達形式:

Hades:移動端靜态分析架構

在示例代碼中,我們簡單了解下包含的語義邏輯:

  • 這是一段 Objective-C 代碼,實作檔案名為

    HadesViewController.m

  • 在實作檔案中,定義了一個名為

    HadesMacro

    的宏定義。
  • 實作檔案中包含了

    HadesViewController

    類的實作部分,

    HadesViewController

    UIViewController

    的子類。
  • HadesViewController

    類中包含了兩個方法實作。其中第一個方法名為

    sayHello

    ,裡面包含了局部對象

    testView

    的初始化以及對象的方法調用,另外還包含了宏定義的使用。

可以發現,HadesModel 能夠表達開發者對語義資訊的直覺了解即可。

如何生成語義模型:HadesModel ?

接下來介紹 Hades 基本架構圖中 HadesCore 的核心實作,重點在如何生成前文所述的 HadesModel。

這裡 HadesCore 借助 Clang LibTooling 分析源碼的 AST,然後将我們所需的語義資訊抽象成 HadesModel。将資料抽象和轉換過程用以下簡要流程表示:

Hades:移動端靜态分析架構

下面将從一個流程圖來看看 HadesCore 是如何生成 HadesModel 的實作細節:

Hades:移動端靜态分析架構

流程圖中主要包括以下幾點内容。

1. 建構編譯資料庫

首先,Hades 是基于 Clang 的子產品化設計開發,是以它可以獨立運作,是以,可以利用 RubyGem 的方式将模型生成過程封裝并提供指令行工具。對于需要得到 HadesModel 的編譯單元

.m

,首先需要作為源檔案內建到 workspace (iOS 可以用 CocoaPods),然後利用 Xcode 提供的 xcodebuild 結合 xcpretty 編譯得到項目的編譯資料庫

compile_commands.json

。編譯資料庫用來指定每個編譯單元的指令行參數。

2. 建立 HadesDriver

在建立驅動器之前,可以使用 Clang 提供的

CommonOptionsParser

類,它将負責解析與編譯資料庫和輸入相關的指令行參數,然後将其作為驅動器的輸入。驅動器控制整個模型生成周期,它的輸出結果便是 HadesModel。

3. 建構 HadesModel

在 HadesDriver 的驅動下,首先需要建立編譯器執行個體,執行編譯前可以分析宏定義和頭檔案展開等預處理資訊,并将這些内容初始化到 HadesModel 對象。接着,在編譯器執行個體中将

FrontendAction

接口作為擴充編譯過程的執行入口,利用 Clang LibTooling 提供的 ASTVistor 通路 AST 節點(更多 Clang 技術細節見:Clang 8 documentation),最終将所有翻譯單元的“中繼資料”填充到 HadesModel。

以前文的

HadesViewController.m

為例,我們得到 HadesModel 并序列化為 JSON 資料以後,如下圖所示:

Hades:移動端靜态分析架構

顯然,示例 HadesModel 已經能夠表達開發者 Code Review 時,絕大多數“直白”的語義資訊了。

HadesModel 的序列化/持久化

由于 HadesModel 最終需要以 JSON 格式作為提供靜态分析的原始資料類型,是以需要保證 HadesModel 具備序列化的能力。

JSON 格式使 Hades 具備了全局分析能力,也符合設計之初的分析和平台、語言無關的要求。再者,JSON 類型也友善利用具備較好類型系統的語言作為分析接口層。

實踐中,以 iOS 常用的 CocoaPods 的 Pod 為機關,在私有 Pod 發版時生成模型資料然後打包存儲在 Maven 中,以便于增量分析。

在 CI 系統中,特别是大型項目持久化的模型存儲非常重要。CI 中為了加快內建速度,不得不使用部分二進制的內建方式,但是這樣将無法對靜态庫進行源碼分析。利用 Hades 的模型緩存,我們可以解決二進制內建的局限性。緩存資料也不需要再次編譯、模型生成等耗時操作,是以接入 Hades 後基本不影響內建項目的內建速度。

Hades 應用案例(1):制作 Lint 工具

在這一章,我們将介紹 Hades 架構中的接口層,以及在 Lint 工具上的應用。

HadesLint 架構描述

HadesLint 是基于 Hades 架構制作的靜态分析工具。作為平台标準的 Lint 工具,目前在持續內建有了廣泛應用(詳情見此篇文章:MCI:大衆點評千人移動研發團隊怎樣做持續內建?)。

HadesLint 開發語言是 TypeScript。它具備完善的類型系統,結合 VSCode 的智能補全和完善的 Debug 能力,使得 HadesLint 具備良好的開發體驗。

HadesLint 的實作細節如下圖所示:

Hades:移動端靜态分析架構

在接入 HadesLint 的項目後,我們将項目以 Pod 為機關,從 Maven 中讀取緩存模型 Zip 包。如果不存在緩存,那麼将利用前文所述封裝好的 HadesGem 通過編譯資料庫實時生成每個編譯單元的 HadesModel。

由于我們的項目較大,模型資料量也非常龐大,為了防止分析過程記憶體洩露的危險,提升分析性能,可以通過

Lazy.js

進行惰性求值,漸進加載有效解決了模型資料龐大的問題。

被 Lazy.js 加載的 JSON 對象,需要通過 TypeScript 聲明來保證 HadesModel 具備類型。這樣,我們就可以在 VSCode 中編寫代碼時,享受自動補全、類型推斷,進而保證編寫過程更加安全、高效。借助 VSCode 對 TypeScript 的良好支援,在編寫分析過程中友善地 Debug。

最後 HadesLint Driver 會加載每個規則對象,在規則中分析 HadesModel 然後确定檢查項是否合法。

當然,如果希望程式執行效率更高些,也可以嘗試 OCaml+ATD 來建構 Lint 項目。

HadesLint 應用案例:列印項目中的類名

需求描述:我們需要找到項目中定義的所有類名。

我們隻需要通過腳手架建立新的規則,然後編寫以下代碼(HadesLint規則代碼):

this.hadesModels.each((hadesModel: HadesModel.HModel) => {
  hadesModel.class_list.forEach((occlass: HadesNode.Class) => {
    console.log(occlass.name);
  })
});
           

編寫代碼以後,可以在 VSCode 的 Debug 面闆中開啟調試:

Hades:移動端靜态分析架構

當然,除了以上簡單的查詢功能以外,我們也可以定制相對複雜的檢查規則,比如繼承鍊管控、方法複寫檢查、非空檢查等。

在引出方法複寫管控之前,開發者往往會通過随意繼承的方式複寫代碼,或者通過不合理擴充方式來滿足目前需求。但是,人工 Review 代碼很難保證內建項目中,這些擴充或者子類在運作時的行為。是以,對繼承鍊管控的需求非常有必要。我們的 App 之前就出現了擴充同名方法,意外導緻方法複寫,進而在程式運作時出現問題,甚至導緻 Crash。

為此,我們在內建準入檢查中加入了方法覆寫檢查。當然,如果父類設計之初本身是希望子類複寫,我們在 Lint 過程中通常會忽略這些合法的複寫情況。

對于這類跨編譯單元的分析需求,如果我們按照 Clang Static Analyser 是較難分析的,但是 Hades 就可以非常輕松地做到,因為 Hades 可以輕松擷取整個繼承鍊以及每個類的實作定義。

Hades 應用案例(2):建構 HadesDB

HadesModel 是結構化資料,是以,我們也可以将這些模型資料以 Document 的形式存儲到文檔型資料庫中,例如:CouchDB。

在 CouchDB 的基礎上建立模型資料庫,這樣便能夠友善地通過 Map-Reduce 建立視圖文檔(Design Documents),然後,我們可以擷取項目中包含的類及其方法清單、分析每個 Document 的字段按需輸出結果。

例如,存儲建立完整的項目 HadesModel 資料後,在 CouchDB 中建立 Design Document,然後在 Map Function 中編寫以下代碼:

function (doc) {
  if (doc.extracontext.macro_list !== null) {
    emit(doc._id, doc.extracontext.macro_list);
  }
}
           

CouchDB 支援 JS 代碼編寫 map-reduce,以上代碼表示在目前的資料庫中,對于每個 HadesModel Document 判斷是否存在宏定義,如果存在,那麼輸出宏定義作為 Design Document 的結果。

最後,通過 CouchDB 接口傳回可以擷取如下結果:

// App 項目中源碼中使用的所有宏定義資訊:
{
  "total_rows": xxx,
  "offset": 0,
  "rows": [
    {
      "id": "NVShopInfoBlackPearlMultiDealCell",
      "key": "NVShopInfoBlackPearlMultiDealCell",
      "value": [
        {
          "name": "NVActionSheet",
          "expanded": true,
          "expandstr": "UIResponder<NVActionSheetDelegate> *",
          "location": ${path_location},
          ...
        }
      ]
    },
    ...
 ]
}
           

有了 HadesDB 以後,我們能賦予代碼語義分析更大的想象空間。比如,可以利用 HadesDB 制作 Web 項目,通過 Web 頁面搜尋、查詢我們所需要知道的語義資訊和分析資料。

總結

本文介紹了在美團點評業務快速發展背景下,針對大型移動項目的靜态分析需求,結合開源項目利弊,最終設計實作的靜态分析架構 Hades。

Hades 作為大衆點評移動研發的基礎設施之一,在實踐中得到了廣泛的應用,為大型 App 項目的日常維護、代碼分析提供支援。基于 HadesModel 的靜态分析易上手,開發接入成本低,能夠了解代碼語義,具備全局分析能力等諸多優點。

最後,我們也希望 Hades 的設計是賦予創造能力的能力,而不僅僅是作為傳統意義上的 Lint 輔助工具,這也是我們為什麼不取名為“工具”,而是稱之為“架構”的原因。當然,基于 Hades 我們也是能夠很友善地制作出 Lint 工具的。

Hades 是否開源?不久将會開源,敬請期待。如果對我們平台感興趣,歡迎小夥伴們加入大衆點評的大家庭。

參考資料

  • [1] Clang 8 documentation
  • [2] Infer static analyzer
  • [3] Clang Tidy
  • [4] OCLint static analyzer
  • [5] Apache CouchDB
  • [6] TypeScript
  • [7] ATD
  • [8] Lazy.js
  • [9] xcpretty
  • [10] Visual Studio Code

作者簡介

  • 吳達,大衆點評 iOS 技術專家,Hades 項目開發者。目前專注于移動 CI 研發,靜态分析和點評 App 業務研發。
  • 智聰,移動資訊元件負責人,大衆點評 iOS 進階專家。專注于移動工具鍊開發,對移動持續內建、靜态分析平台建設有深刻了解和豐富的實踐經驗。

招聘資訊

大衆點評移動研發中心,Base 上海,為美團提供移動端底層基礎設施服務,包含網絡通信、移動監控、推送觸達、動态化引擎、移動研發工具等。同時團隊還承載流量分發、UGC、内容生态、個人中心等業務研發工作,長年虛位以待專注于移動端研發的各路英雄豪傑。歡迎投遞履歷:[email protected]。