【軟件分析學習筆記】4:中間表示(Intermediate Representation)

程序源碼直接拿來給靜態分析器做靜態分析是不合適的,像編譯一樣也需要在程序的中間表示(IR)上進行分析,這樣能讓靜態分析算法比較簡潔、高效。IR也沒有絕對的標準,這節課學習的是絕大多數靜態分析器採取的IR。

1 編譯的基本流程

1.1 詞法分析

詞法分析(Lexical Analysis)會去檢查是否輸入的源代碼是若干合法單詞的組合。詞法分析器統稱Scanner,運行時需要關鍵字的詞典,以及用正則表達式描述的詞法規則。

詞法分析會爲每個合法的單詞生成Token

1.2 語法分析

語法分析(Syntax Analysis)會去檢查這些Token的組合形式是否符合語法規則。語法分析器統稱Parser,相應的描述語法規則的方式是上下文無關文法(Context-Free Grammer)。Context-Free Grammer的表達能力比Context-Sensitive Grammer弱,但是對幾乎所有的編程語言而言,這樣的表達能力就足夠了,如果去使用後者反倒會更慢。

語法分析會生成抽象語法樹(AST)

1.3 語義分析

語義分析(Semantic Analysis)會去檢查語義是否合理。和自然語言的語義不同,對編譯器而言,只具備檢查簡單的語義的功能,如類型檢查器(Type Checker),相應的進行類型檢查時描述規則的方法是Attribute Grammer

語義分析會生成Decorate AST

1.4 轉換

如果編譯器有編譯優化的功能,那麼就要通過轉換器(Translator) 轉換成中間表示(IR) 的形式,這裏的中間表示IR通常指的都是三地址碼形式(3-Address Form)

生成IR之前的部分是編譯器前端,生成IR之後的部分是編譯器後端。編譯優化就是靜態分析的一種應用,靜態分析是在IR的基礎上做的,所以是屬於編譯器後端。這也說明了,要做靜態分析,就不得不走完編譯器前端的部分才能拿到IR,接着再在IR的基礎上做分析。

1.5 生成

爲了能執行,要將上一步的中間表示,通過代碼生成器(Code Generator) 生成機器碼(Machine Code)

2 AST和三地址碼的比較

如對於一段簡單do-while循環的程序:

do {
	i = i + 1;
} while(a[i] < v);

2.1 AST

表示成AST是這樣的:

  • 比較高層,很貼合編程語言的文法結構
  • 通常依賴於具體的編程語言(因爲和文法結構貼合)
  • 很適合快速進行類型檢查
  • 控制流信息很隱晦

2.2 三地址碼形式的IR

表示成三地址碼形式的IR是這樣的:

