ASM简介(六)

作者: 千里山南 | 来源:发表于2016-08-04 21:54 被阅读779次

TreeAPI

Class

ASM中修改生成class主要依赖ClassNode类

public class ClassNode ... {
    public int version;
    public int access;
    public String name;
    public String signature;
    public String superName;
    public List<String> interfaces;
    public String sourceFile;
    public String sourceDebug;
    public String outerClass;
    public String outerMethod;
    public String outerMethodDesc;
    public List<AnnotationNode> visibleAnnotations;
    public List<AnnotationNode> invisibleAnnotations;
    public List<Attribute> attrs;
    public List<InnerClassNode> innerClasses;
    public List<FieldNode> fields;
    public List<MethodNode> methods;
}

生成class时我们只需构造对应的ClassNode即可。不过TreeAPI比CoreAPI慢30%左右,内存占用也高。
修改Class,我们只需使用ClassTransformer,然后在transform方法中修改对应的ClassNode即可。使用TreeAPI比CoreAPI更耗时,内存占用也多,但是对于某些复杂的修改也相对简单。
treeAPI被设计用于那些使用coreAPI一遍解析无法完成,需要解析多次的场景。

使用ClassNode

ClassNode继承于ClassVisitor,其含有accept函数可以接受ClassVisitor。我们从一个ByteArray构造一个ClassNode的方式如下:

ClassNode cn = new ClassNode();
ClassReader cr = new ClassReader(...);
cr.accept(cn, 0);

相反,我们也可以将一个ClassNode转化为ByteArray

ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
byte[] b = cw.toByteArray();

我们也可以将ClassNode当作普通的ClassVisit,只需在适当的时候调用accept方法将时间传递到后续的Visit即可。

Methods

对于函数,我们可以使用MethodNode来表达其数据结构。

public class MethodNode ... {
    public int access;
    public String name;
    public String desc;
    public String signature;
    public List<String> exceptions;
    public List<AnnotationNode> visibleAnnotations;
    public List<AnnotationNode> invisibleAnnotations;
    public List<Attribute> attrs;
    public Object annotationDefault;
    public List<AnnotationNode>[] visibleParameterAnnotations;
    public List<AnnotationNode>[] invisibleParameterAnnotations;
    public InsnList instructions;
    public List<TryCatchBlockNode> tryCatchBlocks;
    public List<LocalVariableNode> localVariables;
    public int maxStack;
    public int maxLocals;
}

这里面的成员基本和ClassNode类似,其中最重要的是instructions对象,它是InsnList类型

public class InsnList { // public accessors omitted
    int size();
    AbstractInsnNode getFirst();
    AbstractInsnNode getLast();
    AbstractInsnNode get(int index);
    boolean contains(AbstractInsnNode insn);
    int indexOf(AbstractInsnNode insn);
    void accept(MethodVisitor mv);
    ListIterator iterator();
    ListIterator iterator(int index);
    AbstractInsnNode[] toArray();
    void set(AbstractInsnNode location, AbstractInsnNode insn);
    void add(AbstractInsnNode insn);
    void add(InsnList insns);
    void insert(AbstractInsnNode insn);
    void insert(InsnList insns);
    void insert(AbstractInsnNode location, AbstractInsnNode insn);
    void insert(AbstractInsnNode location, InsnList insns);
    void insertBefore(AbstractInsnNode location, AbstractInsnNode insn);
    void insertBefore(AbstractInsnNode location, InsnList insns);
    void remove(AbstractInsnNode insn);
    void clear();
}

InsnList对象可以看作是指令的链表。其主要有以下的特性:

  • 同一个AbstractInsnNode对象最多在链表中出现一次
  • 同一个AbstractInsNode不能同时属于多个InsnList对象
  • 因此,如果将一个AbstractInsNode添加到链表中,需要先从之前的链表中将其移除。

AbstractInsnNode代表了一条字节码指令,其格式如下:

public abstract class AbstractInsnNode {
    public int getOpcode();
    public int getType();
    public AbstractInsnNode getPrevious();
    public AbstractInsnNode getNext();
    public void accept(MethodVisitor cv);
    public AbstractInsnNode clone(Map labels);
}

其XxxInsnNode子类和visitXxxInsn 函数相对应。labels和frames以及lineNumbers,尽管它们不属于指令,也使用AbstractInsnNode的子类来表示:LabelNode,FrameNode,LineNumberNode。
生成一个方法的示例如下:

MethodNode mn = new MethodNode(...);
InsnList il = mn.instructions;
il.add(new VarInsnNode(ILOAD, 1));
LabelNode label = new LabelNode();
il.add(new JumpInsnNode(IFLT, label));
il.add(new VarInsnNode(ALOAD, 0));
il.add(new VarInsnNode(ILOAD, 1));
il.add(new FieldInsnNode(PUTFIELD, "pkg/Bean", "f", "I"));
LabelNode end = new LabelNode();
il.add(new JumpInsnNode(GOTO, end));
il.add(label);
il.add(new FrameNode(F_SAME, 0, null, 0, null));
il.add(new TypeInsnNode(NEW, "java/lang/IllegalArgumentException"));
il.add(new InsnNode(DUP));
il.add(new MethodInsnNode(INVOKESPECIAL,
                          "java/lang/IllegalArgumentException", "<init>", "()V"));
il.add(new InsnNode(ATHROW));
il.add(end);
il.add(new FrameNode(F_SAME, 0, null, 0, null));
il.add(new InsnNode(RETURN));
mn.maxStack = 2;
mn.maxLocals = 2;

和生成Class一样,这种方式生成方法比CoreAPI慢,并且占用更多内存,但是它的好处是可以以任意的顺序进行构建。
修改Method,我们只需修改MethodNode对象即可。一种常见的修改是我们直接修改InsnList对象。另一种是先将需要插入的指令保存到临时的InsnList中,最后统一插入到InsnList中,这种方式更高效。InsnList我们在其Iterat过程中可以添加或者删除某个item.
如果我们需要修改的指令依赖一个距离比较远的指令,此时使用TreeAPI会方便更多。
MethodNode同样也是继承MethodVisitor.

代码分析相关

代码分析相关的技术庞大而繁杂,我们这里主要介绍两种分析方法:数据流分析和控制流分析

  • 数据流分析:该方法分析函数每一个执行帧的状态,这些状态一般归纳为一系列抽象的状态。例如引用数据类型的值只有三种状态:null 非null 可能为null
    数据流分析可以按照两种不同的方式进行:向前分析,对于一个指令主要关注执行前到执行后变化。向后分析主要执行后和执行前的差别。数据流分析主要通过模拟函数中各个指令的执行。这个看似和jvm虚拟机所做的工作类似,但是模拟更关注函数执行的方方面面,包括各种各样的边界条件。简单来说,对于一个分支语句,模拟会把各个分支都走一遍,而虚拟机可能只走一条分支。
  • 控制流分析主要分析函数控制流程,并且分析这些流程。
    控制流会把每一个分支单独分成一块,每一块包含一系列指令,最后一条指令代表整个分支执行成功,每个流程块中只有第一条指令可以作为跳转的目标。
    ASM提供了一些代码分析相关的组件。它们主要分布在org.objectweb.adm.tree.analysis包中。它们是基于TreeAPI的。
    做数据流分析的时候我们既可以使用ASM提供的数据描述,也可以使用我们自己定义的数据范围(Interpreter、Value),尽管Analyzer主要用于数据流分析,它同时也能构造控制流表,通过复写newControlFlowEdge和newControlFlowExceptionEdge
    BasicInterPreter类是InterPreter类的子类,它可以模拟对字节码指令使用预定义的数据类型进行测试。这些预定义数据包括BasicValue中的:
  • UNINITIALIZED_VALUE 代表所有可能的值
  • INT_VALUE 代表所有的int short byte boolean 或者char类型的数据
  • FLOAT_VALUE
  • LONG_VALUE
  • DOUBLE_VALUE
  • REFERENCE_VALUE 所有的引用数据类型
  • RETURNADDRESS_VALUE 主要用于子程序

这个分析器主要用于作为默认的分析器构造Analyzer。这个Analyzer可以用于查看无法抵达的代码区域。对于不可达的代码区域无论Interpreter的实现是怎样的,其覆盖的frames(Analyzer.getFrames返回的)都是null.因此我们可以移除这些不可达的代码:

public class RemoveDeadCodeAdapter extends MethodVisitor {
    String owner;
    MethodVisitor next;
    public RemoveDeadCodeAdapter(String owner, int access, String name,
                                 String desc, MethodVisitor mv) {
        super(ASM4, new MethodNode(access, name, desc, null, null));
        this.owner = owner;
        next = mv;
    }
    @Override public void visitEnd() {
        MethodNode mn = (MethodNode) mv;
        Analyzer<BasicValue> a =
                new Analyzer<BasicValue>(new BasicInterpreter());
        try {
            a.analyze(owner, mn);
            Frame<BasicValue>[] frames = a.getFrames();
            AbstractInsnNode[] insns = mn.instructions.toArray();
            for (int i = 0; i < frames.length; ++i) {
                if (frames[i] == null && !(insns[i] instanceof LabelNode)) {
                    mn.instructions.remove(insns[i]);
                }
            }
        } catch (AnalyzerException ignored) {
        }
        mn.accept(next);
    }
}

基础数据流分析

BasicVerifier是继承BasicInterpreter,和BasicInterpreter不同的是BasicVerify主要检查指令的合法性。举例来说它有可能会检查IADD操作的数据是INTEGER_VALUE。这个类可以用于检查自己生成的class文件的合法性。比如,这个类可以检测到诸如 ISTORE 1 ALOAD 1是非法的指令。

public class BasicVerifierAdapter extends MethodVisitor {
String owner;
MethodVisitor next;
public class BasicVerifierAdapter extends MethodVisitor {
    String owner;
    MethodVisitor next;
    public BasicVerifierAdapter(String owner, int access, String name,
                                String desc, MethodVisitor mv) {
        super(ASM4, new MethodNode(access, name, desc, null, null));
        this.owner = owner;
        next = mv;
    }
    @Override public void visitEnd() {
        MethodNode mn = (MethodNode) mv;
        Analyzer<BasicValue> a =
                new Analyzer<BasicValue(new BasicVerifier());
        try {
            a.analyze(owner, mn);
        } catch (AnalyzerException e) {
            throw new RuntimeException(e.getMessage());
        }
        mn.accept(next);
    }
}

SimpleVerifier 是继承 BasicVerifier的,它使用更多的测试用例来检测字节码指令。例如它可以检测调用的函数是否属于该实例。该类是通过反射来校验对应的函数是否存在的。此类同样可以用于class文件合法性的校验。当然也可以用于其他用途,比如用于删除代码中不必要的类型转换。

@Override public MethodNode transform(MethodNode mn) {
    Analyzer<BasicValue> a =
            new Analyzer<BasicValue>(new SimpleVerifier());
    try {
        a.analyze(owner, mn);
        Frame<BasicValue>[] frames = a.getFrames();
        AbstractInsnNode[] insns = mn.instructions.toArray();
        for (int i = 0; i < insns.length; ++i) {
            AbstractInsnNode insn = insns[i];
            if (insn.getOpcode() == CHECKCAST) {
                Frame f = frames[i];
                if (f != null && f.getStackSize() > 0) {
                    Object operand = f.getStack(f.getStackSize() - 1);
                    Class<?> to = getClass(((TypeInsnNode) insn).desc);
                    Class<?> from = getClass(((BasicValue) operand).getType());
                    if (to.isAssignableFrom(from)) {
                        mn.instructions.remove(insn);
                    }
                }
            }
        }
    } catch (AnalyzerException ignored) {
    }
    return mt == null ? mn : mt.transform(mn);
}

用户自定义数据流分析

我们认为如果是ACONST_NULL我们就认为是一个NULL的赋值语句,如果是一个引用的赋值,我们认为其有可能为空。那么就可以找出赋值语句中所有有可能为空的地方:

class IsNullInterpreter extends BasicInterpreter {
    public final static BasicValue NULL = new BasicValue(null);
    public final static BasicValue MAYBENULL = new BasicValue(null);
    public IsNullInterpreter() {
        super(ASM4);
    }
    @Override public BasicValue newOperation(AbstractInsnNode insn) {
        if (insn.getOpcode() == ACONST_NULL) {
            return NULL;
        }
        return super.newOperation(insn);
    }
    @Override public BasicValue merge(BasicValue v, BasicValue w) {
        if (isRef(v) && isRef(w) && v != w) {
            return MAYBENULL;
        }
        return super.merge(v, w);
    }
    private boolean isRef(Value v) {
        return v == REFERENCE_VALUE || v == NULL || v == MAYBENULL;
    }
}

而我们使用这个Interpreter很容易找到所有有可能出现空指针的地方:

public class NullDereferenceAnalyzer {
    public List<AbstractInsnNode> findNullDereferences(String owner,
                                                       MethodNode mn) throws AnalyzerException {
        List<AbstractInsnNode> result = new ArrayList<AbstractInsnNode>();
        Analyzer<BasicValue> a =
                new Analyzer<BasicValue>(new IsNullInterpreter());
        a.analyze(owner, mn);
        Frame<BasicValue>[] frames = a.getFrames();
        AbstractInsnNode[] insns = mn.instructions.toArray();
        for (int i = 0; i < insns.length; ++i) {
            AbstractInsnNode insn = insns[i];
            if (frames[i] != null) {
                Value v = getTarget(insn, frames[i]);
                if (v == NULL || v == MAYBENULL) {
                    result.add(insn);
                }
            }
        }
        return result;
    }
    private static BasicValue getTarget(AbstractInsnNode insn,
                                        Frame<BasicValue> f) {
        switch (insn.getOpcode()) {
            case GETFIELD:
            case ARRAYLENGTH:
            case MONITORENTER:
            case MONITOREXIT:
                return getStackValue(f, 0);
            case PUTFIELD:
                return getStackValue(f, 1);
            case INVOKEVIRTUAL:
            case INVOKESPECIAL:
            case INVOKEINTERFACE:
                String desc = ((MethodInsnNode) insn).desc;
                return getStackValue(f, Type.getArgumentTypes(desc).length);
        }
        return null;
    }
    private static BasicValue getStackValue(Frame<BasicValue> f,
                                            int index) {
        int top = f.getStackSize() - 1;
        return index <= top ? f.getStack(top - index) : null;
    }
}

控制流分析

控制流分析使用场景也比较多,一个简单的场景是分析方法的控制流复杂度。通常使用控制流图标中边数减去节点数然后加上二代表复杂度。这个复杂度可以用于表示函数的复杂度(这个和方法中出现的bug数量有一定的相关性)。当然它和该函数测试用例的建议性条数也相关。

Metadata

泛型

TreeAPI暂时不支持泛型

注解

主要使用AnnotationNode来描述

public class AnnotationNode extends AnnotationVisitor {
    public String desc;
    public List<Object> values;
    public AnnotationNode(String desc);
    public AnnotationNode(int api, String desc);
    ... // methods of the AnnotationVisitor interface
    public void accept(AnnotationVisitor av);
}

desc代表泛型的类型,values包含了name和value的键值对。AnnotationNode是继承于AnnotationVisitor。

Debug相关

class对应的源文件的相关信息被存储在ClassNode的sourceFile字段中。行号被存储在LineNumberNode中,LineNumberNode是指令列表中的一员。

相关文章

  • ASM简介(六)

    TreeAPI Class ASM中修改生成class主要依赖ClassNode类 生成class时我们只需构造对...

  • ASM 简介

    前言 很早之前就写过面向切面的编程思想,主要学习了AOP的思想(参考:AOP简介)以及使用 AspectJ 实现简...

  • ASM简介(一)

    之前简单研究过ASM这个字节码修改框架,最近要用到,故简单复习下。顺便翻译下官方文档(翻译主要是给自己看的,因此比...

  • ASM简介(二)

    访问class 访问一个class的最简单的方式是声明一个ClassReader类,然后复写其中的方法。Class...

  • ASM简介(三)

    使用修改类 如果我们一次性修改的类比较多,如果想使用这些类,我们可以使用java.lang.instrument....

  • ASM简介(四)

    函数 我们在使用ASM相关API对函数进行操作之前,我们需要了解函数在字节码的存储格式及其执行模型。 执行模型 我...

  • ASM简介(五)

    元数据 泛型 泛型在运行时并不会被字节码指令使用,但可以被反射API拿到,可以被编译器使用。由于向前兼容的原因,泛...

  • 工具开发,字节码技术

    简介 几个对比: https://segmentfault.com/a/1190000009956534ASM(A...

  • Android编译时技术04 --- ASM

    部分内容来自:https://www.jianshu.com/p/29e9b03c0796 一.ASM简介 ASM...

  • ASM Core Api 详解

    前言 前面一篇文章 ASM 简介 对 ASM 框架做了简单的介绍。 本篇文章主要对该框架的 Core Api 其中...

网友评论

    本文标题:ASM简介(六)

    本文链接:https://www.haomeiwen.com/subject/pwrfsttx.html