本文是針對南京大學《軟件分析》譚添老師的視頻課的課堂筆記。
1.Motivation
此前我們討論的問題都是過程內的,也就是不涉及到方法調用。然而實際程序中方法調用屢見不鮮,繼續采用之前的分析方法會丟失精度,這也就是為什么我們需要過程(間)分析。二者的區別如下:
過程內分析Intra-procedural Analysis
- 只考慮過程內部語句,不考慮過程調用
- 目前的所有分析都是過程內的
過程間分析Inter-procedural Analysis
- 考慮過程調用的分析
- 有時又稱為全程序分析Whole Program Analysis
- Call edges和Return edges
- 需要call graph
2.Call Graph(調用圖)
2.1調用圖的概念以及簡單示例
本質上可以看做是一個調用邊的集合,每個調用邊從調用點連接到目標方法(target methods或者callees),簡單例子如下:
void foo(){
int n = ten();
addOne(42)
}
int ten(){
return 10;
}
int addOne(int x){
int y = x + 1;
return y;
}
上面代碼所對應的調用圖如下所示:
該程序有三個方法(foo()、ten()、addOne()),調用圖就是由調用點引出的箭頭指向被調用的方法。
2.2調用圖的應用
- 理論上所有過程間(跨函數)分析的基礎
- 程序的優化
- 程序理解
- 程序debugging
- 程序測試
2.3針對面向對象語言的調用圖構造(以Java為例)
2.3.1代表性算法
- Class hierarchy analysis(CHA)
- Rapid type analysis(RTA)
- Variable type analysis(VTA)
- Pointer analysis(k-CFA)
以上四個算法的排列是有規律的,從上到下越往下精度越高(more precise),越往上速度越快(more efficient)。我們將重點學習第一個和最后一個算法。
2.3.2預備知識
Java中的調用
Java調用主要分為三大類,如下圖所示:
invokestatic調用的目標方法是static methods,就是靜態方法。所以它是沒有reciever object,目標個數只有一個,在編譯期可以確定。
后兩種調用的都是instance(實例)方法:
invokespecial調用的方法有構造函數、私有的實例方法以及父類的實例方法,它的目標個數也是只有一個,在編譯期可以確定。
invokeinterface和invokevirtual調用其他的方法,因為有多態的存在,所以可能調用不同的方法,因此目標方法可能大於一個,具體調用的方法要在運行時才能確定。
因為前兩類相對來說比較簡單,所以我們過程間分析的關鍵是對於第三種Virtual call的分析。
Virtual call中有個關鍵步驟,叫Method Dispatch。因為Virtual call調用的具體方法是要在程序運行時才能得到,在這一過程中涉及到兩個要素:
- reciever object的具體類型:c
- 方法的簽名(method signature):m,一個signature可以充當一個方法的identifier.即通過一個signature可以唯一確定一個方法。
- Signature = class type(方法具體所在的類) + method name(方法名) + descriptor
- Descriptor = return type + parameter types
- 可以參考soot工具中采取的格式,如下圖所示,紅色的是class type,藍色的是method name,綠色的就是desciptor
縮寫為
求這個方法的過程,我們叫做Method Dispatch。
2.3.3Method Dispatch of Virtual Calls
我們定義了一個函數Dispatch(c,m)以模擬動態Method Dispatch的過程。參數c和m是上面定義里的兩個要素(已加粗)。具體過程如下圖所示:
如果非抽象方法(因為dispatch要找的是一個具體的能被調用的方法,所以必須是非抽象)里包含一個和m有着相同名字和descriptor的m’,那么就直接返回m’,我們就認為dispatch找到了目標函數。如果c中沒有滿足條件的方法,那么我們就去c的父類里面找,重復這個過程直到找到為止。
下面是一個利用Method Dispatch的例子:
如圖所示,第一個Dispatch是先在B里面找foo方法,然后發現找不到,所以就去B的父類A里面找,找到了,所以第一個Dispatch的結果就是A中的foo方法。第二個Dispatch是先在C里面去找foo方法,在這里因為C自己就有foo方法,所以第一步我們就返回了C中的foo方法。
2.3.4Class Hierarchy Analysis*(CHA)
該方法需要程序中類繼承(也就是名字里的Class hierarchy)的信息,也就是需要知道每個類的父類和子類。核心思想就是根據每一個Virtual call的receiver variable的declared type(聲明類型)來解目標方法。舉例說就是
對於上圖a這個變量的declared type就是A,那么CHA就會根據A的方式去算。具體思想就是假設a可以指向A以及它所有子類的對象,因此CHA的實現過程就是查詢A的繼承結構,從A和子類繼承結構去找目標方法。
CHA的具體實現過程
我們定義了一個方法Resolve(cs)以利用CHA算法找到可能的對應call site的目標方法。算法偽代碼如下:
首先初始化一個空集合T以裝call site的目標方法,然后我們取出調用點cs的簽名,接下來對cs的類型進行判定:
-
如果cs是靜態調用,那么T就等於對應類中的方法。
-
-
如果cs是special call,在預備知識中我們知道special call有三種情況(構造函數、私有方法或者父類方法),以父類方法為例:
-
如圖所示,因為C繼承了B類,所以C中的父方法就是B中的方法,我們應該先出去方法名的class type,在例子中也就是B類,接下來對cm和m調用Dispatch(因為B中可能並沒有foo方法,我們可能還要從B的父類中去找,所以在這里調用了Dispatch()),因為Dispatch返回的目標方法是唯一的,這也就解答了之前為什么說special call目標個數也是唯一的原因。
- 如果cs是virtual call,如下圖所示:
我們會先找出c的聲明類型,也就是C,對C本身和C所有子類以及子類的子類等等(在這里我們定義它為c‘)都調用一個Dispatch(c’,m)並將返回值添加到集合T中,最后返回集合T。
一個CHA應用實例
如圖所示,c的聲明類型是C,而C沒有子類,所以Resolve(c.foo()) = {C.foo()}。同理可得Resolve(a.foo()) = {A.foo(),C.foo(),D.foo()},Resolve(b.foo()) = {A.foo(),C.foo(),D.foo()}(這里要注意因為B中沒有foo方法,所以要到B的父類A中去找)。
這里也暴露了CHA算法的問題,那就是如果將“B b = ..."替換成”B b = new B()",Resolve(b.foo())還是會得出同樣的結果,而事實上C.foo()和D.foo()都是錯誤的結果。那是因為CHA只考慮聲明類型,也就是B,這樣就會導致精度的下降。
CHA的特征
優點:快,只需要考慮聲明類型,忽略所有數據流和控制流。
缺點:精度差,因為忽略的太多了。
CHA最常用的場景就是IDE中,如下圖所示:
2.3.5利用CHA構造調用圖
簡單步驟:
- 從程序的入口方法開始(如Java里的main方法)構造調用圖
- 在構造過程中可以通過調用邊達到一些新的方法,每遇到一個可達方法,對他們用CHA的Resolve方法找到目標方法,以此往復,知道找到所有的可達方法,最終得到調用圖。
具體算法實現
如上圖所示,第一行對算法進行了初始化,先是將入口方法添加到Work List里,然后將CG和RM兩個集合清空。整個算法是一個大的while循環,我們不斷從WL中取出方法m並將它添加到RM中(代表此方法已經被分析)並對m中的調用點進行分析,利用CHA找到對應的目標方法和調用邊,將目標方法添加到WL中,並將它和調用邊組成調用圖。
使用實例
3.過程間的控制流圖(ICFG)
3.1與CFG區別
- CFG表示的是單個方法的結構
- ICFG表示的是整個程序的結構
- 我們可以用ICFG進行過程間分析
- 一個ICFG是由程序中各個方法自己的CFG再加上兩種額外的邊(Call edges和Return edges)組成的
- Call edges連結調用點和目標方法的入口
- Return edges從一個return語句連回到緊跟着調用點下面的語句
- Return site一般緊跟着Call site。
一個理解ICFG的例子:
如圖所示,三個方法對應了三個CFG,將這三個CFG用Call edges和Return edges連結到一起。
4.過程間的數據流分析
4.1原理
實際上就是對有method call的程序,基於該程序的ICFG對數據流進行分析。
如圖所示,相較於過程內的數據流分析,過程間 的數據流分析的轉換函數多了一個edge transfer的部分(包含Call edge transfer和Return edge transfer),這也跟前面說的ICFG相較於CFG多的那兩種邊相對應。
4.2過程間的常量傳播數據流分析
對於常量傳播來說:
- Call edge transfer:就是用來傳參的
- Return edge transfer:就是用來傳返回值的
- Node transfer:和過程內大致一樣,對每一個方法調用節點,將等號左邊部分去掉。
實例分析
這部分很好理解,唯一需要注意的是圖上兩條黃色背景的邊,這兩條邊並不是可有可無的,上面說到的node transfer提過對於每一個方法調用節點將等號左邊部分去掉,也就是說從“b=addOne(a)"語句到"c=b-3"語句我們是將a的值傳遞了過去,而b的值由addOne()傳遞,如果去掉這條邊的話就意味着a的值只能通過addOne()傳播,而addOne()中對a並沒有更改,這樣會額外消耗系統資源,另外在第二個黃色邊中,如果不去除掉b的值,那么最后一個節點得到的b就為NAC,出現錯誤,所以我們才會要求去掉等號左邊的元素。
5.總結
過程間分析相較於過程內分析的精度更高,因此在實際項目中,我們應該更傾向於使用過程間分析。