티스토리 뷰

ASM

ASM User Guide 번역 3

yimoc 2018. 9. 11. 16:04

3. Methods


이 장에서는 core ASM API를 사용하여 컴파일 된 메소드를 생성하고 변환하는 방법을 설명합니다. 그것은 컴파일 된 메소드의 표현으로 시작하여 많은 예제를 사용하여 해당하는 ASM 인터페이스, 컴포넌트 및 툴을 생성하고 변환하는 도구를 제공합니다.


3.1. Structure


컴파일 된 클래스 내에서 메소드 코드는 바이트 코드 명령어의 시퀀스로 저장됩니다. 클래스를 생성하고 변환하기 위해 이들 지침을 알고 그들이 어떻게 작동하는지 이해하는 것이 필수적입니다. 이 섹션에서는 간단한 클래스 생성기 및 변환기의 코딩을 시작하기에 충분해야하는 지침의 개요를 제공합니다. 전체 정의를 보려면 Java Virtual Machine Specification을 읽어야합니다.


3.1.1. Execution model


바이트 코드 명령을 제시하기 전에 Java Virtual Machine 실행 모델을 제시합니다. 아시다시피 Java 코드는 스레드 내에서 실행됩니다.

각 스레드는 프레임으로 구성된 자체 실행 스택을 가지고 있습니다. 각 프레임은 메소드 호출을 나타냅니다. 메소드가 호출 될 때마다 현재 스레드의 실행 스택에 새로운 프레임이 푸시됩니다. 메서드가 정상적으로 또는 예외로 인해 반환되면이 프레임이 실행 스택에서 pop되고 호출 메서드에서 실행이 계속됩니다 (이제 프레임이 스택 맨 위에 있음).


각 프레임은 로컬 변수 부분과 피연산자(Operand) 스택 부분의 두 부분으로 구성됩니다. 지역 변수 부분에는 인덱스로 임의의 순서로 액세스 할 수있는 변수가 들어 있습니다. 피연산자 스택 부분은 이름에서 알 수 있듯이 바이트 코드 명령어에 의해 피연산자로 사용되는 값들의 스택입니다. 즉,이 스택의 값은 선입 선출 (Last In First Out) 순서로만 액세스 할 수 있습니다. 피연산자 스택과 스레드의 실행 스택을 혼동하지 마십시오. 실행 스택의 각 프레임에는 자체 피연산자 스택이 있습니다.


로컬 변수와 피연산자 스택 부분의 크기는 메서드의 코드에 따라 다릅니다. 컴파일시에 계산되어 컴파일 된 클래스의 바이트 코드 명령어와 함께 저장됩니다. 결과적으로, 주어진 메소드의 호출에 대응하는 모든 프레임은 같은 크기를 갖지만, 다른 메소드에 대응하는 프레임은 로컬 변수와 피연산자 스택 부분에 대해 다른 크기를 가질 수 있습니다.


Figure 2.1 : An execution stack with 3 frames


그림 3.1은 3 프레임을 가진 샘플 실행 스택을 보여줍니다. 첫 번째 프레임은 3 개의 로컬 변수를 포함하고, 피연산자 스택의 최대 크기는 4이며 두 개의 값을 포함합니다. 두 번째 프레임은 2 개의 로컬 변수와 피연산자 스택에 두 개의 값을 포함합니다. 마지막으로 실행 스택 맨 위에있는 세 번째 프레임에는 4 개의 로컬 변수와 두 개의 피연산자가 들어 있습니다.


프레임이 생성되면 프레임은 빈 스택으로 초기화되고 해당 로컬 변수는 대상 객체로 this (비 정적 메서드의 경우) 및 메서드의 인수로 초기화됩니다. 예를 들어, a.equals (b) 메서드를 호출하면 스택이 비어 있고 처음 두 개의 로컬 변수가 a 및 b (다른 로컬 변수는 초기화되지 않음)로 초기화 된 프레임이 만들어집니다.


로컬 변수와 피연산자 스택 부분의 각 슬롯은 long 값과 double 값을 제외한 모든 Java 값을 포함 할 수 있습니다. 이 값에는 두 개의 슬롯이 필요합니다. 이것은 로컬 변수의 관리를 복잡하게합니다. 예를 들어 i 번째 메소드 인수가 반드시 로컬 변수 i에 저장되는 것은 아닙니다. 예를 들어, Math.max (1L, 2L)를 호출하면 첫 번째 두 개의 로컬 변수 슬롯에 1L 값을 가진 프레임이 만들어지고 세 번째와 네 번째 슬롯에 2L 값이있는 프레임이 만들어집니다


3.1.2. Bytecode instructions


바이트 코드 명령어는이 명령어를 식별하는 opcode와 고정 된 수의 argument로 구성됩니다.


• opcode는 부호없는 바이트 값이며 따라서 바이트 코드 이름이며 니모닉(기억하기 쉬운) 기호로 식별됩니다. 예를 들어 opcode 값 0은 니모닉 기호 NOP에 의해 설계되며 아무 것도 수행하지 않는 명령에 해당합니다.


• 인수는 정확한 명령어 동작을 정의하는 정적 값입니다.

그들은 opcode 바로 다음에 주어집니다. 예를 들어 opcode 값이 167 인 GOTO 레이블 명령어는 실행될 다음 명령어를 지정하는 레이블 인 인수 레이블을 취합니다. 명령어 인수는 명령어 피연산자와 혼동되어서는 안됩니다. 인수 값은 정적으로 알려져 컴파일 된 코드에 저장되는 반면, 피연산자 값은 피연산자 스택에서 가져온 것이며 런타임에만 인식됩니다.


바이트 코드 명령어는 두 가지 카테고리로 나누어 질 수 있습니다 : 로컬 변수에서 피연산자 스택으로 값을 전송하도록 작은 명령어 세트가 설계되었습니다. 다른 명령어는 피연산자 스택에서만 작동합니다. 즉, 스택에서 일부 값을 팝핑하고 이러한 값을 기반으로 결과를 계산 한 다음 다시 스택에 푸시합니다.


ILOAD, LLOAD, FLOAD, DLOADALOAD 명령어는 로컬 변수를 읽고 해당 값을 피연산자 스택에 푸시합니다. 그들은 읽어야 만하는 로컬 변수의 인덱스 i를 인수로 취합니다. ILOAD는 boolean, byte, char, short 또는 int 로컬 변수를 로드하는 데 사용됩니다. LLOAD, FLOADDLOAD는 각각 long, float 또는 double 값을 로드하는 데 사용됩니다 (LLOADDLOAD는 실제로 두 개의 슬롯 i 및 i + 1을 로드합니다). 마지막으로 ALOAD는 non primitive 값, 즉 객체 및 배열 참조를 로드하는 데 사용됩니다. 대칭적으로 ISTORE, LSTORE, FSTORE, DSTORE ASTORE 명령어는 피연산자 스택에서 값을 pop하고 해당 인덱스 i로 지정된 로컬 변수에 저장합니다.

보시다시피 xLOADxSTORE 명령어가 입력됩니다 (실제로는 아래에서 볼 수 있듯이 거의 모든 명령어가 입력됩니다). 이것은 불법적인 변환이 수행되지 않도록하기 위해 사용됩니다. 실제로 지역 변수에 값을 저장 한 다음 다른 type으로 값을 로드하는 것은 불법입니다. 예를 들어, ISTORE 1 ALOAD 1 시퀀스는 불법입니다 - 이것은 임의의 메모리 주소를 로컬 변수 1에 저장하고이 주소를 객체 참조로 변환하는 것을 허용합니다! 그러나, 이 로컬 변수에 저장된 현재 값의 type과 다른 type의 값을 로컬 변수에 저장하는 것이 가장 합당합니다. 이것은 로컬 변수의 type, 즉 이 로컬 변수에 저장된 값의 type이 메소드의 실행 중에 변경 될 수 있음을 의미합니다.

위에서 말한 것처럼 다른 모든 바이트 코드 명령어는 피연산자 스택에서만 작동합니다.
다음 범주로 그룹화 할 수 있습니다 (부록 A.1 참조).

Stack
이 명령어는 스택에서 값을 조작하는 데 사용됩니다.
POP는 스택 맨 위에 pop을 팝하고, DUP는 맨 위 스택 값의 복사본을 push하고, SWAP는 두 값을 pop하고 역순으로 push합니다.

Constants
이 명령어는 피연산자 스택에 상수 값을 푸시합니다.
ACONST_NULL은 null을 푸시합니다. ICONST_0은 int 값 0을, FCONST_0은 0f를, DCONST_0은 0d를, BIPUSH는 b를, SIPUSH는 short 값을, LDC cst는 임의의 int, float, long, double, String 또는 class constant cst, 등등을 push한다.

Arithmetic and logic
이 명령어는 피연산자 스택에서 숫자 값을 가져 와서 결합하여 결과를 스택에 푸시합니다. 그들에게는 어떤 arguemnt도 없다. xADD, xSUB, xMUL, xDIVxREM은 +, -, * 및 / 또는 % 연산에 해당하며 여기서 x는 I, L, F 또는 D입니다. 마찬가지로 int 및 long 값에 대해 << <,>>>,>>>, |, & 및 ^에 해당하는 다른 명령어가 있습니다.

