【我的ASM学习进阶之旅】 10 ASM的Core API 的Method的字节码指令的示例讲解

上篇博客 【我的ASM学习进阶之旅】 09 介绍ASM的Core API 的Method的字节码指令https://ouyangpeng.blog.csdn.net/article/details/112574716
我们将了字节码指令的基本概念,下面这篇博客我们来实际操作讲解一下字节码指令。

一、示例讲解

让我们看一些基本示例,以更具体地了解字节码指令的工作方式。

1.1 Java示例类源码

以下bean类:

package pkg;

public class Bean {
    private int f;

    public int getF() {
        return this.f;
    }

    public void setF(int f) {
        this.f = f;
    }
}

1.2 对应的ByteCode字节码

对应的ByteCode字节码为:

// class version 52.0 (52)
// access flags 0x21
public class pkg/Bean {

  // compiled from: Bean.java

  // access flags 0x2
  private I f

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lpkg/Bean; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public getF()I
   L0
    LINENUMBER 7 L0
    ALOAD 0
    GETFIELD pkg/Bean.f : I
    IRETURN
   L1
    LOCALVARIABLE this Lpkg/Bean; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public setF(I)V
   L0
    LINENUMBER 11 L0
    ALOAD 0
    ILOAD 1
    PUTFIELD pkg/Bean.f : I
   L1
    LINENUMBER 12 L1
    RETURN
   L2
    LOCALVARIABLE this Lpkg/Bean; L0 L2 0
    LOCALVARIABLE f I L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2
}

【我的ASM学习进阶之旅】 10 ASM的Core API 的Method的字节码指令的示例讲解

1.3 getter方法的字节码

getter方法的字节码核心代码为:

ALOAD 0
GETFIELD pkg/Bean.f : I
IRETURN
  • 第一条指令读取局部变量0,该局部变量在创建此方法调用的框架期间已初始化为0,并将此值压入操作数堆栈。

  • 第二条指令从堆栈中弹出此值,即this,并压入此对象的f字段,即this.f.

  • 最后一条指令从堆栈中弹出该值,并将其返回给调用方。

此方法的执行帧的连续状态如下图所示。

【我的ASM学习进阶之旅】 10 ASM的Core API 的Method的字节码指令的示例讲解

getF方法的连续frame状态:

  • a)初始状态,
  • b)在ALOAD 0之后
  • c)在GETFIELD之后

1.3 setter方法的字节码

setter方法的字节码核心代码为:

ALOAD 0
ILOAD 1
PUTFIELD pkg/Bean.f : I
RETURN
  • 与之前一样,第一条指令将其压入操作数堆栈。
  • 第二条指令将局部变量1推送到该方法调用的框架的创建过程中,该局部变量使用f参数值进行了初始化。
  • 第三条指令弹出这两个值,并将int值存储在所引用对象的f字段中,即在this.f中。
  • 最后一条指令在源代码中是隐式的,而在编译后的代码中是必需的,它会破坏当前执行框架并返回给调用者。

此方法的执行帧的连续状态如下图所示。
【我的ASM学习进阶之旅】 10 ASM的Core API 的Method的字节码指令的示例讲解
setF方法的连续帧状态:

  • a)初始状态
  • b)ALOAD 0之后
  • c)ILOAD 1之后
  • d)PUTFIELD之后

1.4 构造方法的字节码

Bean类还具有由编译器生成的默认公共构造函数,因为程序员没有定义任何显式构造函数。
此默认的公共构造函数生成为

 Bean(){
   super(); 
 }

该构造函数的字节码如下:

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lpkg/Bean; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

核心代码为

ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
  • 第一条指令将其压入操作数堆栈。
  • 第二条指令从堆栈中弹出该值,并调用Object类中定义的方法。 这对应于super()调用,即对超类Object的构造函数的调用。 您可以在此处看到,在编译类和源类中,构造函数的名称不同:在编译类中,它们始终被命名为,而在源类中,它们具有定义它们的类的名称。
  • 最后,最后一条指令返回给调用者。

