ASM简介(六)

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是指令列表中的一员。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容