Casts
이 명령어는 스택에서 값을 팝하고 다른 유형으로 변환 한 다음 결과를 다시 푸시합니다. 그것들은 Java의 캐스트 표현식에 해당합니다. I2F, F2D, L2D 등은 하나의 숫자 유형에서 다른 숫자 유형으로 숫자 값을 변환합니다. CHECKCAST t는 참조 값을 type t로 변환합니다.

Objects
이 명령어는 객체를 생성하고, 잠그고, type을 테스트하는 등의 작업에 사용됩니다. 예를 들어 NEW 유형의 명령어는 스택에 새로운 유형의 객체를 푸시합니다 (유형은 내부 이름 임).

Fields
이 명령어는 필드의 값을 읽거나 씁니다. GETFIELD owner name desc는 객체 참조를 팝하고 이름 필드의 값을 푸시합니다. PUTFIELD owner name desc는 값과 객체 참조를 팝하고이 값을 이름 필드에 저장합니다. 두 경우 모두 객체는 ​​owner 유형이어야하며 필드의 형식은 desc 여야합니다. GETSTATICPUTSTATIC은 유사한 명령어이지만 정적 필드의 경우에 사용됩니다.

Methods
이 명령어는 메소드 또는 생성자를 호출합니다. 메서드 인수만큼의 값들과 대상 객체에 대한 값을 추가하여 pop하고, 메서드 호출의 결과를 푸시합니다. INVOKEVIRTUAL owner name desc는 owner 클래스에 정의 된 name 메소드를 호출하고 method descriptor가 desc 인 메소드를 호출합니다. INVOKESTATIC은 static메서드에 사용되며, INVOKESPECIAL에는 private 메서드와 생성자가 사용되고 INVOKEINTERFACE에는 인터페이스에 정의 된 메서드가 사용됩니다. 마지막으로 Java 7 클래스의 경우 INVOKEDYNAMIC이 새로운 동적 메서드 호출 메커니즘에 사용됩니다.

Arrays
이 명령어는 배열에서 값을 읽고 쓰는 데 사용됩니다. xALOAD 명령은 인덱스 및 배열을 팝하고 이 인덱스에서 배열 요소의 값을 푸시합니다. xASTORE 명령어는 값, 인덱스 및 배열을 pop하고이 값을 배열의 해당 인덱스에 저장합니다. 여기서 x는 I, L, F, D 또는 A뿐만 아니라 B, C 또는 S가 될 수 있습니다.

Jumps
이 명령어는 어떤 조건이 참이거나 무조건 조건이면 임의의 명령어로 점프합니다. 그것들은 if, for, do, while, break , continue명렴어를 컴파일하기 위해 사용된다. 예를 들어 IFEQ label 은 스택의 int 값을 팝하며 이 값이 0이면 label에 의해 설계된 명령으로 점프합니다 (그렇지 않으면 실행이 다음 명령으로 정상적으로 계속됩니다). IFNEIFGE와 같은 다른 많은 점프 명령어가 존재합니다. 마지막으로 TABLESWITCHLOOKUPSWITCH는 switch Java 명령어에 해당합니다.

Return
마지막으로 xRETURNRETURN 명령어는 메소드의 실행을 종료하고 그 결과를 호출자에게 리턴하는 데 사용됩니다. RETURN은 void를 반환하는 메서드에 사용되고 xRETURN은 다른 메서드에 사용됩니다.


3.1.3. Examples Lets look at some basic examples to get a more concrete sense of how bytecode instructions work. Consider the following bean class:


package pkg;

public class Bean {

private int f;

public int getF() {

return this.f;

}

public void setF(int f) {

this.f = f;

}

}


The bytecode of the getter method is:


ALOAD 0 

GETFIELD pkg/Bean f I 

IRETURN 


첫 번째 명령은 로컬변수 0를 읽습니다. 이 변수는 메서드 호출에 대한 프레임을 만드는 동안이 변수에 초기화되고 이 값을 피연산자 스택에 푸시합니다. 두 번째 명령은 스택에서 이 값을 팝합니다. 즉,이 객체의 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


The bytecode of the setter method is:


ALOAD 0 

ILOAD 1 

PUTFIELD pkg/Bean f I 

RETURN 


첫 번째 명령어는 이전과 마찬가지로 피연산자 스택에서 이를 푸시합니다.

두 번째 명령은이 메서드 호출을위한 프레임을 만드는 동안 f 인수 값으로 초기화 된 로컬 변수 1을 푸시합니다.

세 번째 명령은이 두 값을 팝하고 참조 된 객체의 f 필드에 int 값을 this.f로 저장합니다. 소스 코드에 내포되어 있지만 컴파일 된 코드에서 필수적인 마지막 명령은 현재 실행 프레임을 파괴하고 호출자에게 반환됩니다. 이 방법에 대한 실행 프레임의 연속 상태가 그림 3.3에 나와 있습니다.


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


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


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

RETURN


첫 번째 명령어는 이것을 피연산자 스택에 푸시합니다. 두 번째 명령은 스택에서 이 값을 팝하고 Object 클래스에 정의 된 <init> 메서드를 호출합니다. super () 호출, 즉 수퍼 클래스 인 Object의 생성자에 대한 호출에 해당합니다. 생성자는 컴파일 된 클래스와 소스 클래스에서 이름이 다르게 표시됩니다. 컴파일 된 클래스에서는 항상 <init>으로 명명되지만 소스 클래스에서는 정의 된 클래스의 이름을 갖습니다. 마지막으로 마지막 명령이 호출자에게 반환됩니다.


이제 좀 더 복잡한 setter 메서드를 살펴 보겠습니다.


public void checkAndSetF(int f) {

if (f >= 0) {

this.f = f;

} else {

throw new IllegalArgumentException();

}

}


The bytecode for this new setter method is the following:


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


첫 번째 명령은 피연산자 스택에서 f로 초기화 된 로컬 변수 1을 푸시합니다. IFLT 명령어는 스택에서 이 값을 가져와 0과 비교합니다. LT (Less Than) 0이면 레이블 레이블에 지정된 명령어로 점프합니다. 그렇지 않으면 아무 것도 수행하지 않고 실행이 다음 명령어로 계속 진행됩니다. 다음 세 명령어는 setF 메소드에서와 같은 명령어이다. GOTO 명령어는 RETURN 명령어 인 끝 레이블로 지정된 명령어로 무조건 점프합니다. 레이블과 끝 레이블 사이의 명령어는 예외를 만들고 생성합니다. NEW 명령어는 예외 객체를 만들고이를 피연산자 스택에 푸시합니다. DUP 명령은이 값을 스택에 복제합니다. INVOKESPECIAL 명령어는이 두 복사본 중 하나를 팝하고 예외 생성자를 호출합니다. 마지막으로 ATHROW 명령은 나머지 복사본을 팝하고 예외로 throw합니다 (실행이 다음 명령으로 이어지지 않도록).



3.1.4. Exception handlers

예외를 잡는 바이트 코드 명령어가 없습니다. 대신 메소드의 바이트 코드는 메소드의 주어진 부분에서 예외가 발생할 때 실행되어야 하는 코드를 지정하는 예외 핸들러 목록과 연관됩니다. 예외 핸들러는 try catch 블록과 유사합니다. 범위에는 try 블록의 내용에 해당하는 명령어 시퀀스와 catch 블록의 내용에 해당하는 핸들러가 있습니다. 범위는 시작 및 끝 레이블로 지정되고 처리기는 시작 레이블로 지정됩니다. 아래 소스 코드 예를 들면 :


public static void sleep(long d) {

try {

Thread.sleep(d);

} catch (InterruptedException e) {

e.printStackTrace();

}

}

can be compiled into:


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


trycatch 레이블 사이의 코드는 try 블록에 해당하고 catch 레이블 뒤의 코드는 catch 블록에 해당합니다. TRYCATCHBLOCK 행은 try 와 catch 레이블 사이의 범위를 처리하는 예외 핸들러를 지정하며, catch 레이블에서 시작하는 핸들러는 InterruptedException의 서브 클래스 인 예외에 대해 지정합니다. 즉, try와 catch 사이에 이러한 예외가 발생하면 스택이 지워지고 예외가이 빈 스택에 푸시되고 catch에서 실행이 계속됨을 의미합니다.


3.1.5. Frames


Java 6 이상으로 컴파일 된 클래스에는 바이트 코드 명령어 외에도 Java Virtual Machine 내부의 클래스 검증 프로세스를 가속화하는 데 사용되는 stack map frames의 set가 포함되어 있습니다. stack map frame은 실행 중 어느 시점에서 메소드의 실행 프레임 상태를 제공합니다. 좀 더 정확히 말하면, 특정 바이트 코드 명령어가 실행되기 직전에 특정 로컬 변수 슬롯과 각 피연산자 스택 슬롯에 포함 된 값의 type을  제공합니다.


예를 들어, 이전 섹션의 getF 메소드를 살펴보면 실행 프레임의 상태를 ALOAD 직전, GETFIELD 직전, IRETURN 직전에 제공하는 세 개의 stack map frame을 정의 할 수 있습니다. 이 세 개의 stack map frames은 Figure 3.2의 세 가지 경우에 해당하며 첫 번째 대괄호 사이의 유형이 로컬 변수에 해당하고 다른 유형은 피연산자 스택에 해당하는 경우 다음과 같이 설명 할 수 있습니다.


