標題:靜態程序分析系列(一)

taibeihacker

Moderator

静态程序分析(一)​

1 前言​

1.1 静态程序的定位​

静态程序分析编程语言应用層面下的一個細分領域,它是一個非常重要的核心內容。
在理論部分,考慮的是如何設計一個語言的語法和語義,如何設計語言的類型系統等等問題;有了語言的語法、語義和類型系統之後,我們需要支撐語言的運行。因此,在環境部分,需要考慮如何為運行中的程序提供運行時環境——如何設計編譯器,在運行時需要怎樣的支持(如內存的分配管理)等等;應用部分則關注如何保證語言所寫出程序的效率、安全性和可靠性,主要考慮如何對程序進行分析,驗證和合成(如何自動合成一個程序)。
一句話概括靜態分析:在保證正確性的前提下,在精度和速度上做平衡。

1.2 静态程序分析的应用​

提高程序可靠性
空指針引用與內存洩漏等
提高程序安全性
隱私信息洩漏:多存在於移動應用安全中
注入攻擊
編譯優化
死代碼消除:在編譯器的機器無關優化環節,將不會對程序執行結果產生影響的代碼(即死代碼)刪除。
循環不變量的代碼移動:在編譯器的機器無關優化環節,在保證不影響程序執行結果的情況下,將循環中的特定語句移動到循環外,使得程序運行時執行的語句數減少。
有助於程序理解
為集成開發環境的功能提供幫助:當你使用VS/Idea/Clion/Eclipse/Android Studio 等等IDE 時,將鼠標懸停在代碼上,IDE 能夠動態地分析並提示你所懸停對象的相關信息,背後使用的技術就是靜態程序分析。

1.3 Sound Complete​

以上就是靜態程序分析所希望做的一些事情,但是實際上靜態分析發展至今,依舊不能完美地解決以上的問題。
Any non-trivial property of the behavior of programs in a r.e. language is undecidable.
在一個即递归可枚举(recursively enumerable)語言中,任何程序行為的non-trivial屬性都是不可解釋的。non-trivial property指的是那些與程序運行行為有關的屬性。
——Rice’s Theorem也就是說,Rice 理論告訴我們,世界上並不存在一種完美的静态分析,可以完美地解決上文提到的那些問題。
202202140954018.png-water_print

由上圖可以直觀地看出,三者成包含關係,而完美的程序分析就是中間的Truth,一個既Sound 又Complete 的狀況,而我們正常的程序分析只能獲得要么sound 要么complete 的結果,一種useful 的結果。
簡單來說,sound 是一種過多的輸出,輸出的是全部的真實報錯和部分的虛假報錯;而complete 與之相反,輸出的是全是真是報錯,但是比truth 少了一部分的報錯。
由上可知,我們在實際使用場景中,自然是希望輸出是sound 的,也就是說可以有誤報但不要有漏報。

1.4 静态程序分析的核心​

1.4.1 抽象:Abstract​

以符號分析為例,將待分析的具體的變量取值抽象為:正、負、零、未知和錯誤。
unknown 指的是,如果當前數值會因為變量改變而呈現為不同的狀態,則全部定義為unknown。
undefined 指的是經過判斷肯定不符合int 定義的,例如一個除以0 的數或者字符等。
202202141012410.png-water_print

1.4.2 近似:Over-approximation​

1.4.2.1 转移函数:Transfer function​

在靜態分析中,近似的核心是定義轉移函數(transfer function)。
transfer function 是針對程序中的每一個語句,在抽象值上定義轉換規則。
transfer function 的規則是根據分析的具體問題與程序語義來定義的。
202202141022884.png-water_print

如上圖所示:這是針對符號分析定義的部分的轉換函數,前五個和第八個一目了然,不做解釋。
第六個因為除數是0,故結果是undefined。第七個是由於我們定義的轉換函數是抽象的,進行運算的目標也是抽像出來的,因此相加結果是unknown 的。舉個例子,即使我正數是1000,負數是-1,經過抽像後再經過轉換函數處理,得到的結果仍是unknown,這也是sound 的一種體現。
根據上述的轉換函數,可以對程序語句進行分析,最終得到的結果如下。
202202141026911.png-water_print

1.4.2.2 控制流:Control Flow​

實際上,在程序分析的過程中,近似的過程中還要考慮程序的控制流。
202202141030650.png-water_print

右圖是左圖的運算流展示,在不同情況下y 會有值,而根據sound 原則,在最後一步的y 值只能是unknown。
在程序分析中,不可能與枚舉所有的路徑,因此flow merging (控制流的匯聚)作為一種近似,在靜態分析中十分常用。

