Introduction to SootUp
1. 소개
이 글에서는 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-oss:sootup.core는 핵심 라이브러리입니다.
- org.soot-oss:sootup.java.core는 Java와 작업하기 위한 핵심 모듈입니다.
- org.soot-oss:sootup.java.sourcecode는 Java 소스 코드를 분석하기 위한 모듈입니다.
- org.soot-oss:sootup.java.bytecode는 컴파일된 Java 바이트코드를 분석하기 위한 모듈입니다.
- org.soot-oss: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;
}
이것은 훨씬 더 자세하지만 동일한 기능을 갖추고 있음을 확인할 수 있습니다. 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
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();
이들은 SootClass 인스턴스 대신에 ClassType을 반환합니다. 이는 실제 클래스 정의가 우리 뷰의 일부일 것이라는 보장이 없기 때문입니다.
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 – “capitals”라는 변수.
-
$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 애플리케이션에서 우리가 성취할 수 있는 모든 것에도 해당되는 사실입니다.
8. 요약
이것은 SootUp에 대한 간략한 소개였습니다. 이 라이브러리로 수행할 수 있는 것이 훨씬 더 많이 있습니다. 다음에 Java 코드를 분석할 필요가 있을 때, 왜 시도해보지 않겠습니까?
모든 예제는 GitHub에서 확인할 수 있습니다.