3.3 ASM-方法-工具类

ASM-方法-工具类

3.3 工具集 Tools

‘org.objectweb.asm.commons’包含了一些预定义的方法适配器,可以用来定义自己的适配器。 本节介绍三个工具类,并且会展示它们如何和3.2.4节中的‘AddTimerAdapter’示例结合使用。 也展示了如何使用上一章中的工具简化方法的生成和转化。

3.3.1 基础工具集 Basic tools

在2.3章中介绍的工具也可以使用在方法操作上。

Type

许多字节码指令,例如‘xLOAD‘、 ‘xADD’或者‘xRETURN’,取决于它们所应用的类型。 ‘Type’类提供了一个‘getOpcode’方法,对于这些指令,可以用来获取操作码所对应的给定类型。 这些方法需要一个整数型的操作码作为参数,调用后返回一个对应该类型的操作码。 例如‘t.getOpcode(IMUL)’,如果t是‘Type.FLOAT_TYPE’的话,就会返回‘FMUL’

TraceClassVisitor

这个类,在上一章已经介绍过了,以文本型的描述打印它所访问的类,包括文本型描述该类的方法,和本章中使用的方式非常类似。 因此可以用于在转换链中的任何一点上,跟踪生成或者转换方法的内容。例如:

java -classpath asm.jar:asm-util.jar \
  org.objectweb.asm.util.TraceClassVisitor \
  java.lang.Void

打印:

// 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 { … }’,使用命名为‘‘的方法(对应类的初始化)。 注意,如果你想跟踪链路某一点上单个方法,而不是跟踪该类的全部内容,你可以使用‘TraceMethodVisitor’类代替‘TraceClassVisitor’(在这种情况下,你必须明确指定后端;在这里我们使用‘Textifier’):

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

这段代码检测MyMethodAdapter类使用‘MethodVisitor API’是否正确。 需要注意的是,这个是配器不会检测字节码是否争取:例如它不会见从‘ISTORE 1 ALOAD 1’是否正确。 事实上,这类错误是可以被检测到的,如果你使用CheckMethodAdapter其他的构造函数(参考Javadoc), 并且如果你为visitMaxs方法提供合法的方maxStackmaxLocals**参数。

ASMifier

这个类在上一章中介绍过,也可以用于方法的内容。 它可以被用来知道如果通过ASM生成某些编译后的代码:在Java中只要编写相应的源码即可,使用javac编译,并且使用ASMifier访问编译后的类。 你将会得到ASM代码,来生成相应源码的字节码。

3.3.2. AnalyzerAdapter

这个方法适配器会根据visitFrame方法中被访问的帧,计算出每一个指令之前的栈哈希帧。 事实上,如前面3.1.5节所描述的,为了节省空间,visitFrame仅仅会在一个方法中某些特定的指令前调用,并且“其他的帧也可以从这些帧简单容易的推算出来”。 这就是AnalyzerAdapter的作用。当然在它仅能作用在包含了预先计算过栈哈希帧的编译类,即使用Java 6或者更改版本编译的类(或者像之前的示例一样,使用含有COMPUTE_FRAMES参数的ASM adapter将类升级到Java 6):

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);
  }
}

‘stack’属性在AnalyzerAdapter类中有定义,并且包含了在操作栈中的类型。 更确切地说,对于一个‘visitXxx Insn’指令,在覆盖方法被调用前,这个列表包含了在该条指令前操作栈的状态。 需要注意的是覆盖方法必须被调用,这样栈里的属性才能正确地更新(因此使用父类的原始方法,而不是mv的方法)。 另外,调用父类的方法也可以插入新指令:效果是AyalyzerAdapter会计算出这些指令对应的帧。 因此,该适配器会基于它计算出的帧更新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(索引值是从0开始的),前三个本地变量对应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);
  }
}

注意,当本地变量重写编号后,该方法关联的原始帧就都失效了,更不用说插入新的本地变量了。 庆幸的是避免重头计算这些帧是可行的:事实上没有任何帧需要被添加或者删除,在原本的帧上有足够的空间可以重新排列转换后方法的本地变量。 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,它自身则链接到自定义的适配器上:因此第一个适配器会排列本地变量并且更新帧,考虑到上一章中的重编号,analyzer adapter会计算中间帧,自定义的适配器可以访问到中间帧中重排序的编号。 在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

这个方法适配器是一个抽象类,可以用于在方法的开始和RETURNATHROW指令前插入代码。 它的主要优点是,也可以对构造函数起作用,代码不能在构造函数的一开始就插入,要在调用父类的构造函数后插入。 事实上,本节的大部分代码都是为了检测父类的构造函数而调用的。 如果仔细研究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);
  }
}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏前端菜鸟变老鸟

ES6(二):Promise

ES6之前解决异步编程只能使用回调函数或事件,ES6中加入了 Promise,使得异步编程更加简洁直观和合理

1243
来自专栏C语言及其他语言

【优秀题解】绝对值排序】(合并排序详解+图解)

原题链接:http://www.dotcpp.com/oj/problem1169.html (大家可以自行提交) 解题思路: 1.采用分治法思想,把整个序列...

5658
来自专栏jessetalks

Javascript基础回顾 之(二) 作用域

参数传递的问题   在Javascript中所有的参数传递都是按值传递的。也就是说把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。...

2836
来自专栏landv

C语言_函数【转】

4613
来自专栏专注 Java 基础分享

字节码文件的内部结构之谜

如果计算机的 CPU 只有「x86」这一种,或者操作系统只有 Windows 这一类,那么或许 Java 就不会诞生。Java 诞生之初就曾宣扬过它的初衷,「一...

4049
来自专栏noteless

[二]Java虚拟机 jvm内存结构 运行时数据内存 class文件与jvm内存结构的映射 jvm数据类型 虚拟机栈 方法区 堆 含义

JVM全称是Java Virtual Machine  ,既然是虚拟机,他终归要运行在物理机上

2841
来自专栏开发与安全

从零开始学C++之IO流类库(三):文件的读写、二进制文件的读写、文件随机读写

一、文件的读写 如前面所提,流的读写主要有<<, >>, get, put, read, write 等操作,ofstream 继承自ostream, ifst...

4810
来自专栏GreenLeaves

Proxy代理模式(结构型模式)

在面向对象系统中,有些对象由于某种原因(比如创建对象的开销很大,或者某些操作需要安全控制,或者需要进程外的访问等),直接访问会给调用者带来麻烦,那么如何在不损失...

1333
来自专栏知道一点点

sass入门学习篇(二)

一,sass有两种后缀名文件:一种后缀名为sass,不使用大括号和分号;另一种就是我们这里使用的scss文件,建议scss.

1062
来自专栏程序你好

在c#中,如何序列化/反序列化一个字典对象?

.Net提供的各种序列化的类,通过使用这些类,. Net对象的序列化和反序列化变得很容易。但是字典对象的序列化并不是那么容易。为此,您必须创建一个能够序列化自身...

1031

扫码关注云+社区

领取腾讯云代金券