必威体育Betway必威体育官网
当前位置:首页 > IT技术

ASM

时间:2019-10-07 20:45:35来源:IT技术作者:seo实验室小编阅读:80次「手机版」
 

asm

作用

程序分析、程序生成和程序转换都是非常有用的技术,可在许多应用环境下使用:

  • 程序分析,可用于查找应用程序中的潜在 bug、检测未被用到的代码、对代码 实施逆向工程,等等。

  • 程序生成,在编译器中使用。JIT(即时)编译器,等等。

  • 程序转换可用于优化或混淆(obfuscate)程序、向应用程序中插入调试或性能监视代码,用于面向切面的程序设计,等等。

对于 java 语言,它们可用于 Java 源代码或编译后的 Java 类。在使用经过编译的类时, 其好处之一显然就是不需要源代码。因此,程序转换可用于任何应用程序,既包括保密的源代码, 也包含商业应用程序。使用已编译类的另一个好处是,有可能在运行时,在马上就要将类加载到 Java 虚拟机之前,对类进行分析、生成或转换(在运行时生成和编译源代码也可以,但其速度很慢,而且需要一个完整的 Java 编译器)。

ASM 就是为 Java 语言设计的工具之一, 用于进行运行时(也是脱机的)类生成与转换。用于处理经过编译 的 Java 类。这个库的设计使其尽可能保持快速和小型化。

使用范围

ASM 供了一些工具,使用高于 字节级别的概念来读写和转换这种字节数组,这些概念包括数值常数、字符串、Java 标识符、Java 类型、Java 类结构元素,等等。注意,ASM 库的范围严格限制于类的读、写、转换和分析。具 体来说,类的加载过程就超出了它的范围之外。

模型

ASM 库 供了两个用于生成和转换已编译类的 API,一个是核心 API,以基于事件的形式 来表示类,另一个是树 API,以基于对象的形式来表示类。

在采用基于事件的模型时,类是用一系列事件来表示的,每个事件表示类的一个元素,比如 它的一个标头、一个字段、一个方法声明、一条指令,等等。基于事件的 API 定义了一组可能 事件,以及这些事件必须遵循的发生顺序,还 供了一个类分析器,为每个被分析元素生成一个 事件,还 供一个类写入器,由这些事件的序列生成经过编译的类。

而在采用基于对象的模型时,类用一个对象树表示,每个对象表示类的一部分,比如类本身、 一个字段、一个方法、一条指令,等等,每个对象都有一些引用,指向表示其组成部分的对象。 基于对象的 API 供了一种方法,可以将表示一个类的事件序列转换为表示同一个类的对象树, 也可以反过来,将对象树表示为等价的事件序列。换言之,基于对象的 API 构建在基于事件的 API 之上。

ASM 之所以要 供两个 API,是因为没有哪种 API 是最佳的。实际上,每个 API 都有自己 的优缺点:

  • 基于事件的API要快于基于对象的API,所需要的内存也较少,因为它不需要在内存中 创建和存储用于表示类的对象树(SAX 与 DOM 之间也有同样的差异)。

  • 但在使用基于事件的 API 时,类转换的实现可能要更难一些,因为在任意给定时刻, 类中只有一个元素可供使用(也就是与当前事件对应的元素),而在使用基于对象的 API 时,可以在内存中获得整个类。

体系结构

ASM 应用程序拥有一个很强壮的体系结构方面(aspect)。事实上,对于基于事件的 API, 其组织结构是围绕事件生成器(类分析器)、事件使用器(类写入器)和各种预定义的事件筛选 器进行的,在这一结构中可以添加用户定义的生成器、使用器和筛选器。因此,这一 API 的使 用分为两个步骤:

  • 将事件生成器、筛选器和使用器组件组装为可能很复杂的体系结构
  • 然后启动事件生成器,以执行生成或转换过程。

基于对象的 API 也有一个体系结构方面:实际上,用于操作类树的类生成器或转换器组件 是可以组成形成的,它们之间的链接代表着转换的顺序。

尽管典型 ASM 应用程序中的大多数组件体系结构都非常简单,但还是可以想象一下类似于 如下所示的复杂体系结构,其中的箭头表示在类分析器、写入器或转换器之间进行的基于事件或 基于对象的通信,在整个链中的任何位置,都可能会在基于事件与基于对象的表示之间进行转换:

结构

已编译类中包含如下各部分:

  • 专门一部分,述类的修饰符(比如public和private)、名字、超类、接口和注释。
  • 类中声明的每个字段各有一部分。每一部分 述一个字段的修饰符、名字、类型和注释。
  • 类中声明的每个方法及构造器各有一部分。每一部分 述一个方法的修饰符、名字、返 回类型与参数类型、注释。它还以 Java 字节代码指令的形式,包含了该方法的已编译代码。

源文件类和已编译类之间还是有一些差异:

  • 一个已编译类仅 述一个类,而一个源文件中可以包含几个类。比如,一个源文件 述 了一个类,这个类又有一个内部类,那这个源文件会被编译为两个类文件:主类和内 部类各一个文件。但是,主类文件中包含对其内部类的引用,定义了内部方法的内层 类会包含引用,引向其封装的方法。
  • 已编译类中当然不包含注释(comment),但可以包含类、字段、方法和代码属性,可 以利用这些属性为相应元素关联更多信息。Java 5 中引入可用于同一目的的注解 (annotaion)以后,属性已经变得没有什么用处了。
  • 编译类中不包含package和import部分,因此,所有类型名字都必须是完全限定的。

另一个非常重要的结构性差异是已编译类中包含常量池(constant pool)部分。这个池是一个数组,其中包含了在类中出现的所有数值、字符串和类型常量。这些常量仅在这个常量池部分 中定义一次,然后可以利用其索引,在类文件中的所有其他各部分进行引用。

在许多情况下,一种类型只能是类或接口类型。例如,一个类的超类、由一个类实现的接口, 或者由一个方法抛出的异常就不能是基元类型或数组类型,必须是类或接口类型。这些类型在已 编译类中用内部名字表示。一个类的内部名就是这个类的完全限定名,其中的点号用斜线代替。 例如,String 的内部名为 java/lang/String。

类型描述符

内部名只能用于类或接口类型。所有其他 Java 类型,比如字段类型,在已编译类中都是用 类型 述符表示的。

方法描述符

方法 述符是一个类型 述符列表,它用一个字符串 述一个方法的参数类型和返回类型。 方法 述符以左括号开头,然后是每个形参的类型 述符,然后是一个右括号,接下来是返回类 型的类型 述符,如果该方法返回 void,则是 V(方法 述符中不包含方法的名字或参数名)。

接口和组件

用于生成和变转已编译类的ASM API是基于ClassVisitor抽象类的。这个 类中的每个方法都对应于同名的类文件结构部分。简单的部分只需一个方法调用就能 访问,这个调用返回 void,其参数 述了这些部分的内容。有些部分的内容可以达到任意长度、 任意复杂度,这样的部分可以用一个初始方法调用来访问,返回一个辅助的访问者类。 visitAnnotation、visitfield 和 visitMethod 方法就是这种情况,它们分别返回 AnnotationVisitor、FieldVisitor 和 MethodVisitor。

针对这些辅助类递归适用同样的原则。例如,FieldVisitor 抽象类中的每个方法(见图 2.5)对应于同名的类文件子结构,visitAnnotation 返回一个辅助的 AnnotationVisitor, 和在 ClassVisitor 中一样。

ClassVisitor 类的方法必须按以下顺序调用(在这个类的 Javadoc 中规定):

这意味着必须首先调用 visit,然后是对 visitSource 的最多一个调用,接下来是对 visitOuterClass 的最多一个调用,然后是可按任意顺序对 visitAnnotation 和 visitAttribute 的任意多个访问,接下来是可按任意顺序对 visitInnerClass、 visitField 和 visitMethod 的任意多个调用,最后以一个 visitEnd 调用结束。

ASM 供了三个基于 ClassVisitor API 的核心组件,用于生成和变化类:

  • ClassReader类分析以字节数组形式给出的已编译类,并针对在其accept方法参数 中传送的 ClassVisitor 实例,调用相应的 visitXxx 方法。这个类可以看作一个事 件产生器。
  • ClassWriter 类是 ClassVisitor 抽象类的一个子类,它直接以二进制形式生成编 译后的类。它会生成一个字节数组形式的输出,其中包含了已编译类,可以用 toByteArray 方法来 取。这个类可以看作一个事件使用器。
  • ClassVisitor类将它收到的所有方法调用都委托给另一个ClassVisitor类。这个 类可以看作一个事件筛选器。

分析类

在分析一个已经存在的类时,惟一必需的组件是 ClassReader 组件。让我们用一个例子 来说明。假设希望打印一个类的内容,其方式类似于 javap 工具。第一步是编写 ClassVisitor 类的一个子类,打印它所访问的类的相关信息。

public class Classprinter extends ClassVisitor {
    public ClassPrinter() {
       super(ASM4);
    }
    public void visit(int version, int access, String name,
           String signature, String superName, String[] interfaces) {
        System.out.println(name + " extends " + superName + " {");
    }
    public void visitSource(String source, String debug) {
    }
    public void visitOuterClass(String owner, String name, String desc) {
    }
    public AnnotationVisitor visitAnnotation(String desc,
           boolean visible) {
            return null;
    }
    public void visitAttribute(Attribute attr) {
    }
    public void visitInnerClass(String name, String outerName,
         String innerName, int access) {
    }
    public FieldVisitor visitField(int access, String name, String
       desc, String signature, Object value) {
        System.out.println(" " + desc + " " + name);
        return null;
    }
    public MethodVisitor visitMethod(int access, String name,
      String desc, String signature, String[] exceptions) {
        System.out.println(" " + name + desc);
        return null;
    }
    public void visitEnd() {
        System.out.println("}"); 
    } 
}
复制代码

第二步是将这个 ClassPrinter 与一个 ClassReader 组件合并在一起,使 ClassReader 产生的事件由我们的 ClassPrinter 使用:

ClassPrinter cp = new ClassPrinter();
ClassReader cr = new ClassReader("java.lang.Runnable");
cr.accept(cp, 0);
复制代码

第二行创建了一个 ClassReader,以分析 Runnable 类。在最后一行调用的 accept 方 法分析 Runnable 类字节代码,并对 cp 调用相应的 ClassVisitor 方法。结果为以下输出:

java/lang/Runnable extends java/lang/Object {
    run()V
}
复制代码

注意,构建 ClassReader 实例的方式有若干种。必须读取的类可以像上面一样用名字指定, 也可以像字母数组或 InputStream 一样用值来指定。利用 ClassLoader 的getResourceAsStream 方法,可以获得一个读取类内容的输入流,如下:

cl.getResourceAsStream(classname.replace(’.’, ’/’) + ".class");
复制代码

生成类

为生成一个类,惟一必需的组件是 ClassWriter 组件。考虑以下接口:

 package pkg;
public interface Comparable extends Mesurable {
  int LESS = -1;
  int EQUAL = 0;
  int GREATER = 1;
  int compareTo(Object o);
}
复制代码

可以对 ClassVisitor 进行六次方法调用来生成它:

ClassWriter cw = new ClassWriter(0);
cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
       "pkg/Comparable", null, "java/lang/Object",
new String[] { "pkg/Mesurable" });
cw.visitField(ACC_PUBLIC + ACC_final + ACC_STATIC, "LESS", "I",
null, new integer(-1)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "EQUAL", "I",
null, new Integer(0)).visitEnd();
cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "GREATER", "I",
null, new Integer(1)).visitEnd(); cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "compareTo",
"(Ljava/lang/Object;)I", null, null).visitEnd(); cw.visitEnd();
byte[] b = cw.toByteArray();
复制代码

第一行创建了一个 ClassWriter 实例,它实际上将创建类的字节数组表示。

对 visit 方法的调用定义了类的标头。V1_5 参数是一个常数,与所有其他 ASM 常量一样, 在ASM Opcodes接口中定义。它指明了类的版本——Java 1.5。ACC_XXX常量是与Java修饰 符对应的标志。这里规定这个类是一个接口,而且它是 public 和 abstract 的(因为它不能 被实例化)。下一个参数以内部形式规定了类的名字(见 2.1.2 节)。回忆一下,已编译类不包含 Package 和 Import 部分,因此,所有类名都必须是完全限定的。下一个参数对应于泛型(见 4.1 节)。在我们的例子中,这个参数是 null,因为这个接口并没有由类型变量进行参数化。第 五个参数是内部形式的超类(接口类隐式继承自 Object)。最后一个参数是一个数组,其中是 被扩展的接口,这些接口由其内部名指定。

接下来对 visitField 的三次调用是类似的,用于定义三个接口字段。第一个参数是一组 标志,对应于 Java 修饰符。这里规定这些字段是 public、final 和 static 的。第二个参数 是字段的名字,与它在源代码中的显示相同。第三个参数是字段的类型,采用类型 述符形式。 这里,这些字段是 int 字段,它们的 述符是 I。第四个参数对应于泛型。在我们的例子中, 它是 null,因为这些字段类型没有使用泛型。最后一个参数是字段的常量值:这个参数必须仅用于真正的常量字段,也就是 final static 字段。对于其他字段,它必须为 null。由于此处 没有注释,所以立即调用所返回的 FieldVisitor 的 visitEnd 方法,即对其 visitAnnotation 或 visitAttribute 方法没有任何调用。

visitMethod 调用用于定义 compareTo 方法,同样,第一个参数是一组对应于 Java 修饰 符的标志。第二个参数是方法名,与其在源代码中的显示一样。第三个参数是方法的 述符。第 四个参数对应于泛型。在我们的例子中,它是 null,因为这个方法没有使用泛型。最后一个参 数是一个数组,其中包括可由该方法抛出的异常,这些异常由其内部名指明。它在这里为 null, 因为这个方法没有声明任何异常。visitMethod 方法返回 MethodVisitor(见图 3.4),可用 于定义该方法的注释和属性,最重要的是这个方法的代码。这里,由于没有注释,而且这个方法 是抽象的,所以我们立即调用所返回的 MethodVisitor 的 visitEnd 方法。

对 visitEnd 的最后一个调用是为了通知 cw:这个类已经结束,对 toByteArray 的调用 用于以字节数组的形式 取它。

使用生成的类

前面的字节数组可以存储在一个 Comparable.class 文件中,供以后使用。或者,也可 以用 ClassLoader 动态加载它。一种方法是定义一个 ClassLoader 子类,它的 defineClass 方法是公有的:

class MyClassLoader extends ClassLoader {
      public Class defineClass(String name, byte[] b) {
       return defineClass(name, b, 0, b.length);
      }
}
复制代码

然后,可以用下面的代码直接调用所生成的类:

Class c = myClassLoader.defineClass("pkg.Comparable", b);
复制代码

另一种加载已生成类的方法可能更清晰一些,那就是定义一个 ClassLoader 子类,它的 findClass 方法被重写,以在运行过程中生成所请求的类:

class StubClassLoader extends ClassLoader {
       @Override
       protected Class findClass(String name)
         throws ClassnotfoundException {
           if (name.endsWith("_Stub")) {
                 ClassWriter cw = new ClassWriter(0);
                 ...
                 byte[] b = cw.toByteArray();
                 return defineClass(name, b, 0, b.length);
            }
       return super.findClass(name);
      }
}
复制代码

转换类

第一步是将 ClassReader 产生的事件转给 ClassWriter。其结果是, 类编写器重新构建了由类读取器分析的类:

byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader(b1);
cr.accept(cw, 0);
byte[] b2 = cw.toByteArray(); // b2 和 b1 表示同一个类
复制代码

下一步是在类读取器和类写入器之间引入一个 ClassVisitor:

byte[] b1 = ...;
ClassWriter cw = new ClassWriter(0);
// cv 将所有事件转发给 cw
ClassVisitor cv = new ClassVisitor(ASM4, cw) { }; ClassReader cr = new ClassReader(b1);
cr.accept(cv, 0);
byte[] b2 = cw.toByteArray(); // b2 与 b1 表示同一个类
复制代码

图 2.6 给出了与上述代码相对应的体系结构,其中的组件用方框表示,事件用箭头表示:

为了能 够转换一个类,只需重写一些方法,筛选一些事件就足够了。例如,考虑下面的 ClassVisitor 子类:

public class ChangeVersionAdapter extends ClassVisitor{ 
    public ChangeVersionAdapter(ClassVisitor cv) {
       super(ASM4, cv);
    }
    
    @Override
    public void visit(int version, int access, String name,
    String signature, String superName, String[] interfaces) {
        cv.visit(V1_5, access, name, signature, superName, interfaces);
    }    
}
复制代码

这个类仅重写了 ClassVisitor 类的一个方法。结果,所有调用都被不加改变地转发到传 送给构造器的类访问器 cv,只有对 visit 方法的调用除外,在转发它时,对类版本号进行了修改。相应的程序图在图 2.7 中给出。

通过修改 visit 方法的其他参数,可以实现其他转换,而不仅仅是修改类的版本。例如, 可以向实现接口的列表中添加一个接口。还可以改变类的名字,但进行这种改变所需要做的工作 要多得多,不只是改变 visit 方法的 name 参数了。实际上,类的名字可以出现在一个已编译 类的许多不同地方,要真正实现类的重命名,必须修改类中出现的所有这些类名字。

前面的转换只修改了原类的四个字节。但是,在使用上面的代码时,整个 b1 均被分析,并 利用相应的事件从头从头构建了 b2,这种做法的效率不是很高。如果将 b1 中不被转换的部分 直接复制到 b2 中,不对其分析,也不生成相应的事件,其效率就会高得多。ASM 自动为方法 执行这一优化:

  • 在 ClassReader 组件的 accept 方法参数中传送了 ClassVisitor,如果 ClassReader 检测到这个 ClassVisitor 返回的 MethodVisitor 来自一个 ClassWriter,这意味着这个方法的内容将不会被转换,事实上,应用程序甚至不会 看到其内容。
  • 在这种情况下,ClassReader组件不会分析这个方法的内容,不会生成相应事件,只 是复制 ClassWriter 中表示这个方法的字节数组。

如果 ClassReader 和 ClassWriter 组件拥有对对方的引用,则由它们进行这种优化, 可设置如下:

byte[] b1 = ...
ClassReader cr = new ClassReader(b1);
ClassWriter cw = new ClassWriter(cr, 0); 
ChangeVersionAdapter ca = new ChangeVersionAdapter(cw); 
cr.accept(ca, 0);
byte[] b2 = cw.toByteArray();
复制代码

执行这一优化后,由于 ChangeVersionAdapter 没有转换任何方法,所以以上代码的 速度可以达到之前代码的两倍。

转换后的类 b2 可以存储在磁盘上,或者用 ClassLoader 加载。但在 ClassLoader 中执行的类转换只能转换由这个类加载器加载的类。如果希望转换所有类,则必 须将转换放在ClassFileTransformer内部,见java.lang.instrument包中的定义:

public static void premain(String agentArgs, Instrumentation inst) {
      inst.addTransformer(new ClassFileTransformer() {
       public byte[] transform(ClassLoader l, String name, Class c,
          ProtectionDomain d, byte[] b)
          throws IllegalClassFormatException {
             ClassReader cr = new ClassReader(b);
             ClassWriter cw = new ClassWriter(cr, 0);
             ClassVisitor cv = new ChangeVersionAdapter(cw);
             cr.accept(cv, 0);
             return cw.toByteArray();
} });
}
复制代码

移除类成员

通过改 变 visitField 和 visitMethod 方法的 access 或 name 参数,可以改变一个字段或一个方 法的修饰字段或名字。另外,除了在转发的方法调用中使用经过修改的参数之外,还可以选择根 本不转发该调用。其效果就是相应的类元素被移除。

下面的类适配器移除了有关外部类及内部类的信息,还删除了一个源文件的名字,也 就是由其编译这个类的源文件(所得到的类仍然具有全部功能,因为删除的这些元素仅用于调试 目的)。这一移除操作是通过在适当的访问方法中不转发任何内容而实现的:

public class RemoveDebugAdapter extends ClassVisitor {
      public RemoveDebugAdapter(ClassVisitor cv) {
       super(ASM4, cv);
      }
      @Override
      public void visitSource(String source, String debug) {
      }
      @Override
      public void visitOuterClass(String owner, String name, String desc) {
      }
      @Override
    public void visitInnerClass(String name, String outerName,
        String innerName, int access) {
    } 
}
复制代码

这一策略对于字段和方法是无效的,因为 visitField 和 visitMethod 方法必须返回一 个结果。要移除字段或方法,不得转发方法调用,并向调用者返回 null。例如,下面的类适配 器移除了一个方法,该方法由其名字及 述符指明(仅使用名字不足以标识一个方法,因为一个 类中可能包含若干个具有不同参数的同名方法):

public class RemoveMethodAdapter extends ClassVisitor {
      private String mName;
      private String mDesc;
      public RemoveMethodAdapter(
         ClassVisitor cv, String mName, String mDesc) {
           super(ASM4, cv);
           this.mName = mName;
           this.mDesc = mDesc;
      }
      @Override
      public MethodVisitor visitMethod(int access, String name,
         String desc, String signature, String[] exceptions) {
        if (name.equals(mName) && desc.equals(mDesc)) { // 不要委托至下一个访问器 -> 这样将移除该方法
            return null;
        }
       return cv.visitMethod(access, name, desc, signature, exceptions);
      }
}
复制代码

增加类成员

上述讨论的是少转发一些收到的调用,我们还可以多“转发”一些调用,也就是发出的调用 数多于收到的调用,其效果就是增加了类成员。新的调用可以插在原方法调用之间的若干位置, 只要遵守各个 visitXxx 必须遵循的调用顺序即可。

例如,如果要向一个类中添加一个字段,必须在原方法调用之间添加对 visitField 的一 个新调用,而且必须将这个新调用放在类适配器的一个访问方法中。比如,不能在 visit 方法 中这样做,因为这样可能会导致对 visitField 的调用之后跟有 visitSource、 visitOuterClass、visitAnnotation 或 visitAttribute,这是无效的。出于同样的原 因,不能将这个新调用放在 visitSource、visitOuterClass、visitAnnotation 或 visitAttribute 方法中. 仅有的可能位置是 visitInnerClass、visitField、 visitMethod 或 visitEnd 方法。

如果将这个新调用放在 visitEnd 方法中,那这个字段将总会被添加(除非增加显式条件), 因为这个方法总会被调用。如果将它放在 visitField 或 visitMethod 中,将会添加几个字 段:原类中的每个字段和方法各有一个相应的字段。这两种解决方案都可能发挥应有的作用;具 体取决于你的需求。例如,可以仅添加一个计数器字段,用于计算对一个对象的调用次数,也可 以为每个方法添加一个计数器,用于分别计算对每个方法的调用次数。

注意:事实上,惟一真正正确的解决方案是在 visitEnd 方法中添加更多调用,以添加新成员。实际上, 一个类中不得包含重复成员,要确保一个新成员没有重复成员,惟一方法就是将它与所有已有成员进行对 比,只有在 visitEnd 方法中访问了所有这些成员后才能完成这一工作。树 API 没有这一限制:可以在任意 时刻向使用这个 API 的转换中添加新成员。

下面给出一个类适配器,它会向类中添加一个字段,除非这个字段 已经存在:

public class AddFieldAdapter extends ClassVisitor {
      private int fAcc;
      private String fName;
      private String fDesc;
      private boolean isFieldPresent;
      public AddFieldAdapter(ClassVisitor cv, int fAcc, String fName,
         String fDesc) {
           super(ASM4, cv);
           this.fAcc = fAcc;
           this.fName = fName;
           this.fDesc = fDesc;
      }
      @Override
      public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
       if (name.equals(fName)) {
            isFieldPresent = true;
        }
       return cv.visitField(access, name, desc, signature, value);
      }
      @Override
      public void visitEnd() {
       if (!isFieldPresent) {
         FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);
         if (fv != null) {
           fv.visitEnd();
        } }
       cv.visitEnd();
      }
}
复制代码

这个字段被添加在 visitEnd 方法中。visitField 方法未被重写为修改已有字段或删除 一个字段,只是检测一下我们希望添加的字段是否已经存在。注意 visitEnd 方法中在调用 fv.visitEnd()之前的 fv != null 检测:这是因为一个类访问器可以在 visitField 中返 回 null

转换链

将几个适配器链接在一 起,就可以组成几个独立的类转换,以完成复杂转换。还要注意,转换链不一定是线性的。我们可以编写一个 ClassVisitor,将接收到的所有方法调用同时转发给几个 ClassVisitor:

public class MultiClassAdapter extends ClassVisitor { 
    protected ClassVisitor[]cvs;
    public MultiClassAdapter(ClassVisitor[]cvs) {
           super(ASM4);
           this.cvs = cvs;
    }
    @Override public void visit(int version, int access, String name,
             String signature, String superName, String[] interfaces) {
        for (ClassVisitor cv : cvs) {
            cv.visit(version, access, name, signature, superName, interfaces);
        }
    }
... }
复制代码

工具

Type

正如之前章节看到的,ASM API公开了存储在字节码中的类型,即内部名称和类型描述符。当然可以使用更加接近源码的方式去暴露它们,让它们有更好的可读性。但是这可能需要使用ClassReader和ClassWriter两个组件去进行系统转换,会导致效率变低。这就是为什么ASM没有透明的将内部名称和类型描述符转换成等价的源代码形式。然而ASM提供了Type类在必要的时候做这件事。

一个Type对象代表一个Java类型,它可以从一个类型描述符活着一个Class对象构建获得。Type类也包含静态的表示原始类型的常量。例如Type.INT_TYPE是表示int类型的对象。

getInternalName方法返回了一个Type的内部名称。例如,Type.getType(String.class).getInternalName()提供了String类的内部名称,即”java/lang/String”。这个方法只能被用于类和接口类型。

getDescriptor方法返回了Type的描述符。所以,可以用Type.getType(String.class).getDescriptor()来替代“Ljava/lang/String;”,可以用Type.INT_TYPE.getDescriptor().代替I

Type对象也可以表示一个方法类型。一个Type对象可以从一个方法描述符活着一个Method对象构建获得。getDescriptor方法会返回方法描述符的类型。此外,getArgumenttypes方法和getReturnType方法被用于从方法类型和方法返回类型构建Type对象。例如使用Type.getArgumentTypes(“(I)V”)返回的是Type.INT_TYPE,使用Type.getReturnType(“(I)V”)返回的是Type.VOID_TYPE对象。

TraceClassVisitor

为了检查一个构建和转换的类是否符合你去往的,根据ClassWriter返回的byte数组是没法判断的,因为人类没法读这个。使用文字表示将会更加的易于理解。这就是TraceClassVisitor所提供的功能。这个类正如它的名字一样,继承自ClassVisitor类,并且对访问的类进行了一个文字表示。所以可以使用TraceClassVisitor类替代ClassVisitor,来获得一个实际生成的有可读性的堆栈。更好的是,你可以同时使用两者。事实上,除了TraceClassVisitor的独有方法,其他的方法都会调用内部的ClassWriter实例实现:

ClassWriter cw = new ClassWriter(0);
TraceClassVisitor cv = new TraceClassVisitor(cw, printwriter);
cv.visit(...);
...
cv.visitEnd();
byte b[] = cw.toByteArray();
复制代码

这段代码创建了一个TraceClassVisitor去分发它接受到的所有的调用给cw,然后它打印了一个这些调用的文字表示使用printWriter。例如,在2.2.3例子中使用TraceClassVisitor将会打印出。

// class version 49.0 (49)
// access flags 1537
public abstract interface pkg/Comparable implements pkg/Mesurable {
// access flags 25
public final static I LESS = -1
// access flags 25
public final static I EQUAL = 0
// access flags 25
public final static I GREATER = 1
// access flags 1025
public abstract compareTo(Ljava/lang/Object;)I
}
复制代码

注意,你可以在一个转换链中的任意一个点中使用TraceClassVisitor,而不仅仅是在一个ClassWriter之前,这样你就可以看到这个点发生了什么。注意可以使用类的String.equals()方法同TraceClassvisitor的输出进行比较。

CheckClassAdapter

ClassWriter类并不能检查这个方法是否用正确的顺序和参数被调用。因此,可能会初始化一个错误的类从而被java虚拟机验证拒绝。为了避免这些错误。可以使用CheckClassAdaoter类。就想TraceClassVisitor,这个类也继承自ClasVisitor,并且分发所有的请求给另一个ClassVisitor实例。这个类的作用是在分发给下一个visitor之前检查方法调用顺序和参数。如果出错,那么会抛出IllegalStateException或者illegalargumentException异常。

为了检查一个类,打印这个类,最后获取这个类的byte数组,你可以这样做:

ClassWriter cw = new ClassWriter(0);
TraceClassVisitor tcv = new TraceClassVisitor(cw, printWriter);
CheckClassAdapter cv = new CheckClassAdapter(tcv);
cv.visit(...);
...
cv.visitEnd();
byte b[] = cw.toByteArray();
复制代码

注意,如果你使用其他的调用顺序,它们的结果表现也将会不同。例如,下面这个例子,检查将会发生在最后。

ClassWriter cw = new ClassWriter(0);
CheckClassAdapter cca = new CheckClassAdapter(cw);
TraceClassVisitor cv = new TraceClassVisitor(cca, printWriter);
复制代码

同TraceClasvisitor一样,你可以在转换链的任何地方使用它。

ASMifier

这个类为TraceClassVisitor工具提供了一个备用的后端。(默认是Textifier提供输出类型)。这个后端能在方法调用的时候直接输出Java源代码。例如调用visitEnd()方法打印cv.visitEnd(); 结果是一段字节码,但是使用ASMifier替换之后,会打印Java源代码。这在你使用visitor访问一个已经存在的class的时候会很有用。例如,如果你并不知道如何使用初始化编译好的类,你可以写下这个类的源码,然后使用javac编译,之后使用ASMifier访问编译好的类,你会获取到初始化这个字节码的ASM代码!

ASMifier类可以使用下面的命令行来调用。

java -classpath asm.jar:asm-util.jar \
  org.objectweb.asm.util.ASMifier \
  java.lang.Runnable
复制代码

之后会产生出下面的代码:

package asm.java.lang;
import org.objectweb.asm.*;
public class RunnableDump implements Opcodes {
    public static byte[] dump() throws Exception {
        ClassWriter cw = new ClassWriter(0);
        FieldVisitor fv;
        MethodVisitor mv;
        AnnotationVisitor av0;
        cw.visit(V1_5, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE,
            "java/lang/Runnable", null, "java/lang/Object", null); 
        {
            mv = cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "run", "()V", null, null);
            mv.visitEnd();
        }
        cw.visitEnd();
        return cw.toByteArray();
    }
}
复制代码

PS: 一个可以帮你生成ASM代码的工具类,如果你不知道怎么写,可以用这个工具类来获取代码。

转载于:https://juejin.im/post/5cdb67b151882503f649c88b

相关阅读

使用minimap+miniasm对nanopore进行基因组组装

我们用来练手的文章发表在 Nature Communication ,”High contiguity Arabidopsis thaliana genome assembly with a single nanop

反病毒工具-C32ASM

C32ASM 简介 国产静态反汇编利器.十六进制高端编辑器.提供方便的跳转以及彩色汇编语法.提供逐字节分析,进程Dump功能etc..其本

ASMM与AMM

ASMM(Automatic Shared Memory Management,自动共享内存管理)是Oracle 10g引入的概念。通过使用ASMM,就不需要手工设置相关内存组件的

能不能把ASMR做成一门正经生意?

ASMR,自发性知觉经络反应,说白了就类似模拟掏耳朵、剪头发的声音之类,来促进听者进入睡眠状态的音视频节目。01还记得去年曾经大火过

C#--反汇编工具ildasm.exe

通用中间语言(Common Intermediate Language,简称IL)是一种属于通用语言架构和 .NET 框架的的人类可读的编程语言。目标为 .NET 框架

分享到:

栏目导航

推荐阅读

热门阅读