1.前言:
鴻蒙程序分析框架ArkAnalyzer(方舟分析器)
源碼地址
入門文檔
2.閱讀入門文檔后:
本人具有一定的Java開發經驗。雖然我對 TypeScript(TS)和 ArkTS 還不熟,但很多概念對我這個 Java 開發者來說并不陌生,反而有種親切感。
在我看來,ArkAnalyzer 本質上就是一個針對 ArkTS 語言的靜態代碼分析框架。這讓我想起了我在 Java 世界里常用的工具:
-
1.Soot?或?ASM:這些是用來分析和修改 Java 字節碼的框架。ArkAnalyzer 似乎在做類似的事情,不過它操作的是更高層次的 ArkTS 源碼。
-
Java Compiler API (JSR 199):這個 API 允許你在 Java 程序里調用 Java 編譯器,并訪問 AST(抽象語法樹)。ArkAnalyzer 的第一步就是生成 AST,這完全是同一個思路。
-
?Checkstyle:這些是用于代碼質量和安全掃描的工具。它們底層也依賴于靜態分析。ArkAnalyzer 提供了構建這類高級工具所需的基礎能力。
第二步:運行第一個基本示例 (第3.1章)
環境搭好了,我就要開始寫代碼了。照著?ex01?的例子來:
-
準備被分析的項目:我創建一個?demo_project?文件夾,并把?books.ts,?bookService.ts,?index.ts?這幾個文件按結構放好。這個項目很簡單,就是一個圖書管理的模型和服務,非常典型的 Java 示例風格。
-
編寫分析腳本?basicUsage.ts:
-
import { SceneConfig, Scene } from 'arkanalyzer';:導入依賴,和 Java 的?import?一樣。
-
const projectRoot = 'tests';:指定要分析的項目路徑。
-
let config = new SceneConfig();
-
config.buildFromProjectDir(projectRoot);:這兩步是配置分析器。
-
let scene = new Scene();
-
scene.buildSceneFromProjectDir(config);:這兩步是執行分析,構建出?Scene?這個大對象。
-
然后,我開始我的探索
-
scene.getFiles():獲取所有文件,打印文件名。
-
scene.getClasses():獲取所有類,打印類名。這里我注意到一個細節:輸出了好幾個?_DEFAULT_ARK_CLASS。文檔解釋說每個文件和命名空間都有一個默認類。這和 Java 不太一樣,Java 的頂層只能是類、接口或枚舉。TS 似乎更靈活,可以在文件頂層直接寫函數或變量,ArkAnalyzer 把這些“游離”的代碼放進一個默認類里,這個設計很合理。
-
scene.getMethods():獲取所有方法。同樣,我也看到了?_DEFAULT_ARK_METHOD。
-
我嘗試鏈式調用,比如?scene.getFiles()[0].getClasses()[0].getMethods(),來感受一下這個 API 的設計。
-
-
第三步:深入探索 CFG 和調用圖 (第3.2章)
基本 API 熟悉后,我對更高級的功能感興趣。
-
獲取 CFG (ex01.6):
-
我找到?getBooksByAuthor?這個方法,然后調用?getBody().getCfg()。
-
我最感興趣的是?DotMethodPrinter。能把 CFG 導出成?.dot?文件太棒了!我知道 Graphviz 這個工具,它可以把?.dot?文件渲染成圖片。這樣我就能直觀地看到那個?for?循環和?if?判斷構成的流程圖了。這對于理解復雜方法的邏輯非常有幫助。
-
-
生成調用圖 (ex02):
-
這部分提到了?CHA, RTA, PTA?三種算法,這對我來說是新知識,但概念不難理解。
-
CHA (Class Hierarchy Analysis):基于類的繼承關系來分析。比如?animal.sound(),如果?animal?的靜態類型是?Animal,那么 CHA 會認為?Dog.sound(),?Cat.sound(),?Pig.sound()?都可能被調用。這是一種最快但最不精確的算法。
-
RTA (Rapid Type Analysis):在 CHA 的基礎上,會去看代碼里到底?new?了哪些類的實例。如果代碼里只?new Cat()?和?new Dog(),那 RTA 就不會把?Pig.sound()?加到調用圖里。比 CHA 精確。
-
PTA (Points-to Analysis):指針分析,最精確也最慢。它會去追蹤每個變量可能指向哪些具體的對象實例。在?makeSound(new Dog())?這個調用里,PTA 能夠精確地知道傳入?makeSound?函數的?animal?參數指向的就是一個?Dog?對象,所以?animal.sound()?只會調用?Dog.sound()。
-
-
我把三種算法都跑一遍,對比它們的差異。
-
PTA算法簡單分析:https://blog.csdn.net/2302_80118884/article/details/151649501?spm=1001.2014.3001.5501
一個例子:
// --- 基礎類 ---
abstract class Animal { abstract sound(): void; }
class Dog extends Animal { sound() { /* 汪汪 */ } }
class Cat extends Animal { sound() { /* 喵喵 */ } }
class Pig extends Animal { sound() { /* 哼哼 */ } }class PetStore {bestSeller: Animal | null = null;inventory: Animal[] = [];setBestSeller(pet: Animal) {this.bestSeller = pet;}stockInventory(pets: Animal[]) {this.inventory = pets;}promoteBestSeller() {if (this.bestSeller) {this.bestSeller.sound();}}
}function main1() {const store = new PetStore();const myDog = new Dog();const myCat = new Cat();// 1. 將 Dog 實例設置到 store 的字段中store.setBestSeller(myDog);// 2. 調用一個方法,該方法會使用這個字段store.promoteBestSeller();
}
我可能會遇到的問題和想進一步了解的
-
ArkUI 和 ViewTree:這是我完全陌生的領域。@State,?@Prop?這種裝飾器看起來像是某種數據綁定機制,類似于前端框架(React, Vue)或 Android Jetpack Compose。ViewTree?顯然是用來分析 UI 結構的。作為一個后端 Java 開發者,這部分對我來說很新奇。我會好奇這個?ViewTree?是如何從代碼中構建出來的,以及它能用來做什么樣的 UI 分析(比如,查找所有未綁定的 UI 控件?分析頁面跳轉邏輯?)。
-
數據流分析的深度:ex05.1 空指針檢測?很有用,這在 Java 里就是?NullPointerException?分析。我想知道 ArkAnalyzer 的數據流分析能力有多強?它能處理多復雜的場景?比如跨文件、跨模塊的污點分析(Taint Analysis),即追蹤一個用戶輸入(可能是惡意的)在系統中的流向,最終是否被未經驗證地執行。
-
可擴展性:我能自定義規則嗎?比如,我想寫一個檢查器,規定我們項目中所有的?Service?類都必須以?Service?結尾。我可以通過?Scene?API 遍歷所有類,然后檢查類名來實現。這個看起來很簡單。但如果我想寫一個更復雜的規則,比如“所有從數據庫讀取的數據,在返回給前端前必須經過一個特定的脫敏函數處理”,這就需要用到數據流分析了。ArkAnalyzer 是否提供了方便的 API 來讓我構建這種自定義的、基于數據流的檢查器?
從文檔腳本分析ts源碼:
import { SceneConfig, Scene} from 'arkanalyzer';
const projectRoot = 'tests';
let config: SceneConfig = new SceneConfig();
//1
config.buildFromProjectDir(projectRoot);
let scene: Scene = new Scene();
//2
scene.buildSceneFromProjectDir(config);
1.調用?config.buildFromProjectDir('tests')?時,SceneConfig?對象內部主要完成了?三件核心任務,這是一個為后續分析進行“信息采集”和“環境設置”的關鍵步驟。
3.buildFromProjectDir?函數源碼
// src/SceneConfig.tspublic buildFromProjectDir(targetProjectDirectory: string): void {// 任務1:記錄項目的根目錄路徑this.targetProjectDirectory = targetProjectDirectory;// 任務2:根據目錄路徑推斷項目名稱this.targetProjectName = path.basename(targetProjectDirectory);// 任務3:掃描并收集項目下所有的源文件this.projectFiles = getAllFiles(targetProjectDirectory, this.options.supportFileExts!, this.options.ignoreFileNames);
}
三大任務詳解
任務 1: 記錄項目根目錄 (this.targetProjectDirectory = targetProjectDirectory)
-
發生了什么:
這行代碼非常直接,它把你傳入的字符串?'tests'?保存到了?SceneConfig?對象的?targetProjectDirectory?這個內部屬性里。 -
為什么重要:
這是整個分析的?“錨點”。后續所有操作,比如解析?tsconfig.json、查找依賴、構建?Scene?等,都需要知道項目的根目錄在哪里。targetProjectDirectory?就是這個基準路徑。
任務 2: 推斷項目名稱 (this.targetProjectName = path.basename(targetProjectDirectory))
path.basename('tests')?會返回路徑的最后一部分,也就是?'tests'?這個字符串本身。所以,targetProjectName?屬性也被賦值為?'tests'。如果你的路徑是?'C:/Users/MyUser/MyApp',那么項目名就會被推斷為?'MyApp'? ? 項目名稱在?Scene?中用于標識和區分不同的代碼來源,尤其是在進行跨項目分析或處理依賴時,這個名稱會非常有用。
任務 3: 掃描并收集所有源文件 (this.projectFiles = getAllFiles(...)
-
它調用了一個名為?getAllFiles?的輔助函數(源碼在?src/utils/getAllFiles.ts)。這個函數會:
-
從你指定的根目錄?'tests'?開始。
-
遞歸地?遍歷?'tests'?文件夾以及其下的所有子文件夾。
-
在遍歷過程中,它會檢查每一個文件的后綴名。
-
如果一個文件的后綴名存在于?this.options.supportFileExts?數組中(默認是?['.ets', '.ts']),那么這個文件的?完整絕對路徑?就會被收集起來。
-
如果配置了?ignoreFileNames,它還會跳過這些被忽略的文件。
-
最終,getAllFiles?返回一個包含所有符合條件的源文件絕對路徑的字符串數組。
-
這個數組被賦值給?SceneConfig?對象的?projectFiles?屬性。? ? ? ? ? ? ? ? ? ? ? ? projectFiles?列表就是?Scene?對象接下來需要處理的?“工作清單”。在?scene.buildSceneFromProjectDir(config)?這一步中,Scene?會從?config?對象中獲取這個文件列表,然后對列表中的每一個文件路徑,執行我們之前討論過的“讀取 -> 解析AST -> 構建ArkFile”的完整流程。沒有這個文件列表,Scene?就不知道要分析哪些文件。
-
所以,config.buildFromProjectDir('tests');?這句看似簡單的代碼,實際上完成了一個至關重要的預處理階段。它為?SceneConfig?對象填充了三個核心屬性:
-
targetProjectDirectory:?'tests'?(分析的根在哪)
-
targetProjectName:?'tests'?(分析的是什么項目)
-
projectFiles:?['D:\codeArk\tests\main.ts', 'D:\codeArk\tests\models\user.ts', 'D:\codeArk\tests\services\authService.ts']?(具體要分析哪些文件,路徑是絕對的)
當這個?config?對象被傳遞給?new Scene()?并用于構建?Scene?時,Scene?就擁有了開始正式分析所需的所有初始信息。
4.buildSceneFromProjectDir源碼
public buildSceneFromProjectDir(sceneConfig: SceneConfig): void {//環境初始化this.buildBasicInfo(sceneConfig);//生成ArkFilethis.genArkFiles();}
關鍵調用路徑:buildSceneFromProjectDir->genArkFile->buildMethodBody->buildBody->build
關鍵調用路徑逐步分析:https://blog.csdn.net/2302_80118884/article/details/151649408?spm=1001.2014.3001.5502
4.IR
它的標準格式是:
result = operand1 operator operand2
這行代碼里正好有三個“地址”(或者說,三個變量/值的位置):
-
result: 存放結果的地址。
-
operand1: 第一個操作數的地址。
-
operand2: 第二個操作數的地址。
這就是它叫“三地址碼”的原因。
代碼解讀 (Stmt.ts,?Expr.ts)
Stmt?(Statement, 語句):可以理解為一條完整的?“指令”。
Expr?(Expression, 表達式):可以理解為構成指令的?“操作數”或“計算過程”。
-
Stmt.ts?(語句):?這里定義了所有可能的三地址碼?指令。比如:
-
ArkAssignStmt: 賦值語句 (x = y)
-
ArkInvokeStmt: 調用語句 (foo(a, b))
-
ArkIfStmt: 條件跳轉 (if (x > 0) goto L1)
-
ArkReturnStmt: 返回 (return x)
-
-
Expr.ts?(表達式):?這里定義了構成語句的各種?操作數和計算。比如:
ArkInstanceInvokeExpr: 封裝了調用一個實例方法所需的所有信息。ArkNewExpr:?new MyClass()? ? ? ? ? ?ArkConditionExpr:?a < b? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
例子:let result = myCalculator.add(5, b);
[ArkAssignStmt]/ \/ \(leftOp) / \ (rightOp)/ \[Local (name='result')] [ArkInstanceInvokeExpr]/ | \/ | \(base) / (methodSignature) \ (args)/ | \[Local (name='myCalculator')] [MethodSignature] [Array](points to 'add' method) ||-------------------| |(element 0) | | (element 1)| |[Constant (value=5)] [Local (name='b')]
CFG → 調用圖(CG)
這是“建立全局聯系”階段。?我們檢查每個方法的 CFG,找出里面的調用指令,從而繪制出整個項目中方法與方法之間的調用關系網。
代碼解讀 (CallGraphBuilder.ts)
buildDirectCallGraph?方法:
public buildDirectCallGraph(methods: ArkMethod[]): void {this.buildCGNodes(methods);for (const method of methods) {let cfg = method.getCfg();if (cfg === undefined) {// abstract method cfg is undefinedcontinue;}let stmts = cfg.getStmts();//遍歷一個方法CFG中每一條IR語句for (const stmt of stmts) {//檢查這條語句是不是一個調用語句,// 是的話就提取出被調用方法的簽名(方法名+參數類型)let invokeExpr = stmt.getInvokeExpr();if (invokeExpr === undefined) {continue;}//區分靜態調用和動態調用。 // 如果是靜態調用(比如 Math.random()),目標函數 callee 是唯一確定的。// 如果是動態調用(比如 new Array()),目標函數 callee 需要通過類型推斷來確定。let callee: Method | undefined = this.getDCCallee(invokeExpr);// abstract method will also be added into direct cgif (callee && invokeExpr instanceof ArkStaticInvokeExpr) {//如果是靜態調用,直接在調用圖(cg)中添加一條從當前方法到目標方法的邊。this.cg.addDirectOrSpecialCallEdge(method.getSignature(), callee, stmt);} else {//如果是動態調用(比如 obj.run(),obj 的具體類型不確定),就暫時記錄下這個調用點的信息,// 等待后續的 CHA/RTA 分析來解析它可能的目標。this.cg.addDynamicCallInfo(stmt, method.getSignature(), callee);}}}}
buildClassHierarchyCallGraph?/?buildRapidTypeCallGraph:
這兩個方法就是用來處理上面留下的動態調用問題的。它們利用類繼承關系 (CHA) 或更精確的類型推斷結果 (RTA) 來推測動態調用可能鏈接到哪些具體的方法實現,從而把調用圖補充完整。
第六步:CG → 數據流分析(IFDS 框架)
這是“深度分析與求解”階段。?有了調用圖,我們就可以追蹤數據在方法之間的流動了。這里提供的是一個通用的?數據流分析框架。
代碼解讀 (DataflowProblem.ts,?DataflowSolver.ts):
-
DataflowProblem.ts:
-
這是一個?abstract class?(抽象類),定義了一個數據流問題的?“問卷”。
-
Java類比:?這就像一個?interface。如果你想實現一個特定的分析(比如“空指針分析”),你就需要繼承這個類,并回答問卷上的所有問題:
-
getNormalFlowFunction: 普通語句(如賦值)如何改變數據流信息?
-
getCallFlowFunction: 當調用一個函數時,數據流信息如何從調用者傳遞給被調用者?
-
createZeroValue: 分析開始時,初始的數據流信息是什么?
-
-
這是一個非常優雅的設計,?ArkAnalyzer的作者寫好了通用的求解引擎(DataflowSolver),而用戶只需要填寫這份“問卷”就能定義自己的分析任務。
-
-
DataflowSolver.ts:
-
solve(): 這是求解器的入口。
-
processCallNode(...):?這是處理跨函數數據流的核心邏輯。?當分析流程遇到一個函數調用時,它會:
-
用CHA等手段解析出所有可能被調用的目標方法。
-
對每個目標方法,執行用戶在?DataflowProblem?里定義的?getCallFlowFunction,計算出傳入被調用方法的數據流信息。
-
將新的信息推入被調用方法的入口,繼續分析。
-
同時,它還處理了從被調用方法返回時數據流如何影響調用者后續代碼的邏輯(exit-to-return?和?call-to-return)。
-
-