1.5 稍微复杂的setter方法

现在让我们考虑一个稍微复杂一些的setter方法:

public void checkAndSetF(int f) {
        if (f >= 0) {
            this.f = f;
        } else {
            throw new IllegalArgumentException();
        }
    }

此新的setter方法的字节码如下:

// access flags 0x1
  public checkAndSetF(I)V
   L0
    LINENUMBER 16 L0
    ILOAD 1
    IFLT L1
   L2
    LINENUMBER 17 L2
    ALOAD 0
    ILOAD 1
    PUTFIELD pkg/Bean.f : I
    GOTO L3
   L1
    LINENUMBER 19 L1
   FRAME SAME
    NEW java/lang/IllegalArgumentException
    DUP
    INVOKESPECIAL java/lang/IllegalArgumentException.<init> ()V
    ATHROW
   L3
    LINENUMBER 21 L3
   FRAME SAME
    RETURN
   L4
    LOCALVARIABLE this Lpkg/Bean; L0 L4 0
    LOCALVARIABLE f I L0 L4 1
    MAXSTACK = 2
    MAXLOCALS = 2
}

核心代码为

    ILOAD 1
    IFLT L1
    ALOAD 0
    ILOAD 1
    PUTFIELD pkg/Bean.f : I
    GOTO L3
L1
    NEW java/lang/IllegalArgumentException
    DUP
    INVOKESPECIAL java/lang/IllegalArgumentException.<init> ()V
    ATHROW
L3
    RETURN
  • 第一条指令将局部变量1(初始化为f)压入操作数堆栈。

  • IFLT指令从堆栈中弹出该值,并将其与0进行比较。如果它小于(LT)0,则跳转到L1标签指定的指令,否则不执行任何操作,并继续执行下一条指令。

  • 接下来的三个指令与setF方法中的指令相同。

  • GOTO指令无条件地跳转到由L3标签指定的指令,即RETURN指令。

  • L1和L3标签之间的指令创建并引发异常:NEW指令创建异常对象并将其压入操作数堆栈。

  • DUP指令在堆栈上复制此值。

  • INVOKESPECIAL指令将弹出这两个副本之一,并在其上调用异常构造函数。

  • 最后,ATHROW指令弹出剩余的副本并将其作为异常抛出(因此执行不会继续执行下一条指令)

1.6 异常处理程序

没有捕获异常的字节码指令:相反,方法的字节码与一系列异常处理程序相关联,这些异常处理程序指定了在方法的给定部分抛出异常时必须执行的代码。

异常处理程序类似于try catch块:它具有一个范围(该范围是与try块的内容相对应的一系列指令)和一个处理程序(与catch块的内容相对应)。

该范围由开始和结束标签指定,而处理程序由开始标签指定。

例如下面的源代码:

   public static void sleep(long d) {
        try {
            Thread.sleep(d);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

对应的sleep方法的字节码为:

  // access flags 0x9
  public static sleep(J)V
    TRYCATCHBLOCK L0 L1 L2 java/lang/InterruptedException
   L0
    LINENUMBER 25 L0
    LLOAD 0
    INVOKESTATIC java/lang/Thread.sleep (J)V
   L1
    LINENUMBER 28 L1
    GOTO L3
   L2
    LINENUMBER 26 L2
   FRAME SAME1 java/lang/InterruptedException
    ASTORE 2
   L4
    LINENUMBER 27 L4
    ALOAD 2
    INVOKEVIRTUAL java/lang/InterruptedException.printStackTrace ()V
   L3
    LINENUMBER 29 L3
   FRAME SAME
    RETURN
   L5
    LOCALVARIABLE e Ljava/lang/InterruptedException; L4 L3 2
    LOCALVARIABLE d J L0 L5 0
    MAXSTACK = 2
    MAXLOCALS = 3

扩展

上一篇:51Nod1220 约数之和


下一篇:Java指定行读写数据