2 中间表示:Intermediate Representation​

2.1 编译器与静态分析器​

首先介紹一下編譯器與靜態分析器的關係:
202202141045561.png-water_print

編譯器主要目的是為了將程序員寫的Source Code 轉換為機器可以理解的Machine Code,並在轉換過程中及時報錯。下面以英語的語法規則為例。
第一步,通過掃描器(Scanner) 對輸入進行掃描,根據正則表達式(Regular Expression) 進行詞法分析(Lexical Analysis),主要是分析輸入的每個字母及單詞是否為合法字母及單詞,接著將通過檢測的內容轉換為Tokens 傳入下一步。
第二步,通過解析器(Parser) 對Tokens 進行掃描,根據上下文無關語法(Context-Free Grammar) 進行語法分析(Syntax Analysis),主要分析幾個單詞的組合是否符合語法結構規則,接著將通過檢測的內容以抽象語法樹(AST) 的形式傳入下一步。其中使用上下文無關語法進行分析是為了加快分析效率。
第三步,通過類型檢查(Type Checker),根據屬性語法(Attribute Grammar) 進行語義分析(Semantic Analysis),理想的目的是為了識別像apples eat me 這種語法結構正確但是語義錯誤的情況,但是實際編譯器只能識別類如“不可進行int 除以string”之類的類型錯誤,故該步稱為類型檢測。生成進一步處理後的AST 傳入下一步。
第四步,為了進行靜態分析優化,要將Decorated AST 轉換為中間表示(IR),一般轉換為三地址碼,再進行靜態分析。最後通過代碼生成器將優化後的代碼生成機器碼傳給機器。

2.2 IR​

2.2.1 AST vs. IR​

以一個循環為例來闡述AST 與IR 的區別。 AST 是一個語法樹的形式,是一個高層級的形式,更加接近程序的源代碼,語言相關的,適合做快速的類型檢測,但是缺少了控制流信息
IR 即為中間表示,圖中以三地址碼表示,是一個低層級的形式,接近機器編碼,語言無關的,結構緊湊,包含了控制流信息,因此更加適合用來做靜態分析。
202202141058531.png-water_print

AST
high-level and closed to grammar structure
usually language dependent
suitable for fast type checking
lack of control flow information
IR
low-level and closed to machine code
usually language independent
compact and uniform
contains control flow information
usually considered as the basis for static analysis
所以IR 更適合靜態分析
IR 沒有固定的定義,常用三地址碼

2.3 三地址码​

三地址碼沒有形式化的定義,通常來說,三地址碼就是最多有三個地址(address) 的表達形式,並且由於這個性質,每個三地址碼最多只會有一個運算符。
202202141105178.png-water_print

在Java 中,可以利用soot 工具來生成三地址碼,在soot 中,被稱之為Jimple。下面以一個do-while循環為例,來分析其三地址碼。
1
2
3
4
5
6
7
8
9
10
package nju.sa.example;
public class DoWhileLoop3AC {
public static void main(String[] args) {
int[] arr=new int[10];
int i=0;
do {
i=i + 1;
} while (arr 10);
}
}
轉換後的三地址碼形式為:
202202141113259.png-water_print

再以Method Call 為例,介紹下面向對象環境下的三地址碼。
1
2
3
4
5
6
7
8
9
10
package nju.sa.example;
public class MethodCall3AC {
String foo(String para1, String para2) {
return para1 + ' ' + para2;
}
public static void main(String[] args) {
MethodCall3AC mc=new MethodCall3AC();
String result=mc.foo('hello', 'world');
}
}
202202141119361.png-water_print

Java 中的invoke 指令對應的方法調用情況:
invokespecial: call constructor, call superclass methods, call private methods
invokevirtual: instance method call
invokeinterface: cannot optimization, checking interface implementation
invokestatic: call static methods

2.4 控制流分析​

控制流分析(Control Flow Analysis)通常指的是構建控制流圖(Control Flow Graph, CFG),並以CFG 作為基礎結構進行靜態分析的過程。

2.4.1 控制流图​

通常採用控制流圖進行控制流分析的表示
控制流圖可以用來表示控制流的結構
控制流圖的節點可以是單個的三地址碼,不過通常是Basic Block(BB),如下所示。
202202150948606.png-water_print

2.4.2 Basic Block​

Basic Block 是具有以下属性的、最大長度的三地址碼組成的塊單元:
只能從塊的第一條指令進入。
只能從塊的最後一條指令離開。
如何構建基本塊:
P 的第一條指令就是一個基本塊的leader
跳轉的目標指令是一個基本塊的leader
跳轉指令的後一條指令,也是一個基本塊的leader
一個基本塊就是一個基本塊的leader 及其後續直到下一個基本塊的leader 前的所有指令。

2.4.3 边的生成​

除了基本塊,CFG 中還會有塊到塊的邊。塊A 和塊B 之間有一條邊,當且僅當:
A 的末尾有一條指向了B 開頭的跳轉指令。
A 的末尾緊接著B 的開頭,且A 的末尾不是一條無條件跳轉指令。
202202151002339.png-water_print

注意到每個基本塊和開頭指令的標號唯一對應,因此很自然地,需要將跳轉指令的目標改為基本塊的標號而非指令標號:
202202151004659.png-water_print

有了這些定義,我們就可以了解一些概念:
若A - B,則我們說A 是B 的前驅(predecessor),B 是A 的後繼(successor)
除了構建好的基本塊,我們還會額外添加兩個結點,「入口(Entry)」和「出口(Exit)」
這兩個結點不對應任何IR
入口有一條邊指向IR 中的第一條指令
如果一個基本塊的最後一條指令會讓程序離開這段IR,那麼這個基本塊就會有一條邊指向出口。
這樣就完成了一個控制流圖的構建。

3 数据流分析​

3.1 相关概念​

近似方案1:忽略掉程序的條件判斷,認為所有的分析都有可能到達
程序可以看成是**狀態(數據)狀態之間的轉移(控制)**兩部分,因為狀態轉移的條件都被忽略了,核心分析的部分是狀態數據在轉移過程中的變化,所以叫做數據流分析。
近似方案2:不在路徑末尾做合併,在控制流匯合的所有位置提前做合併
數據流分析要保證Safe-approximation,即:
may analysis: over-approximation
must analysis: under-approximation
總之,不同的數據流分析有不同的數據抽象方法(應用不同),有不同的Safe-approximation 策略,有不同的轉移函數與控制流匯聚方式。

3.1.1 输入输出状态​

每一條IR 的執行,都會使狀態從输入状态變成新的输出状态輸入/輸出狀態與語句前/後的program point相關聯。
202202151027498.png-water_print

在數據流分析中,我們會把每一個program point 關聯一個數據流值,代表在該點中可觀察到的抽象的程序狀態。數據流分析的目的是為所有語句的輸入輸出找到一組,基於控制流和轉換函數的,安全的近似約束的解決方案。

3.1.2 转移函数​

分析數據流有前向和後向兩種:
202202151033803.png-water_print

3.1.2 控制流约束​

每條語句s 都會使程序狀態發生改變。 B 的輸出自然是其輸入在經過多次轉換後得到的狀態。而B 的輸入要根據數據流分析的需求,對其前驅應用合適的meet operator 進行處理。後向分析時亦然。
202202151037521.png-water_print

3.2 应用​

3.2.1 到达定值分析:Reaching Definitions Analysis​

基本概念​

假定x 有定值d (definition),如果存在一個路徑,從緊隨d 的點到達某點p,並且此路徑上面沒有x 的其他定值點,則稱x 的定值d 到達(reaching) p。
如果在這條路徑上有對x 的其它定值,我們說變量x 的這個定值d 被殺死(killed) 了
202202151042510.png-water_print

到達定值可以用來分析未定義的變量。例如,我們在程序入口為各變量引入一個dummy 定值。當程序出口的某變量定值依然為dummy,則我們可以認為該變量未被定義。
對於一條賦值語句D: v=x op y,該語句生成了v 的一個定值D,並殺死程序中其它對變量v 定義的定值。

理解到达定值分析​

數據流值:
程序中所有變量的定值。
可以用一個bit vector 來定義,有多少個賦值語句,就有多少個位。
202202151051269.png-water_print

D 即Definition,可以表示為:D:v=x op y
一個D 可以視為一行三地址碼,做了兩件事:
生成了一個定義D
在保持其他傳入程序不受影響的同時,kill 了程序中其它對變量v 的定義
轉移方程與控制流:
202202151053712.png-water_print

其中U 表示union,結合前文使用bit 字節表示的D,可以知道,IN 的輸入等於OUT[P1] 並OUT[P2],意味著只要存在一條路徑可以reach,那麼就算作可以reach。
202202151056283.png-water_print

达定值分析的算法表示​

202202151100805.png-water_print

這是一個經典的迭代算法:
首先讓所有BB 和入口的OUT 為空。因為你不知道BB 中有哪些定值被生成。
當任意OUT 發生變化,則分析出的定值可能需要繼續往下流動,所需要修改各BB 的IN 和OUT。
先處理IN,然後再根據轉移完成更新OUT
 
返回
上方