ASM学习指导02

131 阅读3分钟

首先我们先来编写一个类,用于本文编译后文件的学习,这段代码所对应的完整的编译后的文本详见附录

public class Female {
    protected Object obj;

    public String func() {
        return obj.toString();
    }
}

public class Sxm extends Female{
    public static long timer = 0;

    private int num;

    public int getNum() {
        return this.num;
    }

    public void setNum(int num) {
        this.num = num;
    }

    public void addNum(int n) {
        if (n < 0) {
            return;
        }
        if (n == 0) {
            throw new RuntimeException();
        } else {
            this.num += n;
        }
    }
    
    public Object getSuperObj() {
        return super.obj;
    }
    
    class Inner {
        public String str = "This is inner class";
    }
}

源码和编译后文件的区别

首先源码和编译后的文件是有区别的

  1. 编译后的文件中只包含了一个类,所以源码中的内部类会被单独编译为一个文件
  2. 只有源码中会有注释,编译后的文件中不含有注释信息
  3. 编译后的文件中也没有import和package信息,而是以内部名称(internal name)代替

什么是内部名称

实际上内部名称也是用于标识类的位置,和源码中的全限定类名相似,如Object的全限定类名是java.lang.Object,而在编译后变成了java/lang/Object

类型描述符和方法描述符

在我们了解了internal name之后,编译后文件的变量类型仍然和源码有很大差别。例如在源码中public static long timer = 0;,在编译后文件中则变成了public static J timer,这里可能就会有疑问:J是什么类型? 对于编译后文件来说,类对象基本参照internal name都可以理解,但是基本类型则完全不同

