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