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
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); $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