java typetype descriptor
booleanZ
charC
byteB
shortS
intI
floatF
longL
doubleD
ObjectLjava/lang/Object;
int[][I
Object[][][[Ljava/lang/Object;

编译后文件中类对象表示为L + internal name,数组表示为[ + 数组中元素的type descriptor。这里注意类对象最后的;是必须的

当我们了解编译后文件如何表示变量类型,现在我们可以来看一看编译后文件是如何表示方法的。以public void setNum(int)为例,在编译后文件中public setNum(I)V。方法返回空void在编译后文件中用V表示,如果入参有多个则同事罗列,下面给出几个示例

源码方法描述符
void (int, float)(IF)V
int (Object)(Ljava/lang/Object;)I
int[] (int i,String)(ILjava/lang/String;)[i

如何阅读代码

接下来我们将继续深入,尝试阅读编译后文件中的代码。

局部变量和操作数栈

在尝试阅读字节码之前,首先来学习一下java代码的运行方式。java代码运行在线程(thread)中,每一个线程都有自己的执行栈(stack),也就是所谓的函数堆栈,里面记录线程的调用信息,每当调用一个方法都会新加一层,一个方法退出则出栈一层。每一个调用的方法都被封装为帧(frame)格式,如前文所说,栈中每一帧都表示调用的方法,其中要特别说明的是:即使方法调用抛出异常,也会出栈,直到异常被捕获(catch)。 帧中存在两部分:局部变量表(lacal variables)和操作数栈(operand stack)。局部变量可以通过下标的方式获取变量,也就是说其底层实现类似于数组,在栈帧初始化的时候,局部变量表中下标0存储this对象,然后是方法的入参。比如a.equals(b),局部变量表初始化时会有两个变量a和b,分别对应this对象和方法入参。 操作数栈中存储的是字节码指令使用的操作数,和所有的栈结构一样,遵循先入后出的顺序。所有指令都会取栈顶数据执行动作,如运算指令IADD,ISUB等,会pop两次相加再push;还有IFGE这样的条件指令,pop后进行判断是否满足条件。不过于局部变量不同,操作数栈在初始时是空的。

此外还有一点要注意,操作数栈和局部变量存储所有类型的变量都是一格,除了long和double这种64的数据,需要两格空间存储。

字节码指令

字节码指令包含opcode和参数。opcode实际上是一个unsigned byte,为了便于阅读,所以有一个英文的助记词,例如GOTO指令,实际上在字节码文件中是167,但是为了我们方便阅读,所以有一个英文名字叫GOTO。

这些指令可以分为两类,一组是用于局部变量和操作数栈做交互用的。ILOAD,LLOAD,FLOAD,DLOAD, 和ALOAD指令读取局部变量,然后push到操作数栈。传入一个参数i表示要读取的下标。其中ILOAD用于读取boolean byte char short int类型的数据,而LLOAD FLOAD DLOAD分别用于读取long float double类型数据,最后ALOAD用于读取所有非基本类型的数据,如object和数组。与之相对的ISTORE,LSTORE,FSTORE,DSTOREASTORE指令则是将操作数栈pop后存入局部变量。

其他的指令又可以分成以下几类:

  • 操作操作数栈 这类指令用于操作操作数栈。例如POP弹出栈顶元素;DUP复制栈顶元素再push回去;SWAP弹出两次交换后重新压入
  • 常量 这一类指令把常量压入操作数栈。如ACONST_NULL向栈顶压入null;ICONST_0压入int类型值0;FCONST_0压入float类型值0f;LDC压入任意类型的常量
  • 运算与逻辑 这类指令弹出两个栈元素做运算,再将运算结果重新压入栈。ADD,SUB,MUL,DIVREM分别对应+,-,*,/和%。逻辑运算opcode有**<<,>>,>>>,|,&and^**
  • 类型转换 将栈顶元素弹出,进行类型转换后重新压入,这类指令对应源码中类型强制转换操作。如I2F,F2D,L2D等数值类型转换;CHECKCAST将一个引用值转换为其他类型。
  • 操作对象 创建新对象,对象加锁,检验对象类型等。如NEW会新建一个对象压入操作数栈。
  • 成员变量 读取和写入成员变量,最常见于get和set方法。如GETFIELD owner name desc,先从栈中pop一个类型为owner的引用值,从中读取类型为desc的成员变量name;PUTFIELDowner name desc则是取一个值和一个引用,写入成员变量;此外还有GETSTATICPUTSTATIC用于读取静态变量。
    • 调用方法 这类指令负责调用方法和构造器,先弹出和参数数量一样多的元素,外加一个调用者类型的引用值,如果方法有返回值则将返回值压入栈。如INVOKEVIRTUAL INVOKESTATIC INVOKESPECIAL INVOKEINTERFACEINVOKEDYNAMIC
  • 数组 用于数组的读写。xALOAD弹出一个下标和数组地址,然后取出数组中的这个下标放入栈内;类似的还有xASTORE负责存储数据
  • 跳转 这种指令就是源码中的判断或者循环代码块,还有break和continue关键字。如IFEQ会判断当前栈顶元素是否为0,如果是就会跳转到指向的label;TABLESWITCHLOOKUPSWITCH对应源码中的switch代码块
  • 返回 这里主要是xRETURNRETURN指令。其中RETURN指令返回void,xRETURN则返回其指定的数据类型。

异常

在源码中还有一种代码结构不能被上述提及到的指令表示,那就是try-catch-finally异常捕获处理。如下面这段源码

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

那么编译后对应

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

参考文献:

[1]: ASM开发手册 [2]: JAVA 异常堆栈中的行号(lineNumber)是怎么来的? [3]: 求 Java字节码中 stack map frames 简单说明?

附录 字节码全文

// class version 52.0 (52)
// access flags 0x21
public class org/miku/bean/Sxm extends org/miku/bean/Female {

  // compiled from: Sxm.java
  // access flags 0x0
  INNERCLASS org/miku/bean/Sxm$Inner org/miku/bean/Sxm Inner

  // access flags 0x9
  public static J timer

  // access flags 0x2
  private I num

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 8 L0
    ALOAD 0
    INVOKESPECIAL org/miku/bean/Female.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lorg/miku/bean/Sxm; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public getNum()I
   L0
    LINENUMBER 14 L0
    ALOAD 0
    GETFIELD org/miku/bean/Sxm.num : I
    IRETURN
   L1
    LOCALVARIABLE this Lorg/miku/bean/Sxm; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public setNum(I)V
    // parameter  num
   L0
    LINENUMBER 18 L0
    ALOAD 0
    ILOAD 1
    PUTFIELD org/miku/bean/Sxm.num : I
   L1
    LINENUMBER 19 L1
    RETURN
   L2
    LOCALVARIABLE this Lorg/miku/bean/Sxm; L0 L2 0
    LOCALVARIABLE num I L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1
  public addNum(I)V
    // parameter  n
   L0
    LINENUMBER 22 L0
    ILOAD 1
    IFGE L1
   L2
    LINENUMBER 23 L2
    RETURN
   L1
    LINENUMBER 25 L1
   FRAME SAME
    ILOAD 1
    IFNE L3
   L4
    LINENUMBER 26 L4
    NEW java/lang/RuntimeException
    DUP
    INVOKESPECIAL java/lang/RuntimeException.<init> ()V
    ATHROW
   L3
    LINENUMBER 28 L3
   FRAME SAME
    ALOAD 0
    DUP
    GETFIELD org/miku/bean/Sxm.num : I
    ILOAD 1
    IADD
    PUTFIELD org/miku/bean/Sxm.num : I
   L5
    LINENUMBER 30 L5
    RETURN
   L6
    LOCALVARIABLE this Lorg/miku/bean/Sxm; L0 L6 0
    LOCALVARIABLE n I L0 L6 1
    MAXSTACK = 3
    MAXLOCALS = 2

  // access flags 0x1
  public getSuperObj()Ljava/lang/Object;
   L0
    LINENUMBER 33 L0
    ALOAD 0
    GETFIELD org/miku/bean/Female.obj : Ljava/lang/Object;
    ARETURN
   L1
    LOCALVARIABLE this Lorg/miku/bean/Sxm; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x8
  static <clinit>()V
   L0
    LINENUMBER 9 L0
    LCONST_0
    PUTSTATIC org/miku/bean/Sxm.timer : J
    RETURN
    MAXSTACK = 2
    MAXLOCALS = 0
}