目的
Javaメソッドのコールグラフを静的解析フレームワークの Sootup を用い可視化してみます。
準備
- macos
- maven
- gradle
- jdk23
Sootupライブラリのビルド
Sootupはmavenリポジトリに登録されていない様なのでGitHubからソースコードを取得しビルドすることにします。ビルドはpom.xmlファイルが用意されているため、mavenを用いビルドしていきます。
$ brew install openjdk
$ brew install maven
$ brew install gradle
$ uname -r
24.4.0
$ javac --version
javac 23.0.2
$ mvn --version | grep "Apache Maven"
Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937)
$ gradle --version | grep Gradle
Gradle 8.14
mvnコマンドを使いビルドを行いローカルリポジトリにインストールを実行。
$ git clone https://github.com/soot-oss/SootUp.git
$ cd SootUp
$ git checkout tags/v2.0.0
$ mvn install
分析用のサンプルコード作成
ソースコード保存用のディレクトリを作成。
$ mkdir -p src/main/java/com/example
コールグラフ分析用に利用するコードを作成。
package com.example;
public class Main {
public static void main(String[] args) {
System.out.println("Hello SootUp!");
Main app = new App();
app.callA();
}
public void callA(){
callB();
callC();
}
public void callB() {
callC();
}
public void callC() {
}
}
サンプルコードをビルドするためのbuild.gradleファイルを作成します。
plugins {
id 'application'
}
サンプルコードが問題なくビルドできることを確認します。
$ gradle build
BUILD SUCCESSFUL in 673ms
5 actionable tasks: 4 executed, 1 up-to-date
分析用コードの作成
package com.example;
import java.util.Collections;
import sootup.callgraph.CallGraph;
import sootup.callgraph.ClassHierarchyAnalysisAlgorithm;
import sootup.core.inputlocation.AnalysisInputLocation;
import sootup.core.signatures.MethodSignature;
import sootup.core.types.ClassType;
import sootup.java.bytecode.frontend.inputlocation.JavaClassPathAnalysisInputLocation;
import sootup.java.core.views.JavaView;
public class CallGraphApp {
public static void main(String[] args) {
String classpath = "build/classes/java/main";
AnalysisInputLocation inputLocation = new JavaClassPathAnalysisInputLocation(classpath);
JavaView view = new JavaView(Collections.singletonList(inputLocation));
ClassType classType = view.getIdentifierFactory().getClassType("com.example.Main");
MethodSignature entryMethodSignature = view.getIdentifierFactory().getMethodSignature(
classType, "main", "void", Collections.singletonList("java.lang.String[]")
);
ClassHierarchyAnalysisAlgorithm algorithm = new ClassHierarchyAnalysisAlgorithm(view);
CallGraph callGraph = algorithm.initialize(Collections.singletonList(entryMethodSignature));
callGraph.callsFrom(entryMethodSignature).stream().forEach(call -> {
System.out.println(call);
});
}
}
当初、Main クラスの main メソッドから呼び出されるすべてのメソッドが、呼び出し順に連続して出力されることを期待していました。しかし実際には、main メソッド内で直接呼び出されているメソッドコールの情報のみが出力され、それ以降のネストされた呼び出し(メソッドチェーンなど)は含まれていませんでした。
$ gradle run
> Task :run
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
Call:<com.example.Main: void main(java.lang.String[])> -> <java.io.PrintStream: void println(java.lang.String)> via virtualinvoke $stack2.<java.io.PrintStream: void println(java.lang.String)>("Hello SootUp!");
Call:<com.example.Main: void main(java.lang.String[])> -> <com.example.Main: void <init>()> via specialinvoke $stack3.<com.example.Main: void <init>()>();
Call:<com.example.Main: void main(java.lang.String[])> -> <com.example.Main: void callA()> via virtualinvoke $stack3.<com.example.Main: void callA()>();
BUILD SUCCESSFUL in 655ms
2 actionable tasks: 1 executed, 1 up-to-date
先ほどのコードにより、1つのクラスの1つのメソッドから呼び出されるメソッドの情報を取得できることが確認できました。そこで次は、Main クラス内に定義されているすべてのメソッドについて、それぞれの中で呼び出されているメソッドを一覧化してみようと思います。結果的に Main クラス内に閉じた形でのメソッドチェーンではありますが、コールグラフを作成するための基礎的な情報が得られそうなので実装してみます。
package com.example;
import java.util.Collections;
import sootup.callgraph.CallGraph;
import sootup.callgraph.ClassHierarchyAnalysisAlgorithm;
import sootup.core.inputlocation.AnalysisInputLocation;
import sootup.core.signatures.MethodSignature;
import sootup.core.types.ClassType;
import sootup.java.bytecode.frontend.inputlocation.JavaClassPathAnalysisInputLocation;
import sootup.java.core.JavaSootClass;
import sootup.java.core.JavaSootMethod;
import sootup.java.core.views.JavaView;
public class CallGraphApp {
public static void main(String[] args) {
String classpath = "build/classes/java/main";
AnalysisInputLocation inputLocation = new JavaClassPathAnalysisInputLocation(classpath);
JavaView view = new JavaView(Collections.singletonList(inputLocation));
ClassType classType = view.getIdentifierFactory().getClassType("com.example.Main");
JavaSootClass sootClass = view.getClass(classType);
for (JavaSootMethod sootMethod : sootClass.getMethods()) {
MethodSignature entryMethodSignature = sootMethod.getSignature();
ClassHierarchyAnalysisAlgorithm algorithm = new ClassHierarchyAnalysisAlgorithm(view);
CallGraph callGraph = algorithm.initialize(Collections.singletonList(entryMethodSignature));
callGraph.callsFrom(entryMethodSignature).stream().forEach(call -> {
System.out.println(call);
});
}
}
}
$ gradle run
> Task :run
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
Call:<com.example.Main: void callB()> -> <com.example.Main: void callC()> via virtualinvoke this.<com.example.Main: void callC()>();
Call:<com.example.Main: void callA()> -> <com.example.Main: void callB()> via virtualinvoke this.<com.example.Main: void callB()>();
Call:<com.example.Main: void callA()> -> <com.example.Main: void callC()> via virtualinvoke this.<com.example.Main: void callC()>();
Call:<com.example.Main: void <init>()> -> <java.lang.Object: void <init>()> via specialinvoke this.<java.lang.Object: void <init>()>();
Call:<com.example.Main: void main(java.lang.String[])> -> <java.io.PrintStream: void println(java.lang.String)> via virtualinvoke $stack2.<java.io.PrintStream: void println(java.lang.String)>("Hello SootUp!");
Call:<com.example.Main: void main(java.lang.String[])> -> <com.example.Main: void <init>()> via specialinvoke $stack3.<com.example.Main: void <init>()>();
Call:<com.example.Main: void main(java.lang.String[])> -> <com.example.Main: void callA()> via virtualinvoke $stack3.<com.example.Main: void callA()>();
BUILD SUCCESSFUL in 669ms
2 actionable tasks: 1 executed, 1 up-to-date
このままの出力だと見にくいので整形してみます。コンストラクタの呼び出しを含めすべてのメソッドコールが出力されている様に見えます。この情報元にコールを繋ぎ合わせることでチェインしたコールグラフを計算できそうです。
Main.callB() -> Main.callC()
Main.callA() -> Main.callB()
Main.callA() -> Main.callC()
Main.<init>() -> Object.<init>()
Main.main(String[]) -> PrintStream.println(String)
Main.main(String[]) -> Main.<init>()
Main.main(String[]) -> Main.callA()
CallGraph.Call オブジェクトの一覧をリストとして保持し、それを使って呼び出し元から呼び出し先を再帰的にたどることで、コールグラフを出力しするメソッドを作成します。
public static void printCallGraph(List<CallGraph.Call> list) {
var graph = new HashMap<String, List<String>>();
list.forEach(caller ->
graph.computeIfAbsent(caller.getSourceMethodSignature().toString(), k -> new ArrayList())
.add(caller.getTargetMethodSignature().toString())
);
graph.keySet().forEach(caller -> printCaller(graph, caller, 0));
}
public static void printCaller(Map<String, List<String>> graph, String caller, int depth) {
System.out.println(" ".repeat(depth * 4) + caller);
graph.getOrDefault(caller, List.of()).forEach(callee ->
printCaller(graph, callee, depth + 1)
);
}
実行結果は以下の通りです。
<com.example.Main: void main(java.lang.String[])>
<java.io.PrintStream: void println(java.lang.String)>
<com.example.Main: void <init>()>
<java.lang.Object: void <init>()>
<com.example.Main: void callA()>
<com.example.Main: void callB()>
<com.example.Main: void callC()>
<com.example.Main: void callC()>
<com.example.Main: void callB()>
<com.example.Main: void callC()>
<com.example.Main: void <init>()>
<java.lang.Object: void <init>()>
<com.example.Main: void callA()>
<com.example.Main: void callB()>
<com.example.Main: void callC()>
<com.example.Main: void callC()>
終わりに
当初の目的としては実現できたと思います。コールグラフを作成するために用意したサンプルコードも意図的に単純な処理だけを用いました。今回のコードではインターフェースを用いた呼び出しは計算できていないので、追加の処理やさらなる分析コードをの追加が必要だと思います。