State of the execution frame before 

 Instruction

 [pkg/Bean] []

 ALOAD 0

 [pkg/Bean] [pkg/Bean]

 GETFIELD

 [pkg/Bean] [I]

 IRETURN


checkAndSetF 메소드에 대해서도 동일한 작업을 수행 할 수 있습니다.


State of the execution frame before

 Instruction

 [pkg/Bean I] []

 ILOAD 1

 [pkg/Bean I] [I]

 IFLT label

 [pkg/Bean I] []

 ALOAD 0

 [pkg/Bean I] [pkg/Bean]

 ILOAD 1

 [pkg/Bean I] [pkg/Bean I]

 PUTFIELD

 [pkg/Bean I] []

 GOTO end

 [pkg/Bean I] []

 label :

 [pkg/Bean I] []

 NEW

 [pkg/Bean I] [Uninitialized(label)]

 DUP

 [pkg/Bean I] [Uninitialized(label) Uninitialized(label)] INVOKESPECIAL
 [pkg/Bean I] [java/lang/IllegalArgumentException]

 ATHROW

 [pkg/Bean I] [] end :
 [pkg/Bean I] [] RETURN



이는 초기화되지 않은 (레이블) 유형을 제외하고 이전 메서드과 유사합니다. 이것은 stack map frame에서만 사용되며 메모리가 할당되었지만 아직 생성자가 호출되지 않은 객체를 지정하는 특수 유형입니다. 인수는 이 객체를 생성 한 명령어를 지정합니다. 이 유형의 값에서 호출 할 수있는 유일한 방법은 생성자입니다. 호출 될 때 프레임의이 유형이 모두 실제 유형 여기서는 IllegalArgumentException)으로 바뀝니다. 스택 맵 프레임은 다음과 같은 세 가지 특수 유형을 사용할 수 있습니다. UNINITIALIZED_THIS는 생성자의 로컬 변수 0의 초기 유형이고 TOP은 정의되지 않은 값에 해당하며 NULL은 null에 해당합니다.


위에서 말했듯이, Java 6부터 컴파일 된 클래스에는 bytecode, stack map frames의 set를 포함하고 있다. 저장공간을 절약하기 위해 컴파일 된 메소드에는 명령어 당 하나의 프레임이 포함되지는 않습니다. 실제로는 jump 대상이나 예외 핸들러에 해당하거나 무조건 점프 명령어를 따르는 명령어 프레임 만 포함합니다. 실제로 다른 프레임은 이 프레임에서 쉽고 빠르게 추론 할 수 있습니다. checkAndSetF 메소드의 경우 이는 두 개의 프레임 만 저장된다는 것을 의미합니다. 하나는 NEW 명령, IFLT 명령의 대상이며 또한 무조건 점프 GOTO 명령을 따르기 때문이며, 하나는 RETURN 명령인데 이는 GOTO 명령의 대상며 또한 "무조건 점프"ATHROW 명령을 따르기 때문입니다.


더 많은 공간을 절약하기 위해, 각 프레임은 이전 프레임과 비교 된 그 차이만을 저장함으로써 압축되고,초기 프레임은 메소드 매개 변수 유형에서 쉽게 추론 할 수 있기 때문에 전혀 저장되지 않습니다. checkAndSetF 메소드의 경우, 저장되어야하는 두 개의 프레임은 동일하고 초기 프레임과 동일하므로 F_SAME 니모닉에 의해 지정된 단일 바이트 값으로 저장됩니다. 이러한 프레임은 관련 바이트 코드 명령어 바로 직전에 나타낼 수 있습니다. checkAndSetF 메소드에 대한 최종 바이트 코드를 제공합니다.:


ILOAD 1

IFLT label

ALOAD 0

ILOAD 1

PUTFIELD pkg/Bean f I

GOTO end

label:

F_SAME

NEW java/lang/IllegalArgumentException

DUP

INVOKESPECIAL java/lang/IllegalArgumentException <init> ()V

ATHROW

end:

F_SAME

RETURN



참고할 내용

https://stackoverflow.com/questions/25109942/is-there-a-better-explanation-of-stack-map-frames


3.2. Interfaces and components


3.2.1. Presentation


컴파일 된 메소드를 생성하고 변환하는 ASM API는 MethodVisitor 추상 클래스 (그림 3.4 참조)에 기반한다. 이것은 ClassVisitor의 visitMethod 메소드에 의해 리턴됩니다. 어노테이션 및 디버그 정보와 관련된 일부 메소드 외에도,

다음 장에서 설명 할 이 클래스는 바이트 코드 명령어 카테고리 당 하나의 메소드를 정의하며, 그 명령어들의 인자의 수와 유형를 기준으로 합니다. (이러한 카테고리는 섹션 3.1.2에 제시된 클래스와 일치하지 않습니다) .이러한 메서드는 다음 순서로 호출해야합니다.

(MethodVisitor 인터페이스의 Javadoc에 지정된 몇 가지 추가 제약 조건이 있음).


visitAnnotationDefault? 

( visitAnnotation | visitParameterAnnotation | visitAttribute )*

( visitCode 

( visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn | visitLocalVariable | visitLineNumber )* 

visitMaxs )?

visitEnd


즉, annotaion과 attribute가 있는 경우 먼저 visit해야 하며,  이어서 non abstract method메소드의 바이트 코드가 와야합니다.

이들 메소드의 코드는 정확하게 한번만 visitCode 호출하고 visitMaxs를 한번만 호출하는 순차적인 순서로 visit해야한다.


abstract class MethodVisitor { // public accessors ommited

MethodVisitor(int api);

MethodVisitor(int api, MethodVisitor mv);

AnnotationVisitor visitAnnotationDefault();

AnnotationVisitor visitAnnotation(String desc, boolean visible);

AnnotationVisitor visitParameterAnnotation(int parameter, String desc, boolean visible);

void visitAttribute(Attribute attr);

void visitCode();

void visitFrame(int type, int nLocal, Object[] local, int nStack, Object[] stack);

void visitInsn(int opcode);

void visitIntInsn(int opcode, int operand);

void visitVarInsn(int opcode, int var);

void visitTypeInsn(int opcode, String desc);

void visitFieldInsn(int opc, String owner, String name, String desc);

void visitMethodInsn(int opc, String owner, String name, String desc);

void visitInvokeDynamicInsn(String name, String desc, Handle bsm, Object... bsmArgs);

void visitJumpInsn(int opcode, Label label);

void visitLabel(Label label);

void visitLdcInsn(Object cst);

void visitIincInsn(int var, int increment);

void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels);

void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels);

void visitMultiANewArrayInsn(String desc, int dims);

void visitTryCatchBlock(Label start, Label end, Label handler, String type);

void visitLocalVariable(String name, String desc, String signature, Label start, Label end, int index);

void visitLineNumber(int line, Label start);

void visitMaxs(int maxStack, int maxLocals);

void visitEnd();

}

Figure 3.4.: The MethodVisitor class


따라서 visitCode 및 visitMaxs 메서드를 사용하여 이벤트 시퀀스에서 메서드의 바이트 코드 시작 및 끝을 검색 할 수 있습니다.

클래스와 마찬가지로 visitEnd 메서드는 마지막에 호출되어야하며 이벤트 시퀀스에서 메서드의 끝을 감지하는 데 사용됩니다.


완전한 클래스를 생성하기 위해서, ClassVisitor 클래스와 MethodVisitor 클래스를 조합 할 수 있습니다.


ClassVisitor cv = ...;

cv.visit(...);

MethodVisitor mv1 = cv.visitMethod(..., "m1", ...);

mv1.visitCode();

mv1.visitInsn(...);

...

mv1.visitMaxs(...);

mv1.visitEnd();

MethodVisitor mv2 = cv.visitMethod(..., "m2", ...);

mv2.visitCode();

mv2.visitInsn(...);

...

mv2.visitMaxs(...);

mv2.visitEnd();

cv.visitEnd();



또 다른 visit하기 시작하기 위해 꼭 메소드를 마칠 필요는 없습니다. 사실 MethodVisitor 인스턴스는 완전히 독립적이며 어떤 순서로든 사용할 수 있습니다 (cv.visitEnd ()가 호출되지 않은 한).


ClassVisitor cv = ...;

cv.visit(...);

MethodVisitor mv1 = cv.visitMethod(..., "m1", ...);

mv1.visitCode();

mv1.visitInsn(...);

...

MethodVisitor mv2 = cv.visitMethod(..., "m2", ...);

mv2.visitCode();

mv2.visitInsn(...);

...

mv1.visitMaxs(...);

mv1.visitEnd();

...

mv2.visitMaxs(...);

mv2.visitEnd();

cv.visitEnd();


ASM은 메소드를 생성하고 변환하는 MethodVisitor API를 기반으로 세 가지 핵심 컴포넌트를 제공합니다.


• ClassReader 클래스는 컴파일 된 메서드의 내용을 구문 분석하고 인수로 전달 된 ClassVisitor에 의해 반환 된 MethodVisitor 객체의 해당 메서드를 accept 메서드에 호출합니다.


