본문 바로가기

Java/JVM

[Java] Java 소스코드 Compile, Hex Dump, Disassemble

컴파일 (Compile)

javac [source_code_name].java 명령어를 실행 시켜, 아래의 자바 소스 코드를 바이트 코드로 컴파일하였다..

public class ByteCode {  
    public static void main(String[] args) {  
        int x = 5;  
        int y = 32768;  
        int z = x + y;  
    }  
}

 

컴파일한 클래스 파일(.class)을 vim 텍스트 편집기로 열어보면, 다음과 같이 사람이 읽기 어려운 바이너리 형태로 저장되어있다.

Êþº¾^@^@^@B^@^\
^@^B^@^C^G^@^D^L^@^E^@^F^A^@^Pjava/lang/Object^A^@^F<init>^A^@^C()V^C^@^@<80>^@ ^@      ^@
^G^@^K^L^@^L^@^M^A^@^Pjava/lang/System^A^@^Cout^A^@^ULjava/io/PrintStream;
^@^O^@^P^G^@^Q^L^@^R^@^S^A^@^Sjava/io/PrintStream^A^@^Gprintln^A^@^D(I)V^G^@^U^A^@^Qbytecode/ByteCode^A^@^DCode^A^@^OLineNumberTable^A^@^Dmain^A^@^V([Ljava/lang/String;)V^A^@
SourceFile^A^@^MByteCode.java^@!^@^T^@^B^@^@^@^@^@^B^@^A^@^E^@^F^@^A^@^V^@^@^@^]^@^A^@^A^@^@^@^E*·^@^A±^@^@^@^A^@^W^@^@^@^F^@^A^@^@^@^C^@       ^@^X^@^Y^@^A^@^V^@^@^@9^@^B^@^D^@^@^@^Q^H<^R^G=^[^\`>²^@^H^]¶^@^N±^@^@^@^A^@^W^@^@^@^V^@^E^@^@^@^E^@^B^@^F^@^E^@^G^@    ^@^H^@^P^@      ^@^A^@^Z^@^@^@^B^@^[

바이트코드는 본질적으로 이진 데이터(binary data)이다. 컴파일된 클래스 파일(.class)을 헥사 덤프(hex dump) 도구를 사용하여 이진 데이터에 대한 표현을 볼수있다.


헥사 덤프 (hex dump)

xxd [class_file_name].class 명령어로 헥사 덤프(hex dump)를 떠서 보면, 각 바이트를 16진수 형식으로 나타내고 해당 바이트의 ASCII 문자 표현도 함께 제공하는 것을 볼 수 있다. 이는 주로 이진 데이터(바이너리 데이터)를 분석하거나 디버깅할 때 사용된다.

00000000: cafe babe 0000 0042 001c 0a00 0200 0307  .......B........
00000010: 0004 0c00 0500 0601 0010 6a61 7661 2f6c  ..........java/l
00000020: 616e 672f 4f62 6a65 6374 0100 063c 696e  ang/Object...<in
00000030: 6974 3e01 0003 2829 5603 0000 8000 0900  it>...()V.......
00000040: 0900 0a07 000b 0c00 0c00 0d01 0010 6a61  ..............ja
00000050: 7661 2f6c 616e 672f 5379 7374 656d 0100  va/lang/System..
00000060: 036f 7574 0100 154c 6a61 7661 2f69 6f2f  .out...Ljava/io/
00000070: 5072 696e 7453 7472 6561 6d3b 0a00 0f00  PrintStream;....
00000080: 1007 0011 0c00 1200 1301 0013 6a61 7661  ............java
00000090: 2f69 6f2f 5072 696e 7453 7472 6561 6d01  /io/PrintStream.
000000a0: 0007 7072 696e 746c 6e01 0004 2849 2956  ..println...(I)V
000000b0: 0700 1501 0011 6279 7465 636f 6465 2f42  ......bytecode/B
000000c0: 7974 6543 6f64 6501 0004 436f 6465 0100  yteCode...Code..
000000d0: 0f4c 696e 654e 756d 6265 7254 6162 6c65  .LineNumberTable
000000e0: 0100 046d 6169 6e01 0016 285b 4c6a 6176  ...main...([Ljav
000000f0: 612f 6c61 6e67 2f53 7472 696e 673b 2956  a/lang/String;)V
00000100: 0100 0a53 6f75 7263 6546 696c 6501 000d  ...SourceFile...
00000110: 4279 7465 436f 6465 2e6a 6176 6100 2100  ByteCode.java.!.
00000120: 1400 0200 0000 0000 0200 0100 0500 0600  ................
00000130: 0100 1600 0000 1d00 0100 0100 0000 052a  ...............*
00000140: b700 01b1 0000 0001 0017 0000 0006 0001  ................
00000150: 0000 0003 0009 0018 0019 0001 0016 0000  ................
00000160: 0039 0002 0004 0000 0011 083c 1207 3d1b  .9.........<..=.
00000170: 1c60 3eb2 0008 1db6 000e b100 0000 0100  .`>.............
00000180: 1700 0000 1600 0500 0000 0500 0200 0600  ................
00000190: 0500 0700 0900 0800 1000 0900 0100 1a00  ................
000001a0: 0000 0200 1b                             .....

헥사 덤프의 구조는 3종류의 열로 구성된다.

  • 가장 왼쪽 열 오프셋 (Offset)이며, 각 바이트의 시작 위치를 나타내는 16진수 값이다.
  • 중간 열 데이터의 각 바이트를 16진수로 나타낸 값이며, 여기서 cafebabe는 JVM이 이 파일이 유효한 클래스 파일임을 확인할 때 사용한다.
  • 가장 마지막 열 데이터의 각 바이트중에서 출력 가능한 문자만 ASCII 문자로 변환한 값이다.

역어셈블 (Disassemble)

텍스트 편집기는 기본적으로 Unicode를 기준으로 문자 인코딩으로 해석하려고 시도한다. UTF-8에서도 0부터 127까지의 문자가 ASCII와 동일하게 사용된다. 그러나 대부분의 바이너리 데이터는 문자 인코딩되지 않았기 때문에, 유효한 텍스트 문자가 아니라서 이상한 문자나 기호로 표시된다. 헌데, 이 바이너리 파일에서 유효한 텍스트 형태로 표시되는 경우도 있다. 텍스트 편집기나 헥스 덤프를 보면, 어떠한 데이터를 참조하는 파일 경로가 인코딩이 되어서 문자열이 보이는 것으로 짐작은 할 수 있다.

 

해당 원인을 찾기 위해, javap(disassembler) 프로그램을 사용하여 해당 바이트 코드를 disassemble하여, 읽을 수 있는 형식으로 출력해봐야한다. 이를 통해 컴파일된 Java 프로그램이 실제로 어떻게 동작하는지, 컴파일러가 어떻게 최적화를 수행했는지 등을 확인할 수 있다.

 

javap -v [class_file_name] 명령어를 실행시켜서 다음과 같이 출력이 되었다. -v 옵션은 'verbose'의 약자이며, 추가적인 내용까지 자세히 출력해준다. 여기서 추가적인 내용은 여기서 자세한 내용은 클래스 파일의 클래스 정보와 상수 풀(Constant Pool)의 내용을 추가적을 제공한다. 기본적인 내용은 클래스 파일 내의 메서드들이 동작하는 실행 과정이다.

Classfile /Users/[username]/Documents/dev/backend/java/java_study/java-study/src/bytecode/ByteCode.class
  Last modified May 30, 2024; size 299 bytes
  SHA-256 checksum e631cb5106c5665610c9e25649d149e251ad5a260d6732ec0fbe30dc7a119e02
  Compiled from "ByteCode.java"
public class bytecode.ByteCode
  minor version: 0
  major version: 66
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #8                          // bytecode/ByteCode
  super_class: #2                         // java/lang/Object
  interfaces: 0, fields: 0, methods: 2, attributes: 1
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Integer            32768
   #8 = Class              #9             // bytecode/ByteCode
   #9 = Utf8               bytecode/ByteCode
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               main
  #13 = Utf8               ([Ljava/lang/String;)V
  #14 = Utf8               SourceFile
  #15 = Utf8               ByteCode.java
{
  public bytecode.ByteCode();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: iconst_5
         1: istore_1
         2: ldc           #7                  // int 32768
         4: istore_2
         5: iload_1
         6: iload_2
         7: iadd
         8: bipush        50
        10: iadd
        11: istore_3
        12: return
      LineNumberTable:
        line 5: 0
        line 6: 2
        line 7: 5
        line 8: 12
}
SourceFile: "ByteCode.java"

위에서 Constant pool에 주목해보자. 이 상수 풀(Constant Pool)에 저장된 데이터는 Utf8 형식으로 인코딩되어 있어 읽을 수 있는 텍스트로 보이는 것이다. 클래스 파일에는 클래스 이름, 메서드 이름, 문자열 리터럴 등과 같은 메타데이터가 포함된 상수 풀이 있다. 해당 상수 풀이 UTF-8 형식으로 인코딩 되었다. 

 

위의 Constant pool 아래 있는 메서드 부분의 바이트 코드를 살펴보자.

각 메서드의 명령어(opcode and operand)들과 맵핑된 인덱스가 있다. 그리고 각 메서드가 사용하는 스택 크기와 로컬 변수의 크기를 표시하고 있다.

 

각 명령어는 JVM이 수행할 연산(opcode)과 데이터 또는 데이터의 위치 혹은 명령어의 위치를 나타내는 피연산자(operand)로 구성되어 있다. 마치 어셈블리어와 비슷한 것을 볼 수 있으며, 바이트 코드도 마찬가지로 저수준의 명령어 단위로 수행되는 작업을 기술하기 때문이다.

 

바이트 코드 해석에 대한 내용은 클래스 파일의 메모리 로드과정에 설명되어있다.