引言
眾所周知,資料流分析是實現汙點分析的一種常用技術
資料流分析分為過程內的資料流分析與過程間的資料流分析。前者是對一個方法體內的資料流分析,主要是基於CFG分析,不涉及方法呼叫;後者是基於不同方法間的資料流分析,主要是基於ICFG+CG分析,會涉及方法呼叫。
一、過程內資料流分析
1. CFG的構建
1.1.把程式轉換為IR(此處採用3AC)表示
3地址碼中的地址可能有如下的幾種型別:
- 名字(Name),包括
- 變數(Variable)
- 標籤(Label)
- 用於指示程式位置,方便跳轉指令的書寫
- 字面常量(Literal Constant)
- 編譯器生成的臨時量(Compiler-Generated Temporary)
每一種指令都有其對應的 3 地址碼形式,一些常見的 3 地址碼形式如下:(x, y, z是變數的地址)
x = y bop z // bop 是雙目運算子(Binary Operator),可以是算數運算子,也可以是邏輯運算子
x = uop y // uop 是單目運算子(Unary Operator),可能是取負、按位取反或者型別轉換
x = y
goto L // goto 是無條件跳轉,L 是標籤(Label),是標記程式位置的助記符,本質上還是地址
if x goto L // if... goto 是條件跳轉
if x rop y goto L // rop 是關係運算子(Relational Operator),運算結果一般為布林值
1.2.找程式的Leader集合L,進而劃分Basic Block
- 程式入口
- 跳轉指令的目標指令
- 跳轉指令的下一條指令
(一個Leader到下一個Leader之前就是一個BB)
1.3.連線Basic Block
程式控制流的產生來源於兩個地方:
- 天然的順序執行
- 這是計算系統天然存在的一種控制流
- 跳轉指令
- 這是人為設計新增的一種控制流
示例
二、過程間資料流分析
1.CG 方法呼叫圖
1.1.Java中的方法呼叫型別
- Static Call:呼叫靜態方法 --> 編譯時明確
- Special Call:呼叫構造方法、私有方法、基類例項方法 --> 編譯時明確
- Virtual Call:呼叫其他例項方法 --> 執行時明確(多型,最常見)
所以在構建方法呼叫圖時,最關鍵的是要處理好Virtual Call的情況
1.2.CG的構建方法
- 類層級結構分析(Class Hierarchy Analysis,CHA)
- 快速型別分析(Rapid Type Analysis,RTA)
- 變數型別分析(Variable Type Analysis,VTA)
- 指標分析(Pointer Analysis,k-CFA)
上面的四種方法自上而下精度(Precision)越來越高,但是效率(Efficiency)也越來越低。
本文只關注CHA的方式:
CHA
在方法呼叫點處,只關注caller的宣告型別T及callee的方法簽名sig,會把T及其子類中所有與sig匹配的方法都視為可能的目標方法,示例:
class A {
void foo() { ... }
}
class B extends A { }
class C extends B {
void foo() { ... }
}
class D extends B {
void foo() { ... }
}
類層級結構如下:
現有以下程式碼片段:
void resolve() {
C c = ...;
c.foo();A a = ...;
a.foo();B b = new B();
b.foo();
}
CHA演算法會對於每一個接收變數的宣告型別本身及其子類關於呼叫點處的函式簽名進行方法派發的操作,將所有找到的目標方法加入結果之中。因此,結果如下:
Resolve(c.foo()) = {C.foo()}
Resolve(a.foo()) = {A.foo(), C.foo(), D.foo()}
Resolve(b.foo()) = {A.foo(), C.foo(), D.foo()}
我們需要注意一下的是第三個呼叫點, A.foo()
也在其結果之內,因為對於 B
類本身的方法派發得到的結果是 A.foo()
並且,CHA的Resolve演算法只關心宣告型別,因此 new B()
其實並沒有在演算法中發揮作用,從而我們 Resolve(b.foo())
產生了兩個虛假(Spurious)的目標呼叫 C.foo()
和 D.foo()
CG構建示例:
class A {
static void main() {
A.foo();
}
static void foo() {
A a = new A();
a.bar();
}
void bar() {
C c = new C();
c.bar();
}
}
class B extends A {
void bar() { }
}
class C extends A {
void bar() {
if (...) {
A.foo();
}
}
void m() { }
}
CHA最終構建的CG如下:
在上述例子當中需要注意的是,雖然 A a = new A()
,但是解析 a.bar()
的目標方法時候,依舊會對 A
以及 A
的所有子類作 Dispatch ,故而會有3條從 a.bar()
出發的邊。
最後我們會發現存在一個不可達的方法(Unreachable Method) C.m()
,那麼這個方法中的程式碼就是死程式碼(Dead Code,即在任何情況下控制流都不能到達的程式碼)。
CHA的應用:IDE中的目標方法提示
2.ICFG 過程間控制流圖
2.1.ICFG的構建
ICFG要在CFG基礎上新增call Edges(呼叫邊)、return Edges(返回邊)
ICFG = CFGs + call & return edges ,連線呼叫邊和返回邊的資訊可以從呼叫圖中獲得。因此,過程間控制流圖的精度取決於呼叫圖的精度。
示例:
static void main() {
int a, b, c;
a = 6;
b = addOne(a);
c = b - 3;
b = ten();
c = a * b;
}
static int addOne() {
int y = x + 1;
return y;
}
static int ten() {
return 10;
}
構建的ICFG如下:
從上圖可以看出,在構建ICFG時,仍然保留了Call-to-return edges(呼叫點到返回點的邊),雖然實際程式執行過程不會走這條邊,但是這條邊可以傳遞callee方法不需要的資料,這樣就避免了在目標方法中始終維護其不需要的資料,可以提高效率。
公主號推薦
id:CodeAnalyzer,名稱:CodeAnalyzer Ultra
開源倉庫推薦
https://github.com/HaHarden/CPGPractise