• ClassWriter의 visitMethod 메소드는 컴파일 된 메소드를 직접 바이너리 형식으로 빌드하는 MethodVisitor 인터페이스의 구현을 반환합니다.


• MethodVisitor 클래스는 수신 한 모든 메소드 호출을 다른 MethodVisitor 인스턴스에 위임합니다. 이벤트 필터로 볼 수 있습니다.


ClassWriter options


3.1.5 절에서 보았 듯이, 메서드에 대한 스택 맵 프레임을 계산하는 것은 그리 쉽지 않습니다.

당신은 모든 프레임을 계산해야합니다 . 점프 대상에 해당하거나 무조건 점프를 따르는 프레임을 찾은 다음 마지막으로 나머지 프레임을 압축합니다.  마찬가지로 메소드의 로컬 변수와 피연산자 스택 부분의 크기를 계산하는 것이 더 쉽지만, 그 역시 그리 쉽지는 않습니다. 


다행히도 ASM이이를 계산할 수 있습니다. ClassWriter를 만들 때 자동으로 계산해야하는 것을 지정할 수 있습니다.


new ClassWriter (0)에서는 아무 것도 계산되지 않습니다. 프레임과 로컬 변수 및 피연산자 스택 크기를 직접 계산해야합니다.


new ClassWriter (ClassWriter.COMPUTE_MAXS)를 사용하면 로컬 변수와 피연산자 스택 부분의 크기가 계산됩니다. 여전히 visitMaxs를 호출해야하지만 argument를 사용할 수 있습니다.이 인수는 무시되고 다시 계산됩니다. 이 옵션을 사용하면 여전히 프레임을 스스로 계산해야합니다.


new ClassWriter (ClassWriter.COMPUTE_FRAMES)를 사용하면 모든 것이 자동으로 계산됩니다. visitFrame을 호출 할 필요는 없지만 visitMaxs를 호출해야합니다 (argument는 무시되고 다시 계산됩니다).


이러한 옵션을 사용하는 것이 편리하지만 비용이 많이 듭니다. COMPUTE_MAXS 옵션을 사용하면 ClassWriter가 10 % 더 늦어지며, COMPUTE_FRAMES 옵션을 사용하면 속도가 두 배 느려집니다. 이것은 자신이 계산하는데 걸리는 시간과 비교되어야합니다. 모든 경우를 처리해야하는 ASM에서 사용되는 알고리즘에 비해 특정 상황에서 종종 더 쉽고 빠르게 알고리즘으로 계산할 수 있습니다.


프레임을 직접 계산하도록 선택한 경우, ClassWriter 클래스가 압축 단계를 수행하도록 할 수 있습니다. 이를 위해서는 visitFrame (F_NEW, nLocals, locals, nStack, stack)을 사용하여 압축되지 않은 프레임을 visit하기 만하면 됩니다. 여기서 nLocals 및 nStack은 local 과 피연산자 스택 크기이며, local 및 stack은 해당 type이 들어있는 배열입니다 (자세한 내용은 Javadoc을 참조하십시오).


또한 프레임을 자동으로 계산하기 위해, 때때로 주어진 두 클래스의 공통 수퍼 클래스를 계산하는 데 필요합니다. 기본적으로 ClassWriter 클래스는 getCommonSuperClass 메소드에서 이를 계산하고, 두 클래스를 JVM에 로드하고 리플렉션 API를 사용합니다. 서로를 참조하는 여러 클래스를 참조 된 클래스가 아직 없기 때문에 생성하는 경우 문제가 될 수 있습니다. 이 경우 getCommonSuperClass 메소드를 override하여이 문제점을 해결할 수 있습니다.


3.2.2. Generating methods


3.1.3 절에 정의 된 getF 메소드의 바이트 코드는 mv가 MethodVisitor 인 경우 다음 메소드 호출을 사용하여 생성 할 수 있습니다.


mv.visitCode();

mv.visitVarInsn(ALOAD, 0);

mv.visitFieldInsn(GETFIELD, "pkg/Bean", "f", "I");

mv.visitInsn(IRETURN);

mv.visitMaxs(1, 1);

mv.visitEnd();


첫 번째 호출은 bytecode 생성을 시작합니다. 그 다음에는 이 메소드의 세 가지 명령어를 생성하는 세 번의 호출이 이어집니다 (bytecode와 ASM API 간의 매핑은 매우 간단합니다). visitMaxs는 모든 visit가 완료된 명령어가 수행되고 나서 호출해야 합니다. 이것은 메소드의 실행 프레임에 대한 로컬 변수 및 피연산자 스택 부분의 크기를 정의하는 데 사용됩니다. 3.1.3 절에서 보았 듯이 이 크기는 각 부분에 대해 1 슬롯입니다. 마지막으로 메소드의 생성을 끝내기 위해 마지막 호출이 사용됩니다.

setF 메소드와 생성자의 바이트 코드는 비슷한 방법으로 생성 될 수있다. 보다 흥미로운 예제는 checkAndSetF 메소드입니다.

mv.visitCode();
mv.visitVarInsn(ILOAD, 1);
Label label = new Label();
mv.visitJumpInsn(IFLT, label);
mv.visitVarInsn(ALOAD, 0);
mv.visitVarInsn(ILOAD, 1);
mv.visitFieldInsn(PUTFIELD, "pkg/Bean", "f", "I");
Label end = new Label();
mv.visitJumpInsn(GOTO, end);
mv.visitLabel(label);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitTypeInsn(NEW, "java/lang/IllegalArgumentException");
mv.visitInsn(DUP);
mv.visitMethodInsn(INVOKESPECIAL,
"java/lang/IllegalArgumentException", "<init>", "()V");
mv.visitInsn(ATHROW);
mv.visitLabel(end);
mv.visitFrame(F_SAME, 0, null, 0, null);
mv.visitInsn(RETURN);
mv.visitMaxs(2, 2);
mv.visitEnd();

visitCode 호출과 visitEnd 호출 사이에서 섹션 3.1.5의 끝에 표시된 바이트 코드와 정확하게 매핑되는 메서드 호출을 볼 수 있습니다.
명령, 레이블 또는 프레임 당 하나의 호출 (유일한 예외는 레이블 및 종료 레이블 객체의 선언 및 구성입니다).

참고 : Label 객체는이 레이블에 대한 visitLabel 호출 뒤에 오는 명령어를 지정합니다. 예를 들어, end는 지시가 아니기 때문에 RETURN 명령어를 지정하고 직후에 방문한 프레임은 지정하지 않습니다. 동일한 명령어를 지정하는 레이블이 여러 개있는 것은 당연한 일이지만 레이블은 정확히 하나의 명령어 만 지정해야합니다. 즉, 서로 다른 레이블로 visitLabel을 연속적으로 호출 할 수는 있지만 명령에 사용 된 레이블은 visitLabel을 사용하여 정확히 한 번만 방문해야합니다. 마지막 제약 조건은 라벨을 공유 할 수 없다는 것입니다. 각 메소드는 자체 라벨을 가져야합니다.


3.2.3. Transforming methods

이제 메서드도 클래스처럼 변형 될 수 있다고 추측 했어야합니다. 즉, 메서드 호출을 전달하는 메서드 어댑터를 사용하여 일부 수정을 통해 메서드 호출을 전달할 수 있습니다. 변경 인수는 개별 명령어를 변경하는 데 사용할 수 있으며 수신 된 호출을 전달하지 않고 명령어를 제거하고 수신 된 호출간에 호출을 삽입하면 새로운 명령어가 추가됩니다. MethodVisitor 클래스는 그러한 메소드 어댑터의 기본 구현을 제공합니다.이 어댑터는 수신 한 모든 메소드 호출을 전달하는 것 외에는 아무것도 수행하지 않습니다. 

메소드 어댑터가 사용되는 방법을 이해하기 위해 메소드 내부에서 NOP 명령어를 제거하는 매우 간단한 어댑터를 고려해 봅시다. 아무 것도하지 않기 때문에 문제없이 제거 할 수 있습니다.

public class RemoveNopAdapter extends MethodVisitor {

public RemoveNopAdapter(MethodVisitor mv) {

super(ASM4, mv);

}

@Override

public void visitInsn(int opcode) {

if (opcode != NOP) {

mv.visitInsn(opcode);

}

}

}


This adapter can be used inside a class adapter as follows:


public class RemoveNopClassAdapter extends ClassVisitor {

public RemoveNopClassAdapter(ClassVisitor cv) {

super(ASM4, cv);

}

@Override

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {

MethodVisitor mv;

mv = cv.visitMethod(access, name, desc, signature, exceptions);

if (mv != null) {

mv = new RemoveNopAdapter(mv);

}

return mv;

}

}


즉, 클래스 어댑터는 체인의 다음 class visitor가 반환 한 메서드 어댑터(method visitor를 캡슐화한)를 빌드하고 이 어댑터를 반환합니다. 그 효과는 클래스 어댑터 체인과 유사한 메소드 어댑터 체인의 생성입니다 (그림 3.5 참조).


Figure 3.5 : Sequence diagram for the RemoveNoAdapter


그러나 필수 사항은 아닙니다: 클래스 어댑터 체인과 유사하지 않은 메소드 어댑터 체인을 빌드하는 것이 완벽하게 가능합니다. 각 메소드는 다른 메소드 어댑터 체인을 가질 수 있습니다. 예를 들어, 클래스 어댑터는 생성자가 아닌 메소드에서만 NOP를 제거하도록 선택할 수 있습니다.

이 작업은 다음과 같이 수행 할 수 있습니다.


...

mv = cv.visitMethod(access, name, desc, signature, exceptions);

if (mv != null && !name.equals("<init>")) {

mv = new RemoveNopAdapter(mv);

}

...

이 경우 어댑터 체인은 생성자에 대해 더 짧습니다. 반대로, 생성자 용 어댑터 체인은 visitMethod 내부에 함께 연결된 여러 메소드 어댑터와 함께 더 길 수있었습니다. 메소드 어댑터 체인은 클래스 어댑터 체인과 다른 토폴로지를 가질 수도 있습니다. 예를 들어, 클래스 어댑터 체인은 선형 일 수 있지만, 메소드 어댑터 체인에 branch를 가집니다.

public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { 
MethodVisitor mv1, mv2;
mv1 = cv.visitMethod(access, name, desc, signature, exceptions);
mv2 = cv.visitMethod(access, "_" + name, desc, signature, exceptions);
return new MultiMethodAdapter(mv1, mv2); 
}

이제 메소드 어댑터의 사용과 클래스 어댑터내부에서 결합하는 방법을 살펴 보았습니다. 이제 RemoveNopAdapter보다 더 흥미로운 어댑터를 구현하는 방법을 살펴 보겠습니다.

3.2.4. Stateless transformations

프로그램의 각 클래스에서 소비 한 시간을 측정한다고 가정 해 봅시다.
각 클래스에 정적 타이머 필드를 추가해야하며 이 클래스의 각 메서드 실행 시간을이 타이머 필드에 추가해야합니다.
다시 말하면 우리는 C를 변환하려합니다.

public class C {
public void m() throws Exception {
Thread.sleep(100);
}
}


이걸로


public class C {

public static long timer;

public void m() throws Exception {

timer -= System.currentTimeMillis();

Thread.sleep(100);

timer += System.currentTimeMillis();

}

}


이것이 ASM에서 어떻게 구현 될 수 있는지에 대한 아이디어를 얻기 위해, 이 두 클래스를 컴파일하고이 두 버전 (기본 Textifier 백엔드 또는 ASMifier 백엔드)에서 TraceClassVisitor의 출력을 비교할 수 있습니다. 기본 백엔드에서는 다음과 같은 차이점이 있습니다 (굵게 표시).


GETSTATIC C.timer : J

INVOKESTATIC java/lang/System.currentTimeMillis()J

LSUB

PUTSTATIC C.timer : J

LDC 100

INVOKESTATIC java/lang/Thread.sleep(J)V

GETSTATIC C.timer : J

INVOKESTATIC java/lang/System.currentTimeMillis()J

LADD

PUTSTATIC C.timer : J

RETURN

MAXSTACK = 4

MAXLOCALS = 1


우리는 메서드의 시작 부분에 4 개의 명령와 리턴되는 명령어 앞에 4 개의 다른 명령어를 추가해야한다는 것을 알 수 있습니다. 또한 최대 operan stack 크기를 업데이트해야 합니다. 메소드 코드의 시작 부분은 visitCode 메소드를 사용하여 visit합니다. 따라서 메소드 어댑터에서 이 메소드를 대체하여 처음 네 명령어를 추가 할 수 있습니다.


public void visitCode() {

mv.visitCode();

mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");

mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");

mv.visitInsn(LSUB);

mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");

}


여기서 소유자는 변형 될 클래스의 이름으로 설정되어야합니다.

이제 RETURN 이전에 네 가지 다른 명령어를 추가해야합니다. xRETURN 이전 또는 ATHROW 앞에는 메소드 실행을 종료하는 모든 명령어가 추가되어야합니다. 이 명령어에는 인수가 없으므로 visitInsn 메서드에서 방문합니다. 그런 다음 명령어을 추가하기 위해이 메소드를 override 할 수 있습니다.



public void visitInsn(int opcode) {

if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {

mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");

mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J");

mv.visitInsn(LADD);

mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");

}

mv.visitInsn(opcode);

}


마지막으로 최대 피연산자 스택 크기를 업데이트해야합니다. 추가하는 명령어는 두 개의 long 값을 푸시하므로 피연산자 스택에 네 개의 슬롯이 필요합니다. 메소드의 시작 부분에서 피연산자 스택은 처음에 비어 있으므로 처음에 추가 된 4 개의 명령어에는 크기 4의 스택이 필요합니다. 또한 삽입 된 코드는 스택 상태를 변경되지 않은 상태로 유지한다는 것을 알고 있습니다 (밀어 넣을만큼 많은 값을 표시하기 때문에).

결과적으로, 원래 코드가 크기 s의 스택을 필요로한다면, 변환 된 메소드에 필요한 최대 스택 크기는 max (4, s)입니다.

불행하게도 우리는 return 명령어 앞에 네 개의 명령어를 추가합니다. 여기서는 이러한 명령어 바로 앞에있는 피연산자 스택의 크기를 알지 못합니다. 우리는 그것이 s보다 작거나 같다는 것을 압니다. 결과적으로 리턴 명령어 전에 추가 된 코드에는 s + 4까지의 크기의 피연산자 스택이 필요할 수 있다고 말할 수 있습니다.


이 최악의 시나리오는 실제로는 거의 발생하지 않습니다. 일반적인 컴파일러에서는 RETURN 앞에 피연산자 스택이 반환 값, 즉 최대 0, 1 또는 2의 크기를 포함합니다.그러나 가능한 모든 경우를 처리하기를 원한다면 최악의 시나리오 2를 사용해야합니다.visitMaxs 메서드를 다음과 같이 재정의해야합니다.


public void visitMaxs(int maxStack, int maxLocals) {

mv.visitMaxs(maxStack + 4, maxLocals);

}


물론 최대 스택 크기에 신경 쓰지 않고 COMPUTE_MAXS 옵션을 사용하여 최적의 값을 계산하고 최악의 값을 계산하지 않을 수도 있습니다.

그러나 이러한 단순한 변환의 경우 maxStack을 수동으로 업데이트하는 데 많은 비용이 들지 않습니다.


이제 흥미로운 질문은 :

스택 맵 프레임은? 원래의 코드는 프레임을 포함하지 않았으며, 변형 된 것도 포함하지 않았습니다. 그러나 이것은 우리가 예제로 사용한 특정 코드로 인한 것입니까? 프레임을 업데이트해야하는 경우가 있습니까?

대답은 '아니오'입니다.

1) 삽입 된 코드는 피연산자 스택을 변경하지 않고 남겨 둡니다.

2) 삽입 된 코드는 점프 명령을 포함하지 않으며

3) 원래 코드의 점프 명령 또는 더 형식적으로는 제어 흐름 그래프가 수정되지 않습니다.

이는 원래 프레임이 변경되지 않으며 삽입 된 코드에 대해 새 프레임을 저장해야하므로 압축 된 원래 프레임도 변경되지 않음을 의미합니다.


이제 모든 요소를 연관된 ClassVisitor 및 MethodVisitor 하위 클래스에 함께 넣을 수 있습니다.


public class AddTimerAdapter extends ClassVisitor {

private String owner;

private boolean isInterface;

public AddTimerAdapter(ClassVisitor cv) {

super(ASM4, cv);

}

@Override public void visit(int version, int access, String name,

String signature, String superName, String[] interfaces) {

cv.visit(version, access, name, signature, superName, interfaces);

owner = name;

isInterface = (access & ACC_INTERFACE) != 0;

}

@Override public MethodVisitor visitMethod(int access, String name,

String desc, String signature, String[] exceptions) {

MethodVisitor mv = cv.visitMethod(access, name, desc, signature,

exceptions);

if (!isInterface && mv != null && !name.equals("<init>")) {

mv = new AddTimerMethodAdapter(mv);

}

return mv;

}

@Override public void visitEnd() {

if (!isInterface) {

FieldVisitor fv = cv.visitField(ACC_PUBLIC + ACC_STATIC, "timer",

"J", null, null);

if (fv != null) {

fv.visitEnd();

}

}

cv.visitEnd();

}

class AddTimerMethodAdapter extends MethodVisitor {

public AddTimerMethodAdapter(MethodVisitor mv) {

super(ASM4, mv);

}

@Override public void visitCode() {

mv.visitCode();

mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");

mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",

"currentTimeMillis", "()J");

mv.visitInsn(LSUB);

mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");

}

@Override public void visitInsn(int opcode) {

if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {

mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");

mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",

"currentTimeMillis", "()J");

mv.visitInsn(LADD);

mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");

}

mv.visitInsn(opcode);

}

@Override public void visitMaxs(int maxStack, int maxLocals) {

mv.visitMaxs(maxStack + 4, maxLocals);

}

}

}


