본문 바로가기

기술스택/ASM - [Bytecode]

[A Java bytecode engineering library] - [Core API] 3. Method[1/3]

목차

    https://d2.naver.com/helloworld/1230

     

    개인 요약 노트

    이전 장에서 ClassVisitor의 구현체가 내부에서 'visitCode', 'visitMethod' 등 기능을 Override했었다.

    추가적인 내용은 없었지만 사실 이 내부에서 바이트코드 명령어가 자동적으로 수행되고 있었을 것이다.

     

    바이트코드 변환 과정은 JVM의 스택 메모리 구조를 활용하여 클래스와 메서드를 실행하는 방식을 반영한다.

    메서드의 실행 과정은 지역 변수 배열에서 필요한 정보를 로드하여 피연산자 스택에서 연산을 수행하고, 그 결과를 다시 지역 변수 배열에 저장하거나 반환하는 과정이다.

     

    순서와 스택 프레임, 연산

     

    https://d2.naver.com/helloworld/1230

     

    JVM은 스택 기반의 가상 머신으로 각 스레드는 자신만의 실행 스택을 가지고 있으며, 이 스택은 여러 프레임으로 구성된다. 각 프레임은 하나의 메소드 호출을 나타내고, 로컬 변수와 오퍼랜드 스택을 포함한다.

     

    메서드 호출이 발생하면 새 스택 프레임이 생성되고, 메서드가 종료되면 스택 프레임이 제거된다. 이렇게 스택 프레임이 관리되므로, 여러 변수와 연산이 겹치더라도 각각의 메서드 컨텍스트 내에서 오퍼랜드 스택이 독립적으로 관리된다.

     

    스택 프레임 - 각 스레드 별 별도 생

    각 스택 프레임은 지역 변수 배열(Local Variable Array), 피연산자 스택(Operand Stack), 현재 실행 중인 메서드가 속한 클래스의 런타임 상수 풀에 대한 레퍼런스를 갖는다. 지역 변수 배열, 피연산자 스택의 크기는 컴파일 시에 결정되기 때문에 스택 프레임의 크기도 메서드에 따라 크기가 고정된다.

     

    지역 변수 배열 - 메서드의 매개변수와 지역 변수들이 저장되는 곳

    0부터 시작하는 인덱스를 가진 배열이다. 0은 메서드가 속한 클래스 인스턴스의 this 레퍼런스이고, 1부터는 메서드에 전달된 파라미터들이 저장되며, 메서드 파라미터 이후에는 메서드의 지역 변수들이 저장된다.

     

    피연산자 스택 - 메서드의 연산을 수행하기 위한 임시 저장 공간

    메서드의 실제 작업 공간이다.

    각 메서드는 피연산자 스택과 지역 변수 배열 사이에서 데이터를 교환하고, 다른 메서드 호출 결과를 추가하거나(push) 꺼낸다(pop). 피연산자 스택 공간이 얼마나 필요한지는 컴파일할 때 결정할 수 있으므로, 피연산자 스택의 크기도 컴파일 시에 결정된다.

     

    런타임 상수 풀(Runtime Constant Pool)

    클래스와 인터페이스에 대한 상수 및 메타데이터를 저장한다.

    이 영역은 클래스 또는 인터페이스를 JVM 메모리에 로드할 때 생성된다. 바이트코드 내에서 상수 값을 참조하거나 클래스의 메서드 및 필드에 접근할 때 사용된다.

     

     

    JVM 메모리 구조와 바이트코드 명령어의 상호작용

     

    실행 스택 내부의 지역 변수 배열에서 값을 가져오고, 실행 스택 내부의 오퍼랜드 스택을 통해 작업(연산)하며, 이를 다시 지역 변수 배열에 저장한다

     

    1. 실행 스택(Execution Stack): 각 스레드별로 별도의 실행 스택이 있으며, 메소드 호출 시 새로운 프레임이 이 스택에 푸시된다. 메소드가 반환되면 해당 프레임은 스택에서 pop된다.
    2. 프레임(Frame): 각 프레임은 메소드 호출에 대한 정보를 포함하며, 주로 두 부분으로 구성된다.
      • 로컬 변수 영역(Local Variables): 메소드의 파라미터와 로컬 변수를 저장한다. 각 변수는 인덱스를 통해 접근할 수 있다.
      • 오퍼랜드 스택(Operand Stack):
        오퍼랜드는 명령어가 수행될 때 필요한 데이터를 의미한다.
        바이트코드 명령어의 입력과 출력을 저장하는데 사용된다. 명령어 실행 시 필요한 값들을 스택에서 팝하여 연산을 수행하고, 결과를 다시 스택에 푸시한다.

        예시 : iadd라는 바이트코드 명령어는 정수 두 개를 스택에서 팝하여 더한 뒤 그 결과를 스택에 푸시한다. 여기서 정수 두 개가 '오퍼랜드'이다. 이 오퍼랜드들은 명령어가 실행되기 전에 스택에 위치하게 된다.
    3. 바이트코드 명령어(Bytecode Instructions)
      • 스택 조작 명령어: 스택의 값을 조작. pop, push, dup 등이 있다.
      • 로컬 변수 접근 명령어: 로컬 변수를 오퍼랜드 스택으로 로드하거나, 스택 값을 로컬 변수에 저장. iload, istore 등
      • 상수 로드 명령어: 상수 값을 스택에 로드. iconst_1, ldc 등
      • 산술 연산 명령어: 산술 연산을 수행. iadd, imul 등
      • 타입 변환 명령어: 타입을 변환. i2l, f2d 등
      • 객체 생성 및 조작 명령어: 객체를 생성하거나 필드에 접근하거나, 메소드를 호출. new, getfield, invokevirtual 등이 있다.
      • 조건부 점프 명령어: 조건에 따라 실행 흐름을 변경. ifeq, goto 등
      • 배열 조작 명령어: 배열 요소에 접근하거나 수정. iaload, iastore 등
      • 리턴 명령어: 메소드에서 값을 반환하고 종료. ireturn, areturn 등

     

    예시 : 산술 연산

    예를 들어 'int a = 3; a = a + 5;' 를 작성하면 다음과 같이 동작한다.

    1. 로컬 저장 : 현재 스레드의 스택 프레임 내부 '로컬 변수 영역'에 a라는 이름의 변수가 생성된다. 값은 3으로 할당된다.

    2. 오퍼랜드 스택 연산 : 

    a = a + 5;에 해당하는 바이트코드는 먼저 a의 값을 로컬 변수 영역에서 오퍼랜드 스택으로 로드(ILOAD)한다.

    그 다음 5를 오퍼랜드 스택에 푸시(BIPUSH 5 또는 ICONST_5)하고, 두 값을 더하는 IADD 연산을 수행한다.

    연산의 결과는 오퍼랜드 스택에 임시로 저장된다.

    3. 연산값 저장 :

    연산값은 다시 로컬 변수에 있는 'a' 에 저장되어야 한다. 'ISTORE'

    이 과정에서 2번에서 임시 저장했던 값이 오퍼랜드 스택에서 pop되며 a에 할당된다.

     

    이처럼 바이트코드 명령어들은 메소드의 로직을 낮은 수준에서 표현하며, JVM은 이 명령어들을 순차적으로 해석하고 실행하여 Java 프로그램을 실행한다.

     

    예시 : 실제 코드 예시

    package pkg;
    public class Bean {
            private int f;
            public int getF() { return this.f; }
            public void setF(int f) { this.f = f;} 
    }

     

    바이트 코드 변환시 : 

    ALOAD 0
    GETFIELD pkg/Bean f I  //getF()
    IRETURN
    ALOAD 0
    ILOAD 1
    PUTFIELD pkg/Bean f I  //setF()
    RETURN


    1. ALOAD 0:
    현재 객체(this)를 로컬 변수 영역에서 스택으로 로드(ALOAD)
    0은 로컬 변수 배열에서 this 객체의 위치를 가리킴. (모든 인스턴스 메서드에서 0은 this 자기 자신을 의미)
    즉, ALOAD 0은 만들어진 Bean Class의 인스턴스 참조값을 오퍼랜드 스택의 상단으로 푸시

    2. GETFIELD pkg/Bean f I:
    GETFIELD 명령어는 객체의 필드 값을 스택으로 로드하는 데 사용된다.
    pkg/Bean은 패키지/클래스 이름, f는 필드 이름, I는 필드 타입(int)을 나타낸다.
    이 명령어를 통해 스택의 상단에 있는 객체(this, 여기서는 Bean 인스턴스)로부터 f 필드의 값을 가져와서 다시 스택의 상단에 푸시
    -> f의 값이 오퍼랜드 스택 상단에 로드

    3. IRETURN:
    메서드에서 정수 타입 값을 반환하라는 의미.
    이를 통해 스택의 상단에 있는 값(스택 최상단에는 f를 저장했었음)을 메서드의 반환값으로 사용한다.
    즉, IRETURN은 오퍼랜드 스택의 상단에 있는 f 필드의 값을 가져와서 getF 메서드의 호출자에게 반환한다.
    이는 코드를 순차적으로 읽으며, 스택에 저장되기 때문

    4. ALOAD 0:
    1번과 마찬가지로 this 객체를 오퍼랜드 스택으로 로드

    5. ILOAD 1:
    메서드 파라미터 int f값을 오퍼랜드 스택으로 로드 (1은 인덱스 위치를 의미)

    ILOAD 1 명령어는 메소드의 로컬 변수 배열에서 두 번째 요소(1번 인덱스)를 로드하는 바이트코드 명령어입니다. Java 메소드에서 첫 번째 로컬 변수(0번 인덱스)는 항상 this 참조이다.(인스턴스 메소드의 경우). 두 번째 로컬 변수(1번 인덱스)는 메소드의 첫 번째 매개변수를 의미한다.

     

    6. PUTFIELD pkg/Bean f I:
    오퍼랜드 스택의 상단에 있는 값을 꺼내와서 pkg.Bean 클래스의 f 필드에 저장

    7. RETURN:
    메서드의 실행을 종료하고 호출자에게 제어를 반환. 
    setF 메서드는 반환 타입이 void이므로, 반환할 값이 없다.

     

     

    3. Method

    이 장에서는 코어 ASM API를 사용하여 컴파일된 메소드를 생성하고 변환하는 방법을 설명한다.

    컴파일된 메소드에 대한 소개로 시작하여 해당 ASM 인터페이스, 구성 요소 및 도구를 많은 예제와 함께 제시하고, 이를 통해 메소드를 생성하고 변환하는 방법을 소개한다.

    3.1. Structure

    컴파일된 클래스 내부에서 메소드의 코드는 바이트코드 명령의 시퀀스로 저장된다. 클래스를 생성하고 변환하기 위해서는 이러한 명령들을 알고 그 작동 방식을 이해하는 것이 중요하다. 이 섹션에서는 간단한 클래스 생성기와 변환기를 코딩하기 시작하기에 충분할 만큼 이러한 명령들에 대한 개요를 제공한다. 완전한 정의를 위해서는 Java 가상 머신 사양을 읽어야 한다.

    3.1.1. 실행 모델 - Execution model

    바이트코드 명령을 제시하기 전에, Java 가상 머신 실행 모델을 제시하는 것이 필요하다. 알다시피 Java 코드는 스레드 내에서 실행된다. 각 스레드는 자체 실행 스택을 가지며, 이는 프레임으로 구성된다. 각 프레임은 메소드 호출을 나타낸다: 메소드가 호출될 때마다 새로운 프레임이 현재 스레드의 실행 스택에 푸시된다. 메소드가 정상적으로 반환되거나 예외로 인해 반환될 때, 이 프레임은 실행 스택에서 pop되고 실행은 호출 메소드(스택의 상단에 있는 프레임)에서 계속된다.

     

    각 프레임에는 두 부분이 포함된다:

    로컬 변수 부분과 operand 스택 부분. 로컬 변수 부분에는 인덱스별로 접근할 수 있는 변수들이 포함된다. operand 스택 부분은, 이름에서 암시하듯이, 바이트코드 명령에 의해 사용되는 operand 값들의 스택이다. 이는 operand 스택에 있는 값들이 마지막으로 들어간 것이 먼저 나오는 순서로만 접근될 수 있음을 의미한다. operand 스택과 스레드의 실행 스택을 혼동하지 말라: 실행 스택의 각 프레임은 자체 operand 스택을 포함한다.

     

    로컬 변수와 operand 스택 부분의 크기는 메소드의 코드에 따라 달라진다.

    이는 컴파일 시간에 계산되어 컴파일된 클래스에 바이트코드 명령과 함께 저장된다. 결과적으로, 주어진 메소드 호출에 해당하는 모든 프레임은 동일한 크기를 가지지만, 다른 메소드에 해당하는 프레임은 로컬 변수와 operand 스택 부분의 크기가 다를 수 있다.

     

    Figure 3.1.: An execution stack with 3 frames

     

     

     

     

     

     

    3.1.2. 바이트코드 명령 - Bytecode instructions [중요]

    Opcode와 Arguments

     

    바이트코드 명령은 이 명령을 식별하는 오퍼코드와 고정된 수의 인수로 구성된다:

     

    • opcode 오퍼코드는 부호 없는 바이트 값이며 – 따라서 바이트코드라는 이름 – 니모닉 심볼( mnemonic symbol )로 식별된다.

    예를 들어, 오퍼코드 값 0은 NOP이라는 니모닉 심볼로 지정되며, 아무 것도 하지 않는 명령에 해당한다.

     

    • arguments 인수는 명령 동작을 정확히 정의하는 정적 값이다. 이들은 오퍼코드 바로 뒤에 제공된다. 예를 들어, 오퍼코드 값이 167인 GOTO label 명령은 label이라는 인수를 취하며, 이는 실행될 다음 명령을 지정하는 레이블이다. 명령 인수는 명령 operand 와 혼동해서는 안 된다: 인수 값은 정적으로 알려져 있으며 컴파일된 코드에 저장되는 반면, operand 값은 operand 스택에서 오며 런타임에만 알려진다.

     

    바이트코드 명령은 두 가지 범주로 나눌 수 있다:

    명령의 소수 집합은 로컬 변수에서 operand 스택으로 값을 전달하고 그 반대로 전달하는 데 사용되며 다른 명령은 operand 스택에서만 작동한다: 스택에서 일부 값을 팝하고, 이 값에 기반한 결과를 계산하고, 그 결과를 스택에 다시 push한다.

     

    LOAD와 STORE

     

    ILOAD, LLOAD, FLOAD, DLOAD, 및 ALOAD 명령은 로컬 변수를 읽고 그 값을 operand 스택에 푸시한다.
    이들은 읽어야 할 로컬 변수의 인덱스 i를 인수로 취한다. ILOAD는 boolean, byte, char, short, 또는 int 로컬 변수를 로드하는 데 사용된다. LLOAD, FLOAD 및 DLOAD는 각각 long, float 또는 double 값을 로드하는 데 사용된다(LLOAD 및 DLOAD는 실제로 i와 i+1의 두 슬롯을 로드한다). 마지막으로 ALOAD는 모든 비 기본값, 즉 객체 및 배열 참조를 로드하는 데 사용된다. 대칭적으로 ISTORE, LSTORE, FSTORE, DSTORE 및 ASTORE 명령은 operand 스택에서 값을 팝하고 i 인덱스로 지정된 로컬 변수에 저장한다.


    여기서 xLOAD 및 xSTORE 명령이 타입화되어 있다는 것을 볼 수 있다(실제로 거의 모든 명령이 타입화되어 있다).
    이는 잘못된 변환을 방지하기 위해 사용된다. 실제로 로컬 변수에 값을 저장한 다음 다른 타입으로 로드하는 것은 불법이다. 예를 들어, ISTORE 1 ALOAD 1 시퀀스는 불법이다 - 이는 로컬 변수 1에 임의의 메모리 주소를 저장하고 이 주소를 객체 참조로 변환할 수 있게 한다! 그러나 로컬 변수에 현재 저장된 값의 타입과 다른 타입의 값을 저장하는 것은 완벽하게 합법적이다. 이는 로컬 변수의 타입, 즉 이 로컬 변수에 저장된 값의 타입이 메소드 실행 중 변경될 수 있음을 의미한다.


    위에서 언급했듯이, 다른 모든 바이트코드 명령은 operand 스택에서만 작동한다. 이들은 다음과 같은 범주로 그룹화될 수 있다(부록 A.1 참조):

    Stack 스택 

    이 명령은 스택의 값들을 조작하는 데 사용된다: POP은 스택 맨 위의 값을 팝하고, DUP은 스택 상단의 값을 복사하여 푸시하며, SWAP은 두 값을 팝하고 그들을 반대 순서로 푸시한다.

     

    Constants 상수

    이 명령은 operand 스택에 상수 값을 푸시한다: ACONST_NULL은 null을 푸시하고, ICONST_0은 int 값 0을 푸시하며, FCONST_0은 0f를, DCONST_0은 0d를 푸시한다. BIPUSH b는 byte 값 b를, SIPUSH s는 short 값 s를 푸시하며, LDC cst는 임의의 int, float, long, double, String 또는 class 상수 cst를 푸시한다.

     

    Arithmetic and logic 산술 및 논리

    이 명령은 operand 스택에서 수치 값을 팝하고, 그들을 결합하여 결과를 스택에 푸시한다. 이들은 인수가 없다. xADD, xSUB, xMUL, xDIV 및 xREM은 +, -, *, / 및 % 연산에 해당하며, 여기서 x는 I, L, F 또는 D 중 하나이다. 유사하게 int 및 long 값에 대해 <, >, >>, |, & 및 ^에 해당하는 다른 명령이 있다.

     

    Casts 캐스트

    이 명령은 스택에서 값을 팝하고, 그것을 다른 타입으로 변환한 후 결과를 다시 푸시한다. 이들은 Java에서의 캐스트 표현식에 해당한다. I2F, F2D, L2D 등은 수치 값을 하나의 수치 타입에서 다른 타입으로 변환한다. CHECKCAST t는 참조 값을 t 타입으로 변환한다.

     

    Objects 객체

    이 명령은 객체를 생성하고, 잠그고, 그들의 타입을 테스트하는 데 사용된다. 예를 들어, NEW 타입 명령은 타입 타입의 새 객체를 스택에 푸시한다(여기서 타입은 내부 이름이다).

     

    Fields 필드

    이 명령은 필드의 값을 읽거나 쓴다. GETFIELD owner name desc는 객체 참조를 팝하고 그 name 필드의 값을 푸시한다. PUTFIELD owner name desc는 값을 객체 참조와 함께 팝하고, 이 값을 그 name 필드에 저장한다. 두 경우 모두 객체는 owner 타입이어야 하며, 그 필드는 desc 타입이어야 한다. GETSTATIC 및 PUTSTATIC은 유사한 명령이지만 정적 필드에 대한 것이다.

     

    Methods 메소드

    이 명령은 메소드 또는 생성자를 호출한다. 이들은 메소드 인수 수만큼의 값과 대상 객체를 위한 하나의 값을 팝하고, 메소드 호출의 결과를 푸시한다. INVOKEVIRTUAL owner name desc는 클래스 owner에서 정의된 name 메소드를 호출하며, 그 메소드 설명자는 desc이다. INVOKESTATIC은 정적 메소드에 대해, INVOKESPECIAL은 개인 메소드 및 생성자에 대해, INVOKEINTERFACE는 인터페이스에서 정의된 메소드에 대해 사용된다. 마지막으로, Java 7 클래스의 경우, INVOKEDYNAMIC은 새로운 동적 메소드 호출 메커니즘에 사용된다.

     

    Arrays 배열

    이 명령은 배열에서 값들을 읽고 쓰는 데 사용된다. xALOAD 명령은 인덱스와 배열을 팝하고, 이 인덱스에서 배열 요소의 값을 푸시한다. xASTORE 명령은 값을 인덱스와 배열과 함께 팝하고, 이 값을 배열의 해당 인덱스에 저장한다. 여기서 x는 I, L, F, D 또는 A일 수 있지만, B, C 또는 S일 수도 있다.

     

    Jumps 점프

    이 명령은 특정 조건이 참이거나 무조건적으로 임의의 명령으로 점프한다. 이들은 if, for, do, while, break 및 continue 명령을 컴파일하는 데 사용된다. 예를 들어, IFEQ label은 스택에서 int 값을 팝하고, 이 값이 0이면 label로 지정된 명령으로 점프한다(그렇지 않으면 실행은 다음 명령으로 정상적으로 계속된다). IFNE 또는 IFGE와 같은 많은 다른 점프 명령이 존재한다. 마지막으로, TABLESWITCH 및 LOOKUPSWITCH는 Java의 switch 명령에 해당한다.

     

    Return 반환

    마지막으로 xRETURN 및 RETURN 명령은 메소드 실행을 종료하고 결과를 호출자에게 반환하는 데 사용된다. RETURN은 void를 반환하는 메소드에 사용되며, xRETURN은 다른 메소드에 사용된다.

     

     

    3.1.3. Examples

    바이트코드 명령이 어떻게 작동하는지에 대한 구체적인 감각을 얻기 위해 몇 가지 기본 예제를 살펴보자.

    원문

     

     

    1. getter의 바이트코드 메서드는 다음과 같다.

     

     

    첫 번째 명령은 로컬 변수 0을 읽고, 이 변수는 이 메소드 호출을 위한 프레임 생성 시 this로 초기화되었으며, 이 값을 operand 스택에 푸시한다.

    두 번째 명령은 스택에서 이 값을 팝하고, 즉 this, 그리고 이 객체의 f 필드, 즉 this.f를 푸시한다.

    마지막 명령은 스택에서 이 값을 팝하고, 호출자에게 반환한다. 이 메소드에 대한 실행 프레임의 연속적인 상태는 그림 3.2에서 보여진다.

    Figure 3.2.: Successive frame states for the getF method: a) initial state, b) after ALOAD 0 and c) after GETFIELD

     

     

     

    2. setter의 경우는 다음과 같다.

    원문

     

     

    첫 번째 명령은 이전처럼 this를 operand 스택에 푸시한다.

    두 번째 명령은 로컬 변수 1을 푸시하는데, 이 변수는 이 메소드 호출을 위한 프레임 생성 시 f 인수 값으로 초기화되었다. 세 번째 명령은 이 두 값을 팝하고, 참조된 객체의 f 필드에 int 값을 저장한다, 즉 this.f에 저장한다. 마지막 명령은 소스 코드에서는 암시적이지만 컴파일된 코드에서는 필수적으로, 현재 실행 프레임을 파괴하고 호출자에게 반환한다. 이 메소드에 대한 실행 프레임의 연속적인 상태는 그림 3.3에서 보여진다.

     

    Bean 클래스는 또한 프로그래머에 의해 명시적으로 생성자가 정의되지 않았기 때문에 컴파일러에 의해 생성된 기본 public 생성자를 가지고 있다. 이 기본 public 생성자는 Bean() { super(); }로 생성된다. 이 생성자의 바이트코드는 다음과 같다:

    ALOAD 0

    Figure 3.3.: Successive frame states for the setF method: a) initial state, b) after ALOAD 0, c) after ILOAD 1 and d) after PUTFIELD

     

    INVOKESPECIAL java/lang/Object <init> ()V
    RETURN

     

    첫 번째 명령은 operand 스택에 this를 푸시한다.

    두 번째 명령은 스택에서 이 값을 팝하고, Object 클래스에서 정의된 <init> 메소드를 호출한다. 이것은 super() 호출, 즉 슈퍼 클래스인 Object의 생성자 호출에 해당한다. 여기서 생성자는 컴파일된 클래스와 소스 클래스에서 다르게 명명된다는 것을 볼 수 있다: 컴파일된 클래스에서는 항상 <init>으로 명명되며, 소스 클래스에서는 정의된 클래스의 이름을 가진다.

    마지막으로 마지막 명령은 호출자에게 반환한다.

     

     

    이제 조금 더 복잡한 setter 메소드를 고려해 보자:

     

     

    이 경우 바이트코드는 다음과 같이 적용될 것이다.

    	ILOAD 1
    	IFLT label
    	ALOAD 0
    	ILOAD 1
    	PUTFIELD pkg/Bean f I
    	GOTO end
    label:
    	NEW java/lang/IllegalArgumentException
    	DUP
    	INVOKESPECIAL java/lang/IllegalArgumentException <init> ()V
    	ATHROW
    end:
    	RETURN

     

    첫 번째 명령은 operand 스택에 로컬 변수 1, 즉 초기화된 f를 푸시한다.

    IFLT 명령은 스택에서 이 값을 팝하고, 그것을 0과 비교한다. 만약 이 값이 0보다 작으면 (LT), 레이블 label로 지정된 명령으로 점프한다. 그렇지 않으면 아무것도 하지 않고 다음 명령으로 실행을 계속한다.

     

    다음 세 명령은 setF 메소드의 명령과 동일하다. GOTO 명령은 무조건적으로 end 레이블로 지정된 명령, 즉 RETURN 명령으로 점프한다. label과 end 레이블 사이의 명령은 예외 객체를 생성하고 operand 스택에 푸시한다: NEW 명령은 예외 객체를 생성하고, DUP 명령은 스택에서 이 값을 복제한다. INVOKESPECIAL 명령은 이 두 복사본 중 하나를 팝하고 그것에 예외 생성자를 호출한다. 마지막으로 ATHROW 명령은 남은 복사본을 팝하고 예외로 던진다(그래서 실행은 다음 명령으로 계속되지 않는다).

     

     

    3.1.4. 예외 핸들러 - Exception handlers

     

    예외를 잡는 바이트코드 명령은 없다. 대신 메소드의 바이트코드는 특정 메소드 부분에서 예외가 발생했을 때 실행되어야 하는 코드를 지정하는 예외 핸들러 목록과 연관되어 있다.

    예외 핸들러는 try-catch 블록과 유사하다. 범위가 있으며, 이는 try 블록의 내용에 해당하는 명령 시퀀스이고, 핸들러는 catch 블록의 내용에 해당한다. 범위는 시작과 끝 레이블로 지정되며, 핸들러는 시작 레이블로 지정된다.

    예를 들어, 아래의 소스 코드를 보자.

     

     

    이 예문은 다음과 같이 컴파일된다.

     

    TRYCATCHBLOCK try catch catch java/lang/InterruptedException
    try:
        LLOAD 0
        INVOKESTATIC java/lang/Thread sleep (J)V
        RETURN
    catch:
        INVOKEVIRTUAL java/lang/InterruptedException printStackTrace ()V
        RETURN

     

     

     

    try와 catch 레이블 사이의 코드는 try 블록에 해당하며, catch 레이블 이후의 코드는 catch 블록에 해당한다. TRYCATCHBLOCK 줄은 try와 catch 레이블 사이의 범위를 커버하는 예외 핸들러를 지정하며, catch 레이블에서 시작하는 핸들러와 InterruptedException의 서브클래스인 예외에 대한 핸들러를 지정한다.

    이는 try와 catch 사이 어디에서든 이러한 예외가 발생하면 스택이 클리어되고, 예외가 이 빈 스택에 푸시되며, catch에서 실행이 계속된다는 것을 의미한다.

     

     

    3.1.5. Frames

     

    Java 6 이상으로 컴파일된 클래스는 바이트코드 명령 외에도 Java 가상 머신 내 클래스 검증 프로세스를 가속화하기 위해 사용되는 스택 맵 프레임 세트를 포함한다. 스택 맵 프레임은 메소드의 실행 중 특정 지점에서 메소드의 실행 프레임 상태를 제공한다. 보다 정확히 말하자면, 특정 바이트코드 명령이 실행되기 직전에 각 로컬 변수 슬롯과 operand 스택 슬롯에 포함된 값의 타입을 제공한다.

     

    예를 들어, 이전 섹션에서 getF 메소드를 고려한다면, ALOAD 전, GETFIELD 전, IRETURN 전에 실행 프레임의 상태를 제공하는 세 개의 스택 맵 프레임을 정의할 수 있다. 이 세 개의 스택 맵 프레임은 그림 3.2에서 보여진 세 가지 경우에 해당하며, 다음과 같이 설명될 수 있다. 여기서 첫 번째 대괄호 사이의 타입은 로컬 변수에 해당하고, 다른 타입은 operand 스택에 해당한다:

     

     

     

    We can do the same for the checkAndSetF method:

     

     

    이것은 이전 메소드와 유사하지만, Uninitialized(label) 타입을 제외한다. 이는 오직 스택 맵 프레임에서만 사용되는 특별한 타입으로, 메모리가 할당되었지만 생성자가 아직 호출되지 않은 객체를 지정한다. 인수는 이 객체를 생성한 명령을 지정한다. 이 타입의 값에 대해 호출할 수 있는 유일한 메소드는 생성자이다. 호출되면 프레임의 이 타입의 모든 발생은 여기에서 IllegalArgumentException으로 실제 타입으로 대체된다.

     

    스택 맵 프레임은 다른 세 가지 특별한 타입을 사용할 수 있다: UNINITIALIZED_THIS는 생성자의 로컬 변수 0의 초기 타입, TOP은 정의되지 않은 값을 나타내며, NULL은 null에 해당한다.

     

    Java 6부터 시작하여, 컴파일된 클래스는 바이트코드 외에도 스택 맵 프레임 세트를 포함한다. 공간을 절약하기 위해, 컴파일된 메소드에는 각 명령마다 하나의 프레임이 포함되어 있지 않다. 실제로는 점프 대상이나 예외 핸들러에 해당하는 명령 또는 무조건적인 점프 명령을 따르는 명령에 대한 프레임만 포함된다. 실제로 다른 프레임은 이들로부터 쉽고 빠르게 유추될 수 있다.

     

    checkAndSetF 메소드의 경우, 이는 저장되어야 하는 두 개의 프레임만 의미한다:

    하나는 IFLT 명령의 대상인 NEW 명령을 위한 것이며, 또 다른 하나는 GOTO 명령을 따르는 RETURN 명령을 위한 것이다. 공간을 더욱 절약하기 위해, 각 프레임은 이전 프레임과 비교하여 차이만 저장하여 압축되며, 초기 프레임은 전혀 저장되지 않는다. 왜냐하면 메소드 매개변수 타입에서 쉽게 유추될 수 있기 때문이다. checkAndSetF 메소드의 경우 저장되어야 하는 두 프레임은 동일하고 초기 프레임과 동일하므로 F_SAME 니모닉으로 지정된 단일 바이트 값으로 저장된다. 이 프레임은 관련된 바이트코드 명령 바로 앞에 표현될 수 있다. 이것은 checkAndSetF 메소드에 대한 최종 바이트코드를 제공한다:

     

     

     

     

    Reference

     

    https://asm.ow2.io/asm4-guide.pdf

     

    ASM USER GUIDE

     

    Copyright c 2007, 2011 Eric Bruneton All rights reserved. Redistribution and use in source (LYX format) and compiled forms (LATEX, PDF, PostScript, HTML, RTF, etc), with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code (LYX format) must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in compiled form (converted to LATEX, PDF, PostScript, HTML, RTF, and other formats) must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. The name of the author may not be used to endorse or promote products derived from this documentation without specific prior written permission.

     

    THIS DOCUMENTATION IS PROVIDED BY THE AUTHOR “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.