2008년 3월 7일 금요일

ASM - 자바 바이트코드 분석하기

전부터 자바소스코드를 읽어들여 분석할 수 있는 방법이 없을까 하는 생각을 많이 했었다.

그러려면 거의 자바 컴파일러 수준이 되어야 할 것 같아서 엄두를 못 내다가 얼마전에 자바 바이트코드를 다룰 수 있는 라이브러리가 있다는 사실을 알게 되었다. 대표적인 것이 아파치의 BCEL과 지금 살펴 보려고 하는 ASM이다.

근데 이런 게 있다는 걸 알긴 했는데 도무지 사람들이 관심이 없어서인지 쓸만한 참고자료 찾기가 너무 힘들었다.

결국 ASM 사이트에서 이것저것 파일을 다운받아서 보다보니 내가 딱 원하는 기능의 예제까지 찾아낼 수가 있었다.

 

일단, BCEL(Byte Code Engineering Library)과 ASM은 자바소스파일을 컴파일해서 얻은 클래스파일(바이트코드)을 읽어 들여 변경하거나 분석하는 데 사용하는 라이브러리다. 자바소스파일 같은 경우는 코드상에 문제가 있을 수 있으니까 이걸 분석하는 건 문제가 있을 가능성이 있지만, 클래스파일은 자바소스코드상에 오류가 없어야 컴파일을 통해 만들 수 있는 것이므로 클래스파일을 분석하는 것이 보다 신뢰성 있는 분석결과를 얻을 수 있지 않겠는가? (나는 아직 클래스파일을 동적으로 변경하거나 이런 건 별로 관심이 없다 보니...)

여기저기 자료를 찾다 보니 BCEL보다 ASM이 속도가 빠르다고 해서 나는 ASM을 써 보기로 결정했다.

 

1. 라이브러리 준비

http://asm.objectweb.org/download/index.html에 가서 ASM 관련 파일을 다운받는다.

여기서는 asm관련 모든 패키지가 들어 있는 asm-all-3.1.jar를 사용하는데, 그러려면 asm-3.1-bin.zip을 다운받아서 압축을 풀면 그 안의 lib/all에 이 파일이 있다.

이 파일 외에도 다른 가이드나 예제 같은 것이 많으니 필요에 따라 함께 다운받으면 된다.

 

2. 분석프로그램 생성

※ 이 클래스가 컴파일되려면 컴파일할 때 클래스패스에 1에서 받은 asm-all-3.1.jar를 등록시켜야 한다.


[code java] import java.util.List; import org.objectweb.asm.ClassReader; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.MethodNode; public class ClassInfo implements Opcodes { public static void main(final String[] args) throws Exception { ClassReader cr = new ClassReader("test.Test"); ClassNode cn = new ClassNode(); cr.accept(cn, ClassReader.SKIP_CODE); System.out.println("Class Name : " + cn.name + "\n"); System.out.println("Super Class : " + cn.superName + "\n"); System.out.println("Interfaces :"); List interfaces = cn.interfaces; for (int i = 0; i < interfaces.size(); i++) { System.out.println(interfaces.get(i)); } System.out.println("\nMethods :"); List methods = cn.methods; for (int i = 0; i < methods.size(); ++i) { MethodNode method = (MethodNode) methods.get(i); System.out.println(method.name + method.desc); } } } [/code]

ClassInfo.java


 

코드설명 :

9: 분석하려는 클래스의 패캐지를 포함한 full-name을 인자로 ClassReader 객체를 생성한다.

    여기서 읽기 위해서는 분석하려는 클래스가 클래스패스 경로 안에 반드시 있어야 한다.

12: 아마 클래스파일을 파싱하는 작업이 아닐까 생각이 된다.(아직 잘 몰라서 그만...)

14: 클래스 이름 출력

16: 클래스가 상속한 클래스 출력

18~22: 클래스가 구현한 인터페이스 리스트 출력

24~29: 클래스 내의 메소드 정보 출력

 

3. 분석 대상 클래스를 생성 및 컴파일

위의 ClassInfo 클래스가 test.Test라는 클래스를 분석하려고 하는데 그러려면 아래와 같이 미리 Test를 만들어서 컴파일해서 클래스파일이 위의 클래스가 실행하는 클래스패스 안에 들어가 있어야 한다.