클래스 어댑터는 메소드 어댑터 (생성자 제외)를 인스턴스화하는 데 사용되고, 타이머 필드를 추가하고 .메소드 어댑터에서 액세스 할 수있는 필드를 변환하여 이름을 저장합니다.


3.2.5. Statefull transformations


이전 섹션에서 보았던 변환은 지역적이며 현재 명령어 이전에 방문한 명령어에 의존하지 않습니다.


처음에 추가 된 코드는 항상 동일하며 항상 추가되며, 각 RETURN 명령 앞에 삽입 된 코드에 대해서도 마찬가지입니다.

이러한 변환을 상태 비 저장 변환이라고합니다. 구현하기는 간단하지만 가장 간단한 변환 만이 속성을 확인합니다.


보다 복잡한 변환을 위해서는 현재 상태 이전에 visit한 명령어에 대한 일부 상태를 기억해야합니다. 예를 들어 ICONST_0 IADD 시퀀스의 모든 발생을 제거하는 변환은 그의 빈 효과는 0을 더하는 것입니다.


IADD 명령을 방문하면 마지막으로 방문한 명령이 ICONST_0 인 경우에만 명령을 제거해야합니다.

이를 위해서는 메소드 어댑터 내부에 상태를 저장해야합니다. 이러한 이유로 이러한 변환을 상태 기반 변환이라고합니다.


이 예제에서 더 자세히 살펴 보겠습니다. ICONST_0을 방문하면 다음 명령어가 IADD 인 경우에만 ICONST_0을 제거해야합니다. 문제는 다음 명령어가 아직 알려지지 않았다는 것입니다. 해결 방법은이 결정을 다음 명령으로 연기하는 것입니다. IADD 인 경우 두 명령을 모두 제거하고 그렇지 않으면 ICONST_0 및 현재 명령을 내 보냅니다. 일부 명령 시퀀스를 제거하거나 대체하는 변환을 구현하려면, visitXxx Insn 메소드가 일반적인 visitInsn () 메소드를 호출하는 MethodVisitor 서브 클래스를 도입하는 것이 편리합니다.


public abstract class PatternMethodAdapter extends MethodVisitor {

protected final static int SEEN_NOTHING = 0;

protected int state;

public PatternMethodAdapter(int api, MethodVisitor mv) {

super(api, mv);

}

@Overrid public void visitInsn(int opcode) {

visitInsn();

mv.visitInsn(opcode);

}

@Override public void visitIntInsn(int opcode, int operand) {

visitInsn();

mv.visitIntInsn(opcode, operand);

}

...

protected abstract void visitInsn();

}


Then the above transformation can be implemented like this:


public class RemoveAddZeroAdapter extends PatternMethodAdapter {

private static int SEEN_ICONST_0 = 1;

public RemoveAddZeroAdapter(MethodVisitor mv) {

super(ASM4, mv);

}

@Override public void visitInsn(int opcode) {

if (state == SEEN_ICONST_0) {

if (opcode == IADD) {

state = SEEN_NOTHING;

return;

}

}

visitInsn();

if (opcode == ICONST_0) {

state = SEEN_ICONST_0;

return;

}

mv.visitInsn(opcode);

}

@Override protected void visitInsn() {

if (state == SEEN_ICONST_0) {

mv.visitInsn(ICONST_0);

}

state = SEEN_NOTHING;

}

}


visitInsn (int) 메서드는 먼저 시퀀스가 검색되었는지 테스트합니다.

이 경우 상태를 다시 초기화하고 즉시 반환하므로 시퀀스를 제거하는 효과가 있습니다.

다른 경우에는 일반적인 visitInsn 메소드를 호출합니다.이 메소드는 마지막으로 방문한 명령어 인 경우 ICONST_0을 내 보냅니다.

그리고, 현재의 명령이 ICONST_0이면, 이 명령에 대한 결정을 연기하기 위해 이 사실을 기억하고 반환합니다. 다른 모든 경우에는 현재 명령어가 다음 방문객에게 전달됩니다.


Labels and frames


이전 섹션에서 보았 듯이 레이블과 프레임은 연관된 지시문 바로 전에 방문합니다.

다른 말로하면 지시 사항과 동일한 시간에 방문하지만 지침 자체는 아닙니다. 이것은 명령 시퀀스를 감지하는 변환에 영향을 미치지 만 실제로이 영향은 장점입니다. 실제로 우리가 제거한 지침 중 하나가 점프 지침의 대상이라면 어떻게됩니까? 어떤 명령어가 ICONST_0으로 점프 할 수 있다면, 이는이 명령어를 지정하는 라벨이 있음을 의미합니다.

두 명령어를 제거한 후에이 레이블은 제거 된 IADD 다음에 오는 명령어를 지정합니다.

그것은 우리가 원하는 것입니다. 그러나 어떤 지시가 IADD로 점프 할 수 있다면,

우리는 명령 순서를 제거 할 수 없습니다 (이 점프 전에 0이 스택에 푸시되었음을 확신 할 수 없습니다). ICONST_0과 IADD 사이에는 쉽게 감지 할 수있는 레이블이 있어야합니다. 추론은 스택 맵 프레임에서도 동일합니다. 스택 맵 프레임이 두 명령어 사이에서 방문되면이를 제거 할 수 없습니다. 두 경우 모두 레이블 및 프레임을 패턴 일치 알고리즘의 지침으로 간주하여 처리 할 수 ​​있습니다. PatternMethodAdapter에서이 작업을 수행 할 수 있습니다 (visitMaxs는 일반적인 visitInsn 메서드도 호출하므로 메서드의 끝 부분을 검색해야하는 시퀀스의 접두사로 처리하는 데 사용됩니다).


public abstract class PatternMethodAdapter extends MethodVisitor {

...

@Override public void visitFrame(int type, int nLocal, Object[] local,

int nStack, Object[] stack) {

visitInsn();

mv.visitFrame(type, nLocal, local, nStack, stack);

}

@Override public void visitLabel(Label label) {

visitInsn();

mv.visitLabel(label);

}

@Override public void visitMaxs(int maxStack, int maxLocals) {

visitInsn();

mv.visitMaxs(maxStack, maxLocals);

}

}


다음 장에서 보게 될 것처럼, 컴파일 된 메소드는 예를 들어 예외 스택 추적에 사용되는 소스 파일 행 번호에 대한 정보를 포함 할 수 있습니다. 이 정보는 visitLineNumber 메소드로 방문하며 지침과 동시에 호출됩니다. 그러나 명령 시퀀스의 중간에있는 행 번호의 존재는 이를 변환하거나 제거 할 가능성에 아무런 영향을 미치지 않습니다. 해결책은 패턴 일치 알고리즘에서 완전히 무시하는 것입니다


A more complex example


이전 예제는보다 복잡한 명령 시퀀스로 쉽게 일반화 할 수 있습니다.

예를 들어, 일반적으로 f = f와 같은 오타로 인해 자체 필드 할당을 제거하는 변환을 고려하십시오. 또는 바이트 코드에서 ALOAD 0 ALOAD 0 GETFIELD f PUTFIELD f.


이 변환을 구현하기 전에 상태 시스템이이 시퀀스를 인식하도록 설계하는 것이 바람직합니다 (그림 3.6 참조).


