軟件分析筆記:3.過程分析(Interprocedural Analysis)


本文是針對南京大學《軟件分析》譚添老師的視頻課的課堂筆記。

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.總結

過程間分析相較於過程內分析的精度更高,因此在實際項目中,我們應該更傾向於使用過程間分析。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM