SootUp簡介
一、簡介
在本文中,我們將研究SootUp 函式庫。 SootUp 是一個使用原始原始碼或編譯的 JVM 字節碼對 JVM 程式碼執行靜態分析的函式庫。它是對Soot 庫的徹底修改,旨在變得更加模組化、可測試、可維護和可用。
2. 依賴關係
在使用 SootUp 之前,我們需要在建置中包含最新版本,即撰寫本文時的1.3.0 。
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.core</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.java.core</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.java.sourcecode</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.java.bytecode</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>org.soot-oss</groupId>
<artifactId>sootup.jimple.parser</artifactId>
<version>1.3.0</version>
</dependency>
我們這裡有幾個不同的依賴項,那麼它們都有什麼作用呢?
-
org.soot-uss:sootup.core
是核心函式庫。 -
org.soot-uss:sootup.java.core
是使用 Java 的核心模組。 -
org.soot-uss:sootup.java.sourcecode
是分析Java原始碼的模組。 -
org.soot-uss:sootup.java.bytecode
是用來分析編譯後的 Java 字節碼的模組。 -
org.soot-uss:sootup.jimple.parser
是用來解析Jimple 的模組 – SootUp 用來表示 Java 的中間表示形式。
不幸的是,沒有可用的 BOM 依賴項,因此我們需要單獨管理這些依賴項的每個版本。
3.Jimple是什麼?
SootUp 可以分析多種不同格式的程式碼,包括 Java 原始碼、編譯的位元組程式碼,甚至是 JVM 本身內部的類別。
為此,它將各種輸入轉換為稱為 Jimple 的中間表示形式。
Jimple 的存在是為了表示可以用 Java 原始碼或位元組程式碼完成的所有事情,但以一種更容易執行分析的方式。這意味著它在某些方面故意與這兩種可能的輸入不同。
JVM 字節碼在存取某些值的方式上是基於堆疊的。這對於運行時來說非常高效,但對於分析目的來說卻困難得多。程式碼的 Jimple 表示將其轉換為完全基於變數。這可以產生完全相同的功能,同時更容易理解。
相反,Java原始碼也是基於變數的,但其嵌套結構也使得分析更加困難。這對開發人員來說更容易使用,但對於軟體工具來說更難分析。 Jimple 表示將其轉換為平面結構。
Jimple 也以一種我們可以自己閱讀和編寫程式碼的語言而存在。例如Java原始碼:
public void demoMethod() {
System.out.println("Inside method.");
}
可以寫成 Jimple,如下圖:
public void demoMethod() {
java.io.PrintStream $stack1;
target.exercise1.DemoClass this;
this := @this: target.exercise1.DemoClass;
$stack1 = <java.lang.System: java.io.PrintStream out>;
virtualinvoke $stack1.<java.io.PrintStream: void println(java.lang.String)>("Inside method.");
return;
}
這更加冗長,但我們可以看到它具有相同的功能。如果我們需要以這種格式儲存和轉換該 Jimple 程式碼,SootUp 提供了直接解析和產生該 Jimple 程式碼的功能。
當我們分析程式碼時,無論原始來源是什麼,它都會被轉換成這種結構供我們使用。然後,我們將使用與此表示直接相關的類型,例如SootClass
、 SootField
、 SootMethod
等。
4. 分析程式碼
在使用 SootUp 執行任何操作之前,我們需要分析一些程式碼。這是透過建立AnalysisInputLocation
的適當實例並圍繞它建立JavaView
來完成的。
我們創建的AnalysisInputLocation
的確切類型取決於我們要分析的程式碼的來源。
最簡單但可能最沒用的方法是能夠分析 JVM 本身中的類別。我們可以使用JrtFileSystemAnalysisInputLocation
類別來做到這一點:
AnalysisInputLocation inputLocation = new JrtFileSystemAnalysisInputLocation();
更有用的是,我們可以使用OTFCompileAnalysisInputLocation
分析原始檔:
AnalysisInputLocation inputLocation = new OTFCompileAnalysisInputLocation(
Path.of("src/test/java/com/baeldung/sootup/AnalyzeUnitTest.java"));
這還有一個替代構造函數,用於一次性分析整個原始檔列表:
AnalysisInputLocation inputLocation = new OTFCompileAnalysisInputLocation(List.of(.....));
我們也可以使用它來分析記憶體中作為字串的源代碼:
Path javaFile = Path.of("src/test/java/com/baeldung/sootup/AnalyzeUnitTest.java");
String javaContents = Files.readString(javaFile);
AnalysisInputLocation inputLocation = new OTFCompileAnalysisInputLocation("AnalyzeUnitTest.java", javaContents);
最後,我們可以分析已經編譯的字節碼。這是使用JavaClassPathAnalysisInputLocation
完成的,我們可以將其指向任何可以被視為類別路徑的東西 - 包括 JAR 檔案或包含類別檔案的目錄。
AnalysisInputLocation inputLocation = new JavaClassPathAnalysisInputLocation("target/classes");
還有其他幾種標準方法來存取我們想要分析的程式碼,包括直接解析 Jimple 表示,或讀取 Android APK 檔案。
一旦我們獲得了AnalysisInputLocation
實例,我們就可以圍繞它創建一個JavaView
:
JavaView view = new JavaView(inputLocation);
然後,這允許我們存取輸入中存在的所有類型。
5. 訪問類
一旦我們分析了程式碼並圍繞它建立了JavaView
實例,我們就可以開始存取有關程式碼的詳細資訊。這從訪問類別開始。
如果我們知道我們想要的確切類,我們可以使用完全限定的類別名稱直接存取它。 SootUp 使用各種Signature
類別來描述我們想要存取的元素。在這種情況下,我們需要一個ClassType
實例。幸運的是,我們可以透過使用 SootUp 為我們提供的IdentifierFactory
來使用完全限定的類別名稱輕鬆生成其中之一:
IdentifierFactory identifierFactory = view.getIdentifierFactory();
ClassType javaClass = identifierFactory.getClassType("com.baeldung.sootup.ClassUnitTest");
一旦我們建立了ClassType
實例,我們就可以使用它來存取此類的詳細資訊:
Optional<JavaSootClass> sootClass = view.getClass(javaClass);
這會傳回一個Optional<JavaSootClass>
因為該類別可能不存在於我們的視圖中。或者,我們有一個getClassOrThrow()
方法,它將直接傳回SootClass
– JavaSootClass
的超類別 – 但如果該類別在我們的JavaView
中不可用,則會拋出例外:
SootClass sootClass = view.getClassOrThrow(javaClass);
一旦我們獲得了SootClass
實例,我們就可以使用它來檢查類別的詳細資訊。這讓我們可以確定類別本身的細節,例如它的可見性、它是具體的還是抽象的,等等:
assertTrue(classUnitTest.isPublic());
assertTrue(classUnitTest.isConcrete());
assertFalse(classUnitTest.isFinal());
assertFalse(classUnitTest.isEnum());
我們也可以導航我們解析的程式碼,例如,透過存取我們類別的超類別或介面:
Optional<? extends ClassType> superclass = sootClass.getSuperclass();
Set<? extends ClassType> interfaces = sootClass.getInterfaces();
請注意,它們傳回ClassType
而不是SootClass
實例。這是因為不能保證實際的類別定義是我們視圖的一部分,而只是它們的名稱。
6. 存取字段和方法
除了類別本身之外,我們還可以存取類別的內容,例如欄位和方法。
如果我們已經有了一個可用的SootClass
,那麼我們可以直接查詢它來尋找欄位和方法:
Set<? extends SootField> fields = sootClass.getFields();
Set<? extends SootMethod> methods = sootClass.getMethods();
與我們從一個類別導航到另一個類別時不同,這可以安全地傳回欄位或方法的完整表示,因為它們保證在我們的視圖中。
如果我們確切地知道我們在追求什麼,我們也可以直接進入它。例如,要存取一個字段,我們只需要知道它的名稱:
Optional<? extends SootField> field = sootClass.getField("aField");
存取方法稍微複雜一些,因為我們需要知道方法名稱和參數類型:
Optional<? extends SootMethod> method = sootClass.getMethod("someMethod", List.of());
如果我們的方法採用參數,那麼我們需要從IdentifierFactory
提供Type
實例的清單:
Optional<? extends SootMethod> method = sootClass.getMethod("anotherMethod",
List.of(identifierFactory.getClassType("java.lang.String")));
這使我們能夠在重載方法時獲得正確的實例。我們也可以列出所有具有相同名稱的重載方法:
Set<? extends SootMethod> method = sootClass.getMethodsByName("someMethod");
和以前一樣,一旦我們獲得了SootMethod
或SootField
實例,我們就可以使用它來檢查詳細資訊:
assertTrue(sootMethod.isPrivate());
assertFalse(sootMethod.isStatic());
7. 分析方法體
一旦我們得到了SootMethod
實例,我們就可以用它來分析方法體本身。這意味著方法簽名、方法中的局部變數以及呼叫圖本身。
在我們執行任何操作之前,我們需要存取方法主體本身:
Body methodBody = sootMethod.getBody();
使用它,我們現在可以存取方法主體的所有詳細資訊。
7.1.訪問局部變數
我們可以做的第一件事是存取方法中可用的任何局部變數:
Set<Local> methodLocals = methodBody.getLocals();
這使我們能夠存取該方法中可存取的每個變數。該列表可能不是預期的,它實際上是方法的 Jimple 表示中的變數列表,因此將包括解析過程中的一些附加條目,並且可能沒有原始變數名稱。
例如,以下方法有 5 個局部變數:
private void someMethod(String name) {
var capitals = name.toUpperCase();
System.out.println("Hello, " + capitals);
}
這些都是:
-
this.
-
I1
– 方法參數。 -
I2 –
變數「大寫字母」。 -
$stack3
– 指向System.out
局部變數。 -
$stack4
– 代表“Hello, ” + capitals
局部變數。
$stack3
和$stack4
局部變數是由 Jimple 表示產生的,並非直接出現在原始程式碼中。
7.2.訪問方法語句圖
除了局部變數之外,我們還可以分析整個方法語句圖。這是該方法將執行的每個語句的詳細資訊:
StmtGraph<?> stmtGraph = methodBody.getStmtGraph();
List<Stmt> stmts = stmtGraph.getStmts();
這為我們提供了該方法將執行的每個語句的列表,並按照執行順序排列。其中每一個都將實作Stmt
接口,代表該方法可以執行的操作。
例如,我們先前的方法將產生以下結果:
這看起來比我們實際編寫的程式碼要多得多——它只有兩行長。這是因為這是我們代碼的 Jimple 表示形式。但我們可以將其分解,看看究竟發生了什麼。
我們從兩個JIdentityStmt
實例開始。這些代表傳遞到我們方法中的值 - this
值和我們之前看到的作為第一個參數的I1
。
接下來,我們有三個JAssignStmt
實例。這些代表對我們方法中變數的賦值。在本例中,我們將I1.toUpperCase()
的結果指派給I2
,將System.out
值指派給$stack3
,並將“Hello, ” + I2
的結果指派給$stack4
。
之後,我們就有了一個JInvokeStmt
實例。這表示在$stack3
上呼叫println()
方法,並向其傳遞$stack4
的值。
最後,我們有一個JReturnVoidStmt
實例,它表示方法結束時的隱式回傳。
這是一個非常簡單的方法,沒有分支或控制語句,但我們可以清楚地看到方法所做的一切都在這裡表示。對於我們在 Java 應用程式中可以實現的任何事情也是如此。
八、總結
這是 SootUp 的快速介紹。我們可以用這個庫做更多的事情。下次當您需要分析任何 Java 程式碼時,為什麼不嘗試呢?
與往常一樣,本文中的所有範例都可以在 GitHub 上找到。