[code java] package test; import java.io.Serializable; import java.util.HashMap; public class Test extends HashMap implements Serializable { public static String str; static { str = "abced"; } public Test() { super(); } public static void main(String[] args) throws Exception { String str = "abc.dfd.efef.fjskdl"; System.out.println(str.replace(".", "/")); Test test = new Test(); String msg = "message to be printed"; int count = 5; double d = 1.3; float f = 1.3f; long l = 170343702; HashMap map = new HashMap(); test.printMsg(msg, count, d, f, l, map); } public void printMsg(String msg, int count, double d, float f, long l, HashMap map) { for (int i = 0; i < count; i++) System.out.println(msg); } public String getString(int i, String str) { return "abc"; } } [/code]

Test.java


 

4. 분석 실행 및 결과 확인

 

이렇게 한 다음 ClassInfo 클래스를 실행하면 아래와 같은 결과가 나온다.


[code] Class Name : test/Test Super Class : java/util/HashMap Interfaces : java/io/Serializable Methods : <clinit>()V <init>()V main([Ljava/lang/String;)V printMsg(Ljava/lang/String;IDFJLjav getString(ILjava/lang/String;)Ljava [/code]

클래스나 수퍼클래스, 인터페이스는 무슨 내용인지 바로 알겠는데, 메소드 부분은 바로 안 들어온다.

짧은 지식으로 대강 설명을 드리자면,

9: <clinit>는 클래스의 static 블럭. ()는 인자가 없음. V는 void로 리턴타입이 없다는 뜻

10: <init>는 constructor. 나머지는 위와 동일

11: main은 main 메소드.[Ljava/lang/String;에서 [로 시작하니까 배열이고, L로 시작하니까 자바클래스일 것 같고, V니까 void

      다 합치면 void main(String[]) 이런 형태가 되려나?

12: 인자에서 L로 시작하는 String과 HashMap은 감이 오는데, IDFJ는 뭔가?

        Test.java 소스를 보면서 맞추면 I는 int, D는 double, F는 float, J는 long가 될 것 같네.

       primitive type은 바이트코드에서 한글자로 줄여서 표현하는 모양이다.

13: 맨 마지막이 V가 아니고 Ljava/lang/String; 이니까 String을 리턴하는 메소드로군...

 

 

이렇게 해서 간단하게 클래스파일에서 간단한 정보를 뽑아 보는 방법을 알아 보았다.

여기서 Test.java를 클래스패스에 연결이 영 안 되는 분은 ClassInfo.java의 8라인에 "test.Test" 대신 "java.lang.String"을 입력해서 실행해 보면 String 클래스의 정보가 쭉 나오는 것을 확인할 수 있다.

 

클래스파일을 이용한 분석의 좋은 점은 JSP 파일을 분석할 수도 있다는 것이다.

JSP를 실행하려면 웹컨테이너가 java파일로 변환하고, 이걸 다시 클래스파일로 컴파일해서 실행하는데 JSP를 실행하고 나면 웹컨테이너 디렉토리 어딘가에 클래스파일이 남아 있다. 그러니까 이 경로만 찾아서 적당히 클래스패스로 잡아 주면 JSP도 분석이 가능한 것이다.

참고로 톰캣의 경우는 {Tomcat_Root}/work/Catalina/localhost 디렉토리 아래에 생기니 클래스패스 잘 잡아서 테스트해 보는 것도 재미있을 수 있겠다.

 

사실, 이 정도 정보 외에도 메소드 내의 각 코드 라인별로도 분석이 가능하다.

그런데 이 정도까지 가게 되면 거의 어셈블리 언어 수준까지 읽을 수 있는 능력이 필요하다.

(ASM이 무엇의 약자인지 아무리 해도 못 찾겠는데, 내 생각에는 ASM이 어셈블리의 약자가 아닌가 하는 생각도 든다.)

나도 학교 다닐 때 보던 기억을 더듬어 어느정도 읽어내긴 했지만 그런 걸 모르는 사람은 거의 암호 수준일 것 같다.

모 어려운 것도 있고, 호기심이 발동한 분들이 열심히 공부하길 바라는 마음도 있고, 이걸 보면서 머리 속에 굉장한 아이디어가 떠오른 것도 있고 해서 더 이상 심화된 내용은 다루지 않아야겠다.

 

궁금하면 공부합시다!