각 변환에는 조건 (현재 명령어의 값)과 작업 (굵게 표시된 명령어 시퀀스)이 표시됩니다. 예를 들어 현재 명령이 ALOAD 0이 아닌 경우 S1에서 S0 로의 전환이 발생합니다. 이 경우, 이 상태에 도달하기 위해 방문한 ALOAD 0이 방출됩니다. S2에서 자체로의 전이에 주의하십시오. 이것은 3 개 이상의 연속적인 ALOAD 0이 발견 될 때 발생합니다. 이 경우 두 개의 ALOAD 0을 방문한 상태에서 머물며 세 번째를 내 보냅니다. state machine이 발견되면 해당 메소드 어댑터 작성은 간단합니다 ((8 스위치 케이스는 다이어그램의 8 전환에 해당합니다).


Figure 3.6.: State machine for ALOAD 0 ALOAD 0 GETFIELD f PUTFIELD f



class RemoveGetFieldPutFieldAdapter extends PatternMethodAdapter {

private final static int SEEN_ALOAD_0 = 1;

private final static int SEEN_ALOAD_0ALOAD_0 = 2;

private final static int SEEN_ALOAD_0ALOAD_0GETFIELD = 3;

private String fieldOwner;

private String fieldName;

private String fieldDesc;

public RemoveGetFieldPutFieldAdapter(MethodVisitor mv) {

super(mv);

}

@Override

public void visitVarInsn(int opcode, int var) {

switch (state) {

case SEEN_NOTHING: // S0 -> S1

if (opcode == ALOAD && var == 0) {

state = SEEN_ALOAD_0;

return;

}

break;

case SEEN_ALOAD_0: // S1 -> S2

if (opcode == ALOAD && var == 0) {

state = SEEN_ALOAD_0ALOAD_0;

return;

}

break;

case SEEN_ALOAD_0ALOAD_0: // S2 -> S2

if (opcode == ALOAD && var == 0) {

mv.visitVarInsn(ALOAD, 0);

return;

}

break;

}

visitInsn();

mv.visitVarInsn(opcode, var);

}

@Override

public void visitFieldInsn(int opcode, String owner, String name, String desc) {

switch (state) {

case SEEN_ALOAD_0ALOAD_0: // S2 -> S3

if (opcode == GETFIELD) {

state = SEEN_ALOAD_0ALOAD_0GETFIELD;

fieldOwner = owner;

fieldName = name;

fieldDesc = desc;

return;

}

break;

case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0

if (opcode == PUTFIELD && name.equals(fieldName)) {

state = SEEN_NOTHING;

return;

}

break;

}

visitInsn();

mv.visitFieldInsn(opcode, owner, name, desc);

}

@Override protected void visitInsn() {

switch (state) {

case SEEN_ALOAD_0: // S1 -> S0

mv.visitVarInsn(ALOAD, 0);

break;

case SEEN_ALOAD_0ALOAD_0: // S2 -> S0

mv.visitVarInsn(ALOAD, 0);

mv.visitVarInsn(ALOAD, 0);

break;

case SEEN_ALOAD_0ALOAD_0GETFIELD: // S3 -> S0

mv.visitVarInsn(ALOAD, 0);

mv.visitVarInsn(ALOAD, 0);

mv.visitFieldInsn(GETFIELD, fieldOwner, fieldName, fieldDesc);

break;

}

state = SEEN_NOTHING;

}

}


섹션 3.2.4의 AddTimerAdapter 케이스와 같은 이유로, 이 섹션에 제시된 상태 기반 변환은 스택 맵 프레임을 변환 할 필요가 없습니다. 원래 프레임은 변환 후에도 유효합니다. 로컬 변수와 피연산자 스택 크기를 변환 할 필요조차 없습니다. 마지막으로 statefull 변환은 명령 시퀀스를 탐지하고 변환하는 변환에 국한되지 않음에 유의해야합니다. 다른 유형의 변형도 statefull입니다.

예를 들어, 다음 섹션에 제시된 메소드 어댑터의 경우입니다.




3.3. Tools


org.objectweb.asm.commons 패키지에는 사용자 정의 어댑터를 정의하는 데 유용한 미리 정의 된 메소드 어댑터가 포함되어 있습니다. 이 섹션에서는 세 가지를 제시하고 섹션 3.2.4의 AddTimerAdapter 예제와 함께 어떻게 사용할 수 있는지 보여줍니다. 또한 이전 장에서 본 도구를 사용하여 메서드 생성 또는 변형을 쉽게 수행 할 수있는 방법을 보여줍니다.


3.3.1. Basic tools

섹션 2.3에 제시된 도구는 메소드에도 사용할 수 있습니다.


Type

xLOAD, xADD 또는 xRETURN과 같은 많은 바이트 코드 명령어는 적용되는 type에 따라 다릅니다. Type 클래스는 getOpcode 메소드를 제공합니다.이 메소드는이 명령어에 대해 주어진 유형에 해당하는 opcode를 가져 오는 데 사용할 수 있습니다. 이 메서드는 int 형식의 opcode를 매개 변수로 사용하고 해당 형식의 opcode를 반환합니다. 예를 들어, t.getOpcode (IMUL)는 t가 Type.FLOAT_TYPE과 같으면 FMUL을 반환합니다.


TraceClassVisitor

이전 장에서 이미 제시한 이 클래스는 visit하는 클래스의 텍스트 표현을 인쇄하고, 그들의 방법의 텍스트 표현을 포함하여,이 장에서 사용 된 것과 매우 유사한 형태로 나타납니다. 따라서 변환 체인의 어느 지점에서 생성되거나 변환 된 메서드의 내용을 추적하는 데 사용할 수 있습니다.

예 :


java -classpath asm.jar:asm-util.jar \

org.objectweb.asm.util.TraceClassVisitor \

java.lang.Void


prints:


// class version 49.0 (49)

// access flags 49

public final class java/lang/Void {

// access flags 25

// signature Ljava/lang/Class<Ljava/lang/Void;>;

// declaration: java.lang.Class<java.lang.Void>

public final static Ljava/lang/Class; TYPE

// access flags 2

private <init>()V

ALOAD 0

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

RETURN

MAXSTACK = 1

MAXLOCALS = 1

// access flags 8

static <clinit>()V

LDC "void"

INVOKESTATIC java/lang/Class.getPrimitiveClass (...)...

PUTSTATIC java/lang/Void.TYPE : Ljava/lang/Class;

RETURN

MAXSTACK = 1

MAXLOCALS = 0

}


이것은 정적 블록 static {...}을 생성하는 방법, 즉 <clinit> 메소드 (CLass INITializer의 경우)를 생성하는 방법을 보여줍니다.
체인의 모든 부분을 추적하는 대신 체인의 특정 지점에서 단일 메서드의 내용을 추적하려는 경우 TraceClassVisitor 대신 TraceMethodVisitor를 사용할 수 있습니다. (이 경우에는 백엔드를 명시 적으로 지정해야하며 여기서 Textifier를 사용합니다.)

public MethodVisitor visitMethod(int access, String name,String desc, String signature, String[] exceptions) {
MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);
if (debug && mv != null && ...) { // if this method must be traced
Printer p = new Textifier(ASM4) {
@Override public void visitMethodEnd() {
print(aPrintWriter); // print it after it has been visited
}
};
mv = new TraceMethodVisitor(mv, p);
}
return new MyMethodAdapter(mv);
}

이 코드는 MyMethodAdapter로 변환 한 후 메서드를 인쇄합니다.


CheckClassAdapter


이 장은 이전 장에서 이미 제시되었지만,

ClassVisitor 메소드가 적절한 순서로 호출되고 유효한 인수로 호출되는지 확인하며 MethodVisitor 메소드에 대해서도 동일하게 수행합니다.

따라서 변환 체인의 어느 지점에서나 MethodVisitor API가 올바르게 사용되었는지 확인할 수 있습니다.

TraceMethodVisitor와 마찬가지로 CheckMethodAdapter 클래스를 사용하여 모든 클래스를 검사하는 대신 단일 메서드를 검사 할 수 있습니다.


public MethodVisitor visitMethod(int access, String name,String desc, String signature, String[] exceptions) {

MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);

if (debug && mv != null && ...) { // if this method must be checked

mv = new CheckMethodAdapter(mv);

}

return new MyMethodAdapter(mv);

}


이 코드는 MyMethodAdapter가 MethodVisitor API를 올바르게 사용하는지 확인합니다. 그러나, 이 어댑터는 바이트 코드가 올바른지 확인하지 않습니다. 예를 들어 ISTORE 1 ALOAD 1이 유효하지 않음을 감지하지 못합니다. 실제로 CheckMethodAdapter (Javadoc 참조)의 다른 생성자를 사용하고 visitMaxs에 유효한 maxStack 및 maxLocals 인수를 제공하면 이러한 종류의 오류가 감지 될 수 있습니다.


ASMifier


이전 장에서 이미 설명한이 클래스는 메서드 내용과 함께 작동합니다. 이것은 ASM을 사용하여 컴파일 된 코드를 생성하는 방법을 알기 위해 사용할 수 있습니다. Java에서 해당 소스 코드를 작성하고 javac로 컴파일 한 다음 ASMifier를 사용하여이 클래스를 방문하십시오. 소스 코드에 해당하는 바이트 코드를 생성하는 ASM 코드가 생성됩니다.




3.3.2. AnalyzerAdapter


이 메소드 어댑터는 visitFrame에서 방문한 프레임을 기반으로 각 명령어 앞에 스택 맵 프레임을 계산합니다. 사실, 3.1.5 절에서 설명한 것처럼 visitFrame은 공간을 절약하기 위해 메소드의 일부 특정 지침보다 먼저 호출되기 때문에 "다른 프레임은이 프레임에서 쉽고 빠르게 추정 할 수 있습니다". 이것은 이 어댑터가하는 것입니다. 물론 사전 계산 된 스택 맵 프레임을 포함하는 클래스, 즉 Java 6 이상으로 컴파일 된 (또는 이전에 COMPUTE_FRAMES 옵션을 사용하여 ASM 어댑터가있는 Java 6으로 업그레이드 된) 경우에만 작동합니다.

 

AddTimerAdapter 예제의 경우이 어댑터를 사용하여 RETURN 명령어 바로 앞에있는 피연산자 스택의 크기를 가져올 수 있으므로 visitMax에서 maxStack에 대한 최적의 변환 값을 계산할 수 있습니다 (실제로이 방법은 실제로 권장되지 않습니다. 왜냐하면 COMPUTE_MAXS를 사용하는 것보다 훨씬 효율적이지 않기 때문입니다.)


class AddTimerMethodAdapter2 extends AnalyzerAdapter {

private int maxStack;

public AddTimerMethodAdapter2(String owner, int access, String name, String desc, MethodVisitor mv) {

super(ASM4, owner, access, name, desc, mv);

}

@Override

public void visitCode() {

super.visitCode();

mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");

mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",

"currentTimeMillis", "()J");

mv.visitInsn(LSUB);

mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");

maxStack = 4;

}

@Override

public void visitInsn(int opcode) {

if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {

mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");

mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",

"currentTimeMillis", "()J");

mv.visitInsn(LADD);

mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");

maxStack = Math.max(maxStack, stack.size() + 4);

}

super.visitInsn(opcode);

}

@Override

public void visitMaxs(int maxStack, int maxLocals) {

super.visitMaxs(Math.max(this.maxStack, maxStack), maxLocals);

}

}


스택 필드는 AnalyzerAdapter 클래스에 정의되며,

피연산자 스택에 유형을 포함합니다. 보다 정확하게는 visitXxx Insn에서 오버라이드 된 메소드가 호출되기 전에,

이 목록에는이 명령어 바로 앞에있는 피연산자 스택의 상태가 포함됩니다. 스택 필드가 올바르게 업데이트되도록 오버라이드 된 메서드를 호출해야합니다 (따라서 원본 코드에서 mv 대신 super를 사용).


슈퍼 클래스의 메소드를 호출하여 새 명령어를 삽입 할 수도 있습니다.이 명령어의 프레임은 AnalyzerAdapter에 의해 계산됩니다. 또한이 어댑터는 계산 된 프레임을 기반으로 visitMaxs의 인수를 업데이트하므로 직접 업데이트하지 않아도됩니다.


class AddTimerMethodAdapter3 extends AnalyzerAdapter {

public AddTimerMethodAdapter3(String owner, int access, String name, String desc, MethodVisitor mv) {

super(ASM4, owner, access, name, desc, mv);

}

@Override

public void visitCode() {

super.visitCode();

super.visitFieldInsn(GETSTATIC, owner, "timer", "J");

super.visitMethodInsn(INVOKESTATIC, "java/lang/System",

"currentTimeMillis", "()J");

super.visitInsn(LSUB);

super.visitFieldInsn(PUTSTATIC, owner, "timer", "J");

}

@Override

public void visitInsn(int opcode) {

if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {

super.visitFieldInsn(GETSTATIC, owner, "timer", "J");

super.visitMethodInsn(INVOKESTATIC, "java/lang/System",

"currentTimeMillis", "()J");

super.visitInsn(LADD);

super.visitFieldInsn(PUTSTATIC, owner, "timer", "J");

}

super.visitInsn(opcode);

}

}




3.3.3. LocalVariablesSorter


이 메소드 어댑터는 메소드에서 사용 된 로컬 변수를이 메소드에 나타나는 순서대로 번호를 재 지정합니다.

예를 들어 두 개의 매개 변수가있는 메소드에서 인덱스가 3보다 크거나 같은 첫 번째 로컬 변수는 처음 세 개의 로컬 변수가 this와 두 개의 메소드 매개 변수에 해당하므로 변경할 수 없습니다 - 할당됩니다. 인덱스 3, 두 번째 인덱스 4 등이 할당됩니다.

이 어댑터는 메소드에 새 로컬 변수를 삽입 할 때 유용합니다. 이 어댑터가 없으면 기존의 모든 지역 변수 뒤에 새로운 지역 변수를 추가해야하지만, 아쉽게도 숫자가 visitMaxs의 메소드 끝까지 알려지지 않습니다.

이 어댑터를 사용하는 방법을 보여주기 위해 로컬 변수를 사용하여 AddTimerAdapter를 구현한다고 가정 해 봅니다.


public class C {

public static long timer;

public void m() throws Exception {

long t = System.currentTimeMillis();

Thread.sleep(100);

timer += System.currentTimeMillis() - t;

}

}


LocalVariablesSorter를 확장하고이 클래스에 정의 된 newLocal 메서드를 사용하면 쉽게 수행 할 수 있습니다.

class AddTimerMethodAdapter4 extends LocalVariablesSorter {
private int time;
public AddTimerMethodAdapter4(int access, String desc, MethodVisitor mv) {
super(ASM4, access, desc, mv);
}
@Override
public void visitCode() {
super.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
time = newLocal(Type.LONG_TYPE);
mv.visitVarInsn(LSTORE, time);
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitVarInsn(LLOAD, time);
mv.visitInsn(LSUB);
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
}
super.visitInsn(opcode);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
super.visitMaxs(maxStack + 4, maxLocals);
}
}

로컬 변수가 번호가 매겨지면 메소드에 연결된 원본 프레임이 유효하지 않게되고 새로운 로컬 변수가 삽입되면 fortiori가됩니다. 다행히도이 프레임을 처음부터 다시 계산하는 것을 피할 수 있습니다. 실제로 프레임을 추가하거나 제거해야하며 변형 된 메서드의 프레임을 가져 오려면 원래 프레임의 로컬 변수 내용을 재정렬하는 것으로 충분합니다. LocalVariablesSorter는 자동으로 처리합니다. 메소드 어댑터 중 하나에 대해 점진적 스택 맵 프레임 업데이트를 수행해야하는 경우이 클래스의 소스에서 영감을 얻을 수 있습니다.

위에서 볼 수 있듯이 로컬 변수를 사용한다고해서 maxStack에 대한 최악의 경우에 대한이 클래스의 원래 버전에서의 문제는 해결되지 않습니다. AnalyzerAdapter를 사용하여이를 해결하려면 LocalVariablesSorter 외에도 상속을 통해 위임하지 않고 위임을 통해 이러한 어댑터를 사용해야합니다. (다중 상속이 불가능하기 때문에) :

class AddTimerMethodAdapter5 extends MethodVisitor {
public LocalVariablesSorter lvs;
public AnalyzerAdapter aa;
private int time;
private int maxStack;
public AddTimerMethodAdapter5(MethodVisitor mv) {
super(ASM4, mv);
}
@Override
public void visitCode() {
mv.visitCode();
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
time = lvs.newLocal(Type.LONG_TYPE);
mv.visitVarInsn(LSTORE, time);
maxStack = 4;
}
@Override
public void visitInsn(int opcode) {
if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",
"currentTimeMillis", "()J");
mv.visitVarInsn(LLOAD, time);
mv.visitInsn(LSUB);
mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");
mv.visitInsn(LADD);
mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");
maxStack = Math.max(aa.stack.size() + 4, maxStack);
}
mv.visitInsn(opcode);
}
@Override
public void visitMaxs(int maxStack, int maxLocals) {
mv.visitMaxs(Math.max(this.maxStack, maxStack), maxLocals);
}
}

이 어댑터를 사용하려면 LocalVariablesSorter를 어댑터에 연결해야하는 AnalyzerAdapter에 연결해야합니다. 첫 번째 어댑터는 로컬 변수를 정렬하고 이에 따라 프레임을 업데이트하며, 분석기 어댑터는 이전 어댑터에서 수행 된 번호 매김을 고려하여 중간 프레임을 계산합니다 어댑터는 이러한 번호가 다시 매겨진 중간 프레임에 액세스 할 수 있습니다. 이 체인은 visitMethod에서 다음과 같이 구성 할 수 있습니다.


mv = cv.visitMethod(access, name, desc, signature, exceptions);

if (!isInterface && mv != null && !name.equals("<init>")) {

AddTimerMethodAdapter5 at = new AddTimerMethodAdapter5(mv);

at.aa = new AnalyzerAdapter(owner, access, name, desc, at);

at.lvs = new LocalVariablesSorter(access, desc, at.aa);

return at.lvs;

}



3.3.4. AdviceAdapter


이 메소드 어댑터는 메소드 시작과 모든 RETURN 또는 ATHROW 명령어 바로 앞에 코드를 삽입하는 데 사용할 수있는 추상 클래스입니다.

주된 장점은 생성자의 처음에 코드를 삽입하지 말고 상위 생성자를 호출 한 후 생성자에 대해서도 작동한다는 것입니다. 사실이 어댑터의 대부분의 코드는이 수퍼 생성자 호출을 탐지하는 데 사용됩니다.


3.2.4 절의 AddTimerAdapter 클래스를 자세히 살펴보면,

이 문제 때문에 AddTimerMethodAdapter가 생성자에 사용되지 않는다는 것을 알 수 있습니다. AdviceAdapter를 상속하면이 메소드 어댑터가 생성자에서도 작동하도록 향상 될 수 있습니다 (AdviceAdapter는 LocalVariablesSorter에서 상속되므로 로컬 변수도 쉽게 사용할 수 있습니다).


class AddTimerMethodAdapter6 extends AdviceAdapter {

public AddTimerMethodAdapter6(int access, String name, String desc,

MethodVisitor mv) {

super(ASM4, mv, access, name, desc);

}

@Override

protected void onMethodEnter() {

mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");

mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",

"currentTimeMillis", "()J");

mv.visitInsn(LSUB);

mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");

}

@Override

protected void onMethodExit(int opcode) {

mv.visitFieldInsn(GETSTATIC, owner, "timer", "J");

mv.visitMethodInsn(INVOKESTATIC, "java/lang/System",

"currentTimeMillis", "()J");

mv.visitInsn(LADD);

mv.visitFieldInsn(PUTSTATIC, owner, "timer", "J");

}

@Override

public void visitMaxs(int maxStack, int maxLocals) {

super.visitMaxs(maxStack + 4, maxLocals);

}

}






'ASM' 카테고리의 다른 글

ASM User Guide 번역 4  (0) 2018.09.14
ASM User Guide 번역 2  (0) 2018.09.10
ASM User Guide 번역 1  (0) 2018.09.10
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함