개요
Tree API에서 주로 사용하는 클래스 및 메서드에는 여러 가지가 있다.
기본적으로 ClassNode, MethodNode, InsnList가 중요한 역할을 하지만, 이 외에도 여러 유용한 클래스와 인터페이스가 있다. 여기에는 다음과 같은 것들이 포함된다.
- ClassNode : 클래스 노드. 트리 노드의 최상단 부모라고 생각하자.
- MethodNode : 클래스의 메서드를 나타낸다. 메서드의 접근 지정자, 이름, 설명 등을 포함할 수 있다.
- FieldNode: 클래스의 필드를 나타낸다. 필드의 접근 지정자, 이름, 설명, 초기값 등을 포함할 수 있다.
- AnnotationNode: 어노테이션을 나타낸다. 어노테이션의 설명자와 값들을 포함한다.
- VarInsnNode: 메서드의 지역 변수를 나타낸다. 변수의 이름, 설명자, 시작 및 종료 레이블, 인덱스 등의 정보를 포함한다.
이들은 ASM의 Tree API를 사용할 때 매우 중요한 구성 요소들이며, 클래스나 메서드를 분석하고 수정하는데 필수적인 역할을 한다.
예제 및 설명
MethodNode, InsnList 적용
MethodNode는 하나의 메서드 블록을 의미하고, InsnList는 지시 사항(즉, 코드 자체)의 List를 의미한다.
다음 구문을 통해 메서드와 지시 사항을 적용하는 코드를 살펴보자.
이해가 안 된다면 아래 포스팅을 참조할 것
https://csg1353.tistory.com/184
//기본 생성자 생성 후 적용
MethodNode mn = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null); //메서드 노드
InsnList il = new InsnList(); //지시사항
il.add(new VarInsnNode(ALOAD, 0)); //생성자 스택 적재
il.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)); //init 호출
il.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
il.add(new LdcInsnNode("생성자 생성됨."));
il.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false));
il.add(new InsnNode(RETURN));
mn.instructions = il; //메서드 노드에 지시사항 추가
cn.methods.add(mn); //클래스에 메서드 노드 추가
1. mn 인스턴스는 메서드 생성자를 의미하는 메서드 블록이다.
이것을 고수준으로 추상화하면 public init(클래스이름) () {} 을 의미한다. 이는 생성자를 뜻한다.
2. il.add(new VarInsnNode(ALOAD, 0));
자기 자신(this)의 정보를 stack frame에 적재한다.
이 구문은 생성자의 첫 번째 지시사항으로, 'this' 참조(자기 자신의 객체 참조)를 스택에 푸시한다.
모든 인스턴스 메서드(생성자 포함) 호출 시, JVM은 'this' 참조를 메서드의 첫 번째 파라미터로 전달한다.
3. il.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)); //init 호출
2번에 적재한 operand 스택을 소모하여, 자기 자신의 init(생성자)을 호출한다.
이 부분은 자바에서 super() 호출과 동일하다. (일반적으로 생략하긴 하지만, 모든 생성자는 super를 통해 부모 클래스를 호출한다.)
여기서 INVOKESPECIAL은 특수 메서드 호출(생성자나 private 메서드)를 나타내며, java/lang/Object의 생성자를 호출한다. 즉, 현재 객체의 상위 클래스(Object 클래스) 생성자를 호출합니다.
4. 다음 세 줄은 출력 구문(System.out.println)을 의미한다.
il.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
il.add(new LdcInsnNode("생성자 생성됨."));
il.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false));
5. il.add(new InsnNode(RETURN));
return 구문을 통해 il 지시문의 마지막 리턴 값을 호출한다.
6. mn.instructions = il;
이러한 지시사항의 List 타입을 Method 블록인 MethodNode에 적용한다.
7.cn.methods.add(mn);
마지막으로 이 메서드 블록을 ClassNode cn에 추가한다.
최종 적용 코드는 다음과 같다.
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
public class test1 {
public test1() {
System.out.println("생성자 생성됨.");
}
}
FieldNode 를 추가하여 적용
Class 내부의 필드 파라미터는 다음과 같이 적용할 수 있다.
이전 로직에 포함해서 적용해보자.
cn.version = V21;
cn.access = ACC_PUBLIC;
cn.name = className; //패키지 + 클래스명
cn.superName = "java/lang/Object"; //객체 타입
// cn.interfaces.add(""); //implements
//기본 생성자 생성 후 적용
MethodNode mn = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null); //메서드 노드
InsnList il = new InsnList(); //지시사항
il.add(new VarInsnNode(ALOAD, 0)); //생성자 스택 적재
il.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)); //init 호출
il.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
il.add(new LdcInsnNode("생성자 생성됨."));
il.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false));
il.add(new InsnNode(RETURN));
mn.instructions = il; //메서드 노드에 지시사항 추가
cn.methods.add(mn); //클래스에 메서드 노드 추가
//필드값
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
"LESS", "I", null, -1));
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
"EQUAL", "I", null, 0));
cn.fields.add(new FieldNode(ACC_PUBLIC + ACC_FINAL + ACC_STATIC,
"GREATER", "I", null, 1));
cn.methods.add(new MethodNode(ACC_PUBLIC + ACC_ABSTRACT,
"compareTo", "(Ljava/lang/Object;)I", null, null));
최종 결과는 다음과 같다.
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
public class test1 {
public static final int LESS = -1;
public static final int EQUAL = 0;
public static final int GREATER = 1;
public test1() {
System.out.println("생성자 생성됨.");
}
public abstract int compareTo(Object var1);
}
지역 변수 추가
public class BasicClassLocalValue {
static private ClassNode cn = new ClassNode();
public static ClassNode getClassNode(String className) {
cn.version = V21;
cn.access = ACC_PUBLIC;
cn.name = className; //패키지 + 클래스명
cn.superName = "java/lang/Object"; //객체 타입
// cn.interfaces.add(""); //implements
//기본 생성자 생성 후 적용
MethodNode mn = new MethodNode(ACC_PUBLIC, "<init>", "()V", null, null); //메서드 노드
InsnList il = new InsnList(); //지시사항
il.add(new VarInsnNode(ALOAD, 0)); //생성자 스택 적재
il.add(new MethodInsnNode(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)); //init 호출
il.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));
il.add(new LdcInsnNode("생성자 생성됨. BasicClassLocalValue"));
il.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false));
il.add(new InsnNode(RETURN));
mn.instructions = il; //메서드 노드에 지시사항 추가
cn.methods.add(mn); //클래스에 메서드 노드 추가
//특정 메서드의 지역 변수 넣기,
//Static 정적 변수는 this 지역변수 0번 인덱스를 가지지 않는다. 이를 유의
MethodNode mnLocalValue = new MethodNode(ACC_PUBLIC + ACC_STATIC, "addTwoNumbers", "()I", null, null);
il = new InsnList();
// 지역 변수 'a'에 5 할당
il.add(new LdcInsnNode(5));
il.add(new VarInsnNode(ISTORE, 1));
// 지역 변수 'b'에 10 할당
il.add(new LdcInsnNode(10));
il.add(new VarInsnNode(ISTORE, 2));
// 'a'와 'b' 불러와 더하기
il.add(new VarInsnNode(ILOAD, 1));
il.add(new VarInsnNode(ILOAD, 2));
il.add(new InsnNode(IADD));
// 결과 반환
il.add(new InsnNode(IRETURN));
mnLocalValue.instructions = il; //메서드 지시사항 추가
cn.methods.add(mnLocalValue); //메서드 추가
return cn;
}
}
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
public class testLocalClass {
public testLocalClass() {
System.out.println("생성자 생성됨. BasicClassLocalValue");
}
public static int addTwoNumbers() {
byte var1 = 5;
byte var2 = 10;
return var1 + var2;
}
}
클래스 변조의 경우
클래스 변조의 경우 ClassNode 내부 remove 메서드를 적용한다.
기본 개요는 다음과 같다.
premain에서 ClassFileTransformer 구현체를 통해 모든 클래스 빌드 시 바이트코드를 호출할 것이다.
@Slf4j
public class TreeAPITransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) {
String containsName = "BasicExample"; //스캔할 클래스명
if (className.contains(containsName)) {
log.warn("[TRANSFORM] Find ClassName : {}", containsName);
byte[] modifiedCode = init(classfileBuffer); //here to modify
CodePrinter.printClass(modifiedCode, containsName); //print
return modifiedCode;
} else {
return classfileBuffer;
}
}
//ByteCode -> ClassReader가 읽음 -> ClassNode accept ->
//ClassNode 변조(제거) -> ClassWriter accept -> 최종 결과물을 다시 바이트코드로 변환
public byte[] init(byte[] bytes) {
ClassReader classReader = new ClassReader(bytes);
ClassNode cn = new ClassNode();
classReader.accept(cn, 0); //바이트코드 제공
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
cn = BasicRemoveMethod.removeMethod(cn, "main", "()V"); //<< ClassNode testcode 적용점
cn = BasicRemoveField.removeField(cn, "staticVal"); //<< ClassNode testcode 적용점
cn.accept(cw); //변조된 classNode 바이트코드 제공
return cw.toByteArray();
}
}
메서드와 필드 제거
public class BasicRemoveMethod {
public static ClassNode removeMethod(ClassNode cn, String methodName, String methodDesc) {
cn.methods.removeIf(methodNode -> methodNode.name.equals(methodName) && methodNode.desc.equals(methodDesc));
return cn;
}
}
public class BasicRemoveField {
public static ClassNode removeField(ClassNode cn, String fieldName) {
cn.fields.removeIf(fieldNode -> fieldNode.name.equals(fieldName));
return cn;
}
}
removeIf를 사용하여 각 노드의 하위 필드를 순회하며 동일 조건일 경우 제거하게 된다.
세부 로직(라이브러리 코드)는 다음과 같다.
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
메서드와 필드 변조
변조의 경우는 제거와 유사하다.
기존의 부분을 제거하고, 제거된 부분을 다시 채워넣으면 될 것이다.
public void modifyMethodInstructions(MethodNode mn) {
InsnList il = mn.instructions;
// il에 명령어 추가, 제거 또는 변경 작업을 수행
}
'기술스택 > ASM - [Bytecode]' 카테고리의 다른 글
[A Java bytecode engineering library] - [Core API] 2. Classes[2/2] (0) | 2024.04.02 |
---|---|
[A Java bytecode engineering library] - [Core API] 2. Classes[1/2] (0) | 2024.04.02 |
[A Java bytecode engineering library] 1. Introduction (0) | 2024.04.02 |
[A Java bytecode engineering library] - [Tree API] 7. Method (0) | 2024.04.02 |
ASM 학습 정리 (0) | 2024.02.24 |