1: i = i + 1
2: t1 = a[i]
3: if t1 < v goto 1
  • 比較低層,更貼合彙編語言和機器碼
  • 通常和具體的編程語言是無關的
  • 緊湊、簡潔、格式統一
  • 能自然地看到控制流信息(如上面的goto 1
  • 通常作爲靜態分析器IR的正統格式

3 三地址碼介紹

3.1 簡述

三地址碼(3-Address Code)也可簡稱3AC,其指令右側至多有一個運算符,如:

t2 = a + b + 3

可以轉換成:

t1 = a + b
t2 = t1 + 3

向三地址碼的轉換通常會引入臨時變量,這裏t1就是臨時變量。


之所以叫三地址碼,是因爲它的每一條指令最多包含三個地址(Address),這裏的地址不是通常意義上的地址,而是下面中的一種:

  1. 變量,如ab
  2. 常量,如3
  3. 編譯器或靜態分析器自動生成的臨時變量,如t1t2

3.2 三地址碼的理論形式

以下xyz都是地址,即變量、常量或臨時變量。

  1. x = y bop z,這裏bop是二元運算符或邏輯操作符
  2. x = uop y,這裏uop是一元操作符,如符、取反、類型轉換
  3. x = y
  4. goto L,即無條件跳轉
  5. if x goto L,即條件跳轉
  6. if x rop y goto L,其中rop是關係運算符

這些是理論上的3AC的形式,具體到實現時要更復雜。

4 Soot的三地址碼IR——Jimple

Soot是爲Java服務的最有名的靜態分析器,Jimple是它所使用的中間表示,也是一種特殊的三地址碼的形式,下面是幾個Java源程序到Jimple三地址碼的例子。

4.1 for循環

注意,下面源程序中的x因爲最後也沒用到,被Soot在得到AST時候就優化去掉了,得到的Jimple裏不再有這個變量。
在這裏插入圖片描述
在形參表裏面給出了方法參數的完整類型名,抹去了形參的名字。

第一行指令聲明字符串類型的r0,接着聲明整形的i1(以代替程序for循環中的i)。

後面r0 := @parameter0: java.lang.String[];表示變量r0定義爲該方法的第一個參數,其類型是java.lang.String[]。接下來爲i1賦值爲0

然後在label1中把這個for循環的行爲用三地址碼描述了(注意i1 = i1 + 1對應的是程序裏的i++而不是x = x + 1),如果i1達到10了就跳到label2然後直接返回。

4.2 do-while循環

在這個例子裏args對應r0,數組arr對應r1,變量i對應i1(因爲在while條件中被使用所以不能將其消除),而arr[i]對應臨時變量$i0也即r1[i1]
在這裏插入圖片描述
注意在這個do-while例子的三地址碼中可以看到label1下總是先執行i1 = i1 + 1,這和do-while的語義是一致的。

4.3 方法調用(方法內部)

例子中這個方法是一個成員方法,這裏三地址碼中的r0被定義爲this的指向,即例子中承載這個函數的MethodCall3AC這個類的對象。
在這裏插入圖片描述
函數的兩個參數para1para2分別對應r1r2。爲了完成函數中三個字符串拼接的操作,創建了一個臨時的StringBuilder$r3,然後調用append操作將變量r1拼進來得到$r4,再調用append操作將常量" "拼進來得到$r5,再調用append操作將變量r2拼進來得到$r6,最後用toString()將其轉換成String對象$r7,並將其返回。


在上面的Jimple代碼中還出現了specialinvokevirtualinvoke,這是JVM裏四種主要方法調用中的兩種,這四種命令是:

  • invokespecial:用於調用構造方法、父類方法、私有方法
  • invokevirtual:用於調用普通的成員方法,進行virtual dispatch
  • invokeinterface:用於調用繼承的接口的方法,不能做優化,需要檢查是否實現了接口中的方法
  • invokestatic:用於調用靜態方法

在Java7之後還引入了invokedynamic,用來更方便的實現動態類型的語言在JVM上運行。


在上面的Jimple代碼中方法調用(invoke)的地方,尖括號<>之間的內容是方法簽名(Method Signature),它一般會包含方法所在的類名、方法的返回值類型、形參列表中各個參數的類型,有些還會包含方法名(比如上面的例子裏append就是方法名)。

4.4 方法調用(調用方)

這裏還是4.3例子的程序,但是是方法的調用方(即主函數部分)的Jimple。
在這裏插入圖片描述
可以看到同樣是r0聲明爲主函數形參args,然後$r3是對象mc,先用specialinvoke調用構造方法把這個對象構造出來,然後用virtualinvoke調用了方法foo,把兩個字符串參數傳了進去。

注意,因爲源程序中調用foo的返回變量result沒有用到所以被Soot優化消除了,對應的Jimple裏也就沒有將調用的返回值保存到變量裏了。

4.5 類

這裏類代碼的Jimple不僅要將類名寫完整,還要將繼承的類顯式寫出來,以保全語義信息。
在這裏插入圖片描述
源程序中沒有顯式給出構造函數,Jimple中的<init>是默認生成的構造函數,然後$r0指向this,再用specialinvoke調用其父類(這裏是Object,見方法簽名)的構造函數。

接下來靜態的main方法源程序中是空的,但也要將形參定義以下,這裏r0對應args

接下來靜態的<clinit>方法是類的靜態的初始化方法,當類被初次加載到內存裏時,就是通過調用這個<clinit>方法來將所有的靜態屬性初始化。例子中就是將pi初始化爲3.14這個值,至於pi的聲明在最上方。

5 靜態單賦值(Static Single Assignment)

5.1 簡述

這是一種經典的IR格式,SSA和普通3AC的區別在於它給每一個定義都使用了新的名稱:

左側3SA中第1/3/4行的p在右側SSA中使用了不同的名稱p1/p2/p3,而第2/5行的兩個q也被改成了不同的名稱q1/q2,在賦值右側則用前面出現的名稱進行對應,不會改變整個程序的語義。

5.2 phi-function

這樣使用了不同名稱之後,爲了保證每個變量還是有唯一的定義而不會產生歧義,在控制流匯合的地方使用ϕ\phi函數進行聚合,例如:

圖中ϕ(x0,x1)\phi(x_0, x_1)當程序走左邊時就取x0x_0,走右邊時就取x1x_1,它有專門的分析算法。

5.3 優缺點

  • 它可以將flow-sensitive的一條條指令變成flow-insensitive的,也就是對指令的順序不再敏感,這樣flow-insensitive的分析可以提高分析的速度,同時較好保持flow-sensitive的精度,因爲它本身就帶有了一些flow的信息
  • 能更精確地找到其定義的地方,因爲每條都是新的定義
  • 引入了太多的變量,如果有太多分叉還會引入很多phi-function
  • 轉換到機器碼的過程會有很多低效的地方

6 控制流圖的構建

三地址碼最終是在控制流圖(Control Flow Graph) 上進行分析,這裏學習如何從3AC轉換到CFG
在這裏插入圖片描述
在CFG中若干條指令一起組成Basic Block,作爲圖上結點存在。

6.1 基本塊(Basic Block)

基本塊是滿足下列性質的,最大的連續指令的有序集合。

  1. 只能有一個入口,爲其第一條指令,不存在從另一個位置進入BB的控制流
  2. 只能由一個出口,爲其最後一條指令,不存在從BB中間跳出去的情況

特別注意,滿足這兩條性質的Block,只要不是最大的連續指令集,就不能成爲BB,所以實際程序中每條指令最終所在的BB總是情況唯一的。

6.2 BB的構建思路

  1. 如果一條指令是程序中某個跳轉的目標,那麼它一定是一個BB的入口,否則會違反規則1。

  2. 如果一條指令包含跳轉操作,那麼它一定是一個BB的出口,否則會違反規則2。

這裏老師講的是,如果一條指令緊跟着一條含有跳轉操作的指令,那麼它一定是一個BB的入口,和這個的意思是一樣的,因爲一條指令是BB出口,它的下一條指令一定是下一個BB的入口。

6.3 BB的構建算法

先找整個程序中BB開始的指令,標記爲Leader:

  1. 程序第一條指令
  2. 跳轉的目標指令
  3. 帶跳轉的指令的下一條指令

接着從每個Leader開始往下,直到下一個Leader或者程序結尾之前,合起來是一個BB。

例如:

圖中標紅的就是找出的Leader。

6.4 完整CFG的構建

接下來就是在前面構造好的BB之間添加邊,很直觀:

  1. 兩個BB順次相連,加邊
  2. 從A無條件跳轉B,加邊
  3. 從A條件跳轉到B,加邊

注意,這裏規則1有例外,如果前一個BB的最後一條指令是無條件的goto,那麼不要按照規則1加邊。

在構建邊的同時,將指令中,goto的目標從指令標號變換成BB的名字:
在這裏插入圖片描述
最終添加EntryExit兩個特殊結點,即得到這小節最開始圖上的CFG。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章