Soot生成控制流圖


Soot是McGill大學的Sable研究小組自1996年開始開發的Java字節碼分析工具,它提供了多種字節碼分析和變換功能,通過它可以進行過程內和過程間的分析優化,以及程序流圖的生成,還能通過圖形化的方式輸出,讓用戶對程序有個直觀的了解。尤其是做單元測試的時候,可以很方便的通過這個生成控制流圖然后進行測試用例的覆蓋,顯著提高效率。

 

如果是將Soot當作簡單工具來分析的人,可以直接使用Soot自帶的工具soot.tools.CFGViewer分析類中的每個方法的控制流並生成DOT語言描述的控制流圖,然后用graphviz中的dot命令來轉換成可視化圖形格式如.PNG

使用soot.tools.CFGViewer來生成Triangle.class的控制流圖

進入項目bin目錄下,拷貝soot.trunk.jar

1 java -cp soot-trunk.jar soot.tools.CFGViewer --soot-classpath .;"%JAVA_HOME%"\jre\lib\rt.jar TriangleClass.Triangle

為什么這里是TriangleClass.Triangle,是因為Triangle類是在TriangleClass包下的,所以類名需要加上完整包名

Triangle.java源代碼如下:

 1 package TriangleClass;
 2 
 3 public class Triangle {
 4     
 5     
 6     public String triangle(int a, int b, int c){
 7         
 8         if(a > 0 && b > 0 && c >0){
 9             if(a + b >c)
10             {
11                 if(a == b || b ==c || a ==c)
12                 {
13                     if(a == b && b == c)
14                     {
15                         return "equilateral";
16                     }
17                     return "isosceles";
18                 }
19                 else{
20                     return "scalene";
21                 }
22             }
23             else{
24                 return "Not Triangle";
25             }
26         }
27         else{
28             return "Not Triangle";
29         }
30         
31         
32         
33     }
34     
35     public int a;
36     public int b;
37     public int c;
38 
39 }

其中一個文件已經生成

重命名后(文件名太長了。。),使用graphviz dot轉換為圖片PNG格式

1 dot -Tpng -o Triangle.png Triangle.dot  

再生成Test.class的控制流圖:

Test.java

 1 package Soot;
 2 
 3 public class Test {
 4     
 5     private double num = 5.0;
 6     
 7     public double cal(int num, String type){
 8         double temp=0;
 9         if(type == "sum")
10         {
11             for(int i = 0; i <= num; i++){
12                 temp =temp + i;
13             }
14         }
15         else if(type == "average")
16         {
17             for(int i = 0; i <= num; i++){
18                 temp = temp + i;
19             }
20             temp = temp / (num -1);
21         }else{
22             System.out.println("Please enter the right type(sum or average)");
23         }
24         return temp;
25     }
26 }

 

但是作為程序員,怎么能滿足於簡單的使用!

那么如何使用代碼來在程序內部實現這個從分析代碼到輸出的過程呢?以下為Soot的深入理解內部代碼實現生成控制流圖:

Soot的輸入時多源的,可以是java的字節碼。Soot提供了四個中間表示法,通過將源文件轉換為中間表示,基於這些中間表示傳入不同的變換類來進行分析,優化或者再變換,另外還直接提供一組直接用於優化Java字節碼的API。Soot的擴展機制以Pack為中心,一個Pack包括若干個變換用戶可以自行設計新的變換,將其加入到soot的調度執行過程中以實現特定的功能,如輸出為dot文件格式。

四種中間表示法是:Baf,Grimp,Jimple和Shimple

Baf - 基於棧的bytecode

傳統的JVM bytebode是基於棧操作的指令集(Dalvik 基於寄存器操作),與之對應的Baf同樣如此。那Baf抽象了什么呢?兩個,忽略了constant pool(常量池)和bytecode指令中的type依賴。在bytecode中對不同保留類型,如int和float,的同一操作(如add),有不同的指令。這是因為在計算機中整形和浮點型的表達方式是不一樣的,在底層實現時無法讓兩個操作符分屬於這兩種不同類型,也就是需要不同的指令對應不同的數據類型的操作。我們做分析時不用在意它到底調用的什么類型的指令,不對int還是float做細致區分,只要知道它是個數且知道是對這數的什么樣的操作就行了。Baf因此用於在bytecode層面上的分析。
::使用這種基於棧的中間表示法為的是簡化一定要在棧代碼上執行的分析和變換的開發。
 
Jimple - 緊湊、無棧、類型化的三地址代碼中間表示法
Jimple是Soot的核心,是四種IR中最重要的。Soot能直接創建Jimple碼,也可由Java sourcecode或者bytecode轉化翻譯而來。bytecode會被翻譯成untyped Jimple,再通過type inference 方法對局部變量加上類型。翻譯的重要一步是對表達式作線性化使得每個statement只能最多refernce 3個局部變量或者常量(沒懂。。)。相對於bytecode的200多種指令,Jimple只有15條,分別對應着核心指令的 NopStmt, IdentityStmt, AssignStmt;函數內控制流指令的IfStmt, GotoStt, TableSwitchStmt和LookUpSwitchStmt,函數間控制流的InvoeStmt, ReturnStmt, ReturnVoidStmt, 監視器指令EnterMonitorStmt和ExitMonitorStmt,最后處理異常ThrowStmt和退出的RetStmt。
::結構清晰,更適用於程序分析和代碼優化
 
Grimp -- Jimple聚合版
可以構造樹形結構,更接近於java源代碼,適用於反匯編並便於閱讀
 
Shimple -- Static Single Assignment 版的Jimple
 
給出java源代碼,和不同表示形式的比較:
java源碼:
1 public class Foo {
2 public static void main(String[] args) { Foo f = new Foo();
3 int a = 7;
4 int b = 14;
5 int x = (f.bar(21) + a) * b; }
6 public int bar(int n) { return n + 42; } }

Jimple中間表示:

public static void main(java.lang.String[]) { java.lang.String[] r0;
Foo $r1, r2;
int i0, i1, i2, $i3, $i4;
r0 := @parameter0: java.lang.String[]; $r1 = new Foo;
specialinvoke $r1.<Foo: void <init>()>(); r2 = $r1;
i0 = 7;
i1 = 14;
// InvokeStmt
$i3 = virtualinvoke r2.<Foo: int bar()>(21);&nbsp;
$i4 = $i3 + i0;
i2 = $i4 * i1;
return;
}
public int bar() { Foo r0;
int i0, $i1;
r0 := @this: Foo; // IdentityStmt
i0 := @parameter0: int; // IdentityStmt $i1 = i0 + 21; // AssignStmt
return \$i1; // ReturnStmt
}

Grimp中間表示:

1 public static void main(java.lang.String[]) { java.lang.String[] r0;
2 Foo r2;
3 int i0, i1, i2;
4 r0 := @parameter0: java.lang.String[]; r2 = new Foo();
5 i0 = 7;
6 i1 = 14;
7 i2 = (r2.<Foo: int bar(int)>(21) + i0) * i1;
8 return; }

Soot讀入Java字節碼,然后通過Baf將字節碼轉換為可分析的Jimple,然后再進入分析變換過程,接着可以再轉換為Grimp形式或者Baf形式來輸出或者再轉換為java字節碼輸出優化的Java字節碼,這就是Soot在數據層面抽象的處理過程。

Soot的執行過程被分成了好幾大步,每一大步被稱為一個pack。第一步是把輸入的bytecode (.class)或者.java 文件或者.jimple 翻譯成Jimple code。再把生成的Jimple作為剩下packs的輸入。"函數中分析(intra-procedure analysis)"執行流程示意如下

如圖所示,在soot里每一個圓圈算一個階段,每個階段都有對應的Pack(p)實現,Pack是一組變換器,當Pack被調用時,它按照順序執行每一個變換器。提供拓展機制的是那些允許用戶做自定義變換(Transformation)的Pack:jtp(Jimple Transformation Pack),stp(Shimple Transformation Pack),在不改變soot自身的情況下,用戶可以滿足自身需求的類(變換器),然后將其注入到這些Pack中,之后調用soot.Main,使其進入soot的調度過程中。

Soot變換器通常是繼承了BodyTransformer或SceneTransformer的類的實例。BodyTransformer針對單個方法體進行變換,SceneTransformer針對整個應用進行變換。在這兩種情況下,變換器類都必須重構internalTransform方法,對要分析的代碼執行某種變換。

如下是簡單的格式:

 1 import java.util.Iterator;
 2 import java.util.Map;
 3 import soot.*;
 4 
 5 class Example {
 6 
 7   public void run(String dir){
 8     
 9     Printer printer = new Printer();
10     //生成變換器
11     Transform t1 = new Transform("jtp.Printer", printer);
12     //加入到Pack中
13     PackManager.v().getPack("jtp").add(t1);
14     
15     int size;
16     String[] soot_args = new String[size];
17     /*指定參數選項調用main*/
18     soot.Main.main(soot_args);
19   }
20 
21   public static void main(String[] args){
22     Example example = new Example();
23     example.run("要處理的類路徑");
24   }
25 
26   class Printer extends BodyTransformer {
27 
28     @Override
29     protected void internalTransform(Body body, String string, Map map) {
30       /*添加自己的變換*/
31       }
32     }
33   }
34 }

 這里interTransform是最重要的處理類,通過流程我們會知道在這個階段會傳過來JimpleBody或ShimpleBody,即internalTransform的參數Body body, String string是階段名稱,map是調用soot傳遞的參數選項。

 在Soot中一個Body隸屬於一個SootMethod, 即soot用一個Body為一個方法存儲代碼。我們輸出上面Triangle.class的body

 1 public java.lang.String triangle(int, int, int)
 2     {
 3         TriangleClass.Triangle r0;
 4         int i0, i1, i2, $i3;
 5 
 6         r0 := @this: TriangleClass.Triangle;
 7 
 8         i0 := @parameter0: int;
 9 
10         i1 := @parameter1: int;
11 
12         i2 := @parameter2: int;
13 
14         if i0 <= 0 goto label5;
15 
16         if i1 <= 0 goto label5;
17 
18         if i2 <= 0 goto label5;
19 
20         $i3 = i0 + i1;
21 
22         if $i3 <= i2 goto label4;
23 
24         if i0 == i1 goto label1;
25 
26         if i1 == i2 goto label1;
27 
28         if i0 != i2 goto label3;
29 
30      label1:
31         if i0 != i1 goto label2;
32 
33         if i1 != i2 goto label2;
34 
35         return "equilateral";
36 
37      label2:
38         return "isosceles";
39 
40      label3:
41         return "scalene";
42 
43      label4:
44         return "Not Triangle";
45 
46      label5:
47         return "Not Triangle";
48     }

  有了Body的信息,我們需要進行控制流分析,將Body轉換為控制流圖。控制流圖除了表示這個節點(每一個節點表示一個基本塊)之外,還會存儲這個節點的前驅和后繼,形式上可以表示為:

  前驅

  

  后繼

  

  Soot提供兩種控制流圖UnitGraph和BlockGraph,都實現DirectedGraph接口,這兩個都是抽象類,soot提供了若干實例化的類:CompleteUnitGraph,BriefUnitGraph,都在soot.toolkits.graph中使用這些就可以將Body轉換為控制流圖CFG。

  通過閱讀源代碼可知,CFGGraphType可以通過字符串生成相應類型的控制流圖,這樣有利於參數化程序。

  CFGGraphType.java相應代碼

  

  

  這里通過CFGGraphType類來讀取參數信息,生成相應的控制流圖類型,

protected void internalTransform(Body b, String phaseName, Map<String, String> options) {
         initialize(options);
         CFGGraphType graphtype;
         graphtype = CFGGraphType.getGraphType(“BriefUnitGraph”);
         DirectedGraph<Unit> graph = graphtype.buildGraph(body);
                 /*后續操作*/
         
     }        

  我們可以看看Triangle.class生成的BriefUnitGraph類型控制流圖:可看出每個statement有preds前驅集合和succs后繼集合

// preds: []
r0 := @this: TriangleClass.Triangle
// succs [i0 := @parameter0: int]
// preds: [r0 := @this: TriangleClass.Triangle]
i0 := @parameter0: int
// succs [i1 := @parameter1: int]
// preds: [i0 := @parameter0: int]
i1 := @parameter1: int
// succs [i2 := @parameter2: int]
// preds: [i1 := @parameter1: int]
i2 := @parameter2: int
// succs [if i0 <= 0 goto return "Not Triangle"]
// preds: [i2 := @parameter2: int]
if i0 <= 0 goto return "Not Triangle"
// succs [if i1 <= 0 goto return "Not Triangle", return "Not Triangle"]
// preds: [if i0 <= 0 goto return "Not Triangle"]
if i1 <= 0 goto return "Not Triangle"
// succs [if i2 <= 0 goto return "Not Triangle", return "Not Triangle"]
// preds: [if i1 <= 0 goto return "Not Triangle"]
if i2 <= 0 goto return "Not Triangle"
// succs [$i3 = i0 + i1, return "Not Triangle"]
// preds: [if i2 <= 0 goto return "Not Triangle"]
$i3 = i0 + i1
// succs [if $i3 <= i2 goto return "Not Triangle"]
// preds: [$i3 = i0 + i1]
if $i3 <= i2 goto return "Not Triangle"
// succs [if i0 == i1 goto (branch), return "Not Triangle"]
// preds: [if $i3 <= i2 goto return "Not Triangle"]
if i0 == i1 goto (branch)
// succs [if i1 == i2 goto (branch), if i0 != i1 goto return "isosceles"]
// preds: [if i0 == i1 goto (branch)]
if i1 == i2 goto (branch)
// succs [if i0 != i2 goto return "scalene", if i0 != i1 goto return "isosceles"]
// preds: [if i1 == i2 goto (branch)]
if i0 != i2 goto return "scalene"
// succs [if i0 != i1 goto return "isosceles", return "scalene"]
// preds: [if i0 == i1 goto (branch), if i1 == i2 goto (branch), if i0 != i2 goto return "scalene"]
if i0 != i1 goto return "isosceles"
// succs [if i1 != i2 goto return "isosceles", return "isosceles"]
// preds: [if i0 != i1 goto return "isosceles"]
if i1 != i2 goto return "isosceles"
// succs [return "equilateral", return "isosceles"]
// preds: [if i1 != i2 goto return "isosceles"]
return "equilateral"
// succs []
// preds: [if i0 != i1 goto return "isosceles", if i1 != i2 goto return "isosceles"]
return "isosceles"
// succs []
// preds: [if i0 != i2 goto return "scalene"]
return "scalene"
// succs []
// preds: [if $i3 <= i2 goto return "Not Triangle"]
return "Not Triangle"
// succs []
// preds: [if i0 <= 0 goto return "Not Triangle", if i1 <= 0 goto return "Not Triangle", if i2 <= 0 goto return "Not Triangle"]
return "Not Triangle"
// succs []

  生成了控制流還沒有結束,我們需要將它輸出為圖形,如dot類型文件,所幸soot帶了將CFG轉化成dot的類:CFGToDotGraph和DotGraph,所以我們可以將之前的結果作為輸入,生成dot文件,代碼為:

protected void internalTransform(Body b, String phaseName, Map<String, String> options) {
   
         CFGGraphType graphtype;
         graphtype = CFGGraphType.getGraphType(“BriefUnitGraph”);
         DirectedGraph<Unit> graph = graphtype.buildGraph(b);
         /*后續操作*/
         CFGToDotGraph drawer= new CFGToDotGraph();
         /*設定參數*/
         drawer.setBriefLabels(PhaseOptions.getBoolean(options, briefLabelOptionName));
            drawer.setOnePage(!PhaseOptions.getBoolean(options, multipageOptionName));
    drawer.setUnexceptionalControlFlowAttr("color", "black");
    drawer.setExceptionalControlFlowAttr("color", "red");
    drawer.setExceptionEdgeAttr("color", "lightgray");
    drawer.setShowExceptions(Options.v().show_exception_dests());
        /*生成dotGraph*/
        DotGraph canvas = graphtype.drawGraph(drawer, graph, b);
        /*輸出*/
        String methodname = body.getMethod().getSubSignature();
    String classname = body.getMethod().getDeclaringClass().getName().replaceAll("\\$", "\\.");
    String filename = soot.SourceLocator.v().getOutputDir();
        if (filename.length() > 0) {
            filename = filename + java.io.File.separator;
        }
        filename = filename + classname + " " + methodname.replace(java.io.File.separatorChar, '.') + DotGraph.DOT_EXTENSION;

    G.v().out.println("Generate dot file in " + filename);
    canvas.plot(filename);
         
     } 

  之后只用設定好soot_args就可

  

  完整代碼見Github:

  

https://github.com/clownice/GenerateControlFlow


免責聲明!

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



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