asm字节码手册 - Tree API (三)

570 阅读4分钟

Tree API 简介

和前面的visit api 相比,Tree API 相对而言 理解起来更加容易,写起来更符合人的思维,但是缺点就是要求的内存 更多,执行效率更慢, 但是对我们android开发来说, 99%的情况 我们使用asm 都是为了在编译期 读写字节码,这个阶段 编写效率 显然 是大于 执行效率的。 所以Tree API 学好了,基本上就足以应付日常工作了

Tree API - 创建Class

/*  
package pkg;  
public interface Comparable extends Measurable {  
int LESS = -1;  
int EQUAL = 0;  
int GREATER = 1;  
int compareTo(Object o);  
}  
  
可以按照任意顺序生成元素 写起来很方便
*/  
fun main() {  
val classNode = ClassNode()  
classNode.version = Opcodes.V1_5  
classNode.access = Opcodes.ACC_PUBLIC + Opcodes.ACC_INTERFACE + Opcodes.ACC_ABSTRACT  
classNode.name = "pkg/Comparable"  
classNode.superName = "java/lang/Object"  
classNode.interfaces.add("pkg/Measurable")  
classNode.fields.add(  
FieldNode(  
Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,  
"LESS",  
"I",  
null,  
-1  
)  
)  
classNode.fields.add(  
FieldNode(  
Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,  
"EQUAL",  
"I",  
null,  
0  
)  
)  
classNode.fields.add(  
FieldNode(  
Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL + Opcodes.ACC_STATIC,  
"GREATER",  
"I",  
null,  
1  
)  
)  
classNode.methods.add(  
MethodNode(  
Opcodes.ACC_PUBLIC + Opcodes.ACC_ABSTRACT,  
"compareTo",  
"(Ljava/lang/Object;)I",  
null,  
null  
)  
)  
}

Tree API - 删除方法

新建一个java 类


public class RemoveMethodDemo {  
public void test() {  
}  
  
public void test2() {  
}  
}


我们的目标是 把这个类的test 方法删除

fun main() {  
val classReader = ClassReader("com.vivo.RemoveMethodDemo")  
val classNode = ClassNode()  
classReader.accept(classNode, ClassReader.SKIP_DEBUG)  
// 这里其实就是遍历一下方法 就可以了  
val it = classNode.methods!!.iterator()  
while (it.hasNext()){  
val methodNode = it.next()  
if (methodNode.name == "test" && methodNode.desc == "()V") {  
it.remove()  
}  
}  
val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)  
classNode.accept(classWriter)  
//输出文件查看  
ClassOutputUtil.byte2File("path/files/RemoveMethodDemo.class", classWriter.toByteArray())  
}

Tree API - 增加字段

  
fun main() {  
val classReader = ClassReader("com.andoter.asm_example.part6.RemoveMethodDemo")  
val classNode = ClassNode()  
classReader.accept(classNode, ClassReader.SKIP_DEBUG)  
  
//  
classNode.fields.add(FieldNode(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "addField", "Ljava/lang/String;", null, null))  
  
// 尝试输出进行查看  
val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)  
classNode.accept(classWriter)  
//输出文件查看  
ClassOutputUtil.byte2File("asm_example/files/RemoveMethodDemo.class", classWriter.toByteArray())  
}  

Tree API - 添加方法

这个例子会复杂一些,我们首先看一下java 类

public class MakeMethodDemo {
    private int f = 0;

    public void checkAndSetF(int fparams) {
        if (fparams >= 0) {
            f = fparams;
        } else {
            throw new IllegalArgumentException();
        }
    }
}

很简单的代码对吧, 我们主要看一下checkAndSetF 这个方法的字节码实现,可以装一下jclasslib这个插件

make工程以后 ,show bytecode 即可

image.png

可以看下他的字节码 实际是:

image.png

同时我们再把我们的 java 类修改一下,把这个方法删掉


public class MakeMethodDemo {
    private int f = 0;
    
}

现在我们利用Tree API,来实现 给这个类加上一个checkAndSetF 方法

fun main() {
    val classReader = ClassReader("com.andoter.asm_example.part7.MakeMethodDemo")
    val classNode = ClassNode()
    classReader.accept(classNode, ClassReader.SKIP_DEBUG)

    val mn = MethodNode(Opcodes.ACC_PUBLIC, "checkAndSetF", "(I)V", null, null)
    val list = mn.instructions
    // iload_1 将参数压入栈顶
    list.add(VarInsnNode(Opcodes.ILOAD, 1))

    val l1 = LabelNode()
    // 如果栈顶的int 值小于0 则跳转到 12条指令继续执行
    list.add(JumpInsnNode(Opcodes.IFLT, l1))
    // 将常量压入栈顶
    list.add(VarInsnNode(Opcodes.ALOAD, 0))
    list.add(VarInsnNode(Opcodes.ILOAD, 1))
    // 赋值
    list.add(FieldInsnNode(Opcodes.PUTFIELD, "com/andoter/asm_example/part7/MakeMethodDemo", "f", "I"))
    val l2 = LabelNode()
    list.add(JumpInsnNode(Opcodes.GOTO, l2))
    list.add(l1) // 12
    list.add(TypeInsnNode(Opcodes.NEW, "java/lang/IllegalArgumentException"))
    list.add(InsnNode(Opcodes.DUP))
    list.add(MethodInsnNode(Opcodes.INVOKESPECIAL, "java/lang/IllegalArgumentException", "<init>", "()V", false))
    list.add(InsnNode(Opcodes.ATHROW))
    list.add(l2)
    list.add(InsnNode(Opcodes.RETURN))

    classNode.methods.add(mn)

    // 尝试输出进行查看
    val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
    classNode.accept(classWriter)
    // 输出文件查看
    ClassOutputUtil.byte2File("asm_example/files/MakeMethodDemo.class", classWriter.toByteArray())
}

Tree API - 在方法中 添加部分代码

写一个简单的java类

public class AddTimerMethodTree {

    public static long timer;


    // 原始方法
    public void testTimer() throws InterruptedException {
        Thread.sleep(1000);
    }

    // 想修改成的方法
    public void testTimer2() throws InterruptedException   {
        timer -= System.currentTimeMillis();
        Thread.sleep(1000L);
        timer += System.currentTimeMillis();
    }
}

我们主要就是看testTimer方法和testTimer2 这2个方法 在字节码上的区别,一会我们会利用Tree Api 来修改这个testTimer方法,让他的功能和testTimer2保持一致

首先看下testTimer 的字节码 image.png

再看下 testTimer2的字节码

image.png

我们比较了一下以后 很快就能发现其中的规律: 我们的方法2 其实关键就是在方法1的 return语句 之前 插入了一段字节码,然后在 sleep语句之前也插入一段字节码

搞清楚这其中的不同以后 我们就可以 来着手修改代码了:

我们先还原一下 原始方法

public class AddTimerMethodTree {

    // 原始方法
    public void testTimer() throws InterruptedException {
        Thread.sleep(1000);
    }

}

然后就可以开始 用Tree api 去修改他

val classNode = ClassNode()
val classReader = ClassReader("com.andoter.asm_example.part7.AddTimerMethodTree")
classReader.accept(classNode, 0)
classNode.methods?.forEach {
    if (it.name == "testTimer") {
        val inlist = it.instructions
        val iterator = inlist.iterator()
        while (iterator.hasNext()) {
            val inNode = iterator.next()
            if (inNode.opcode == Opcodes.RETURN) {
                // 在return 指令之前 插入 指令
                val insnListAfter = InsnList()
                // getstatic #5 <com/andoter/asm_example/part7/AddTimerMethodTree.timer : J>
                insnListAfter.add(FieldInsnNode(Opcodes.GETSTATIC, classNode.name, "timer", "J"))
                // 19 invokestatic #6 <java/lang/System.currentTimeMillis
                insnListAfter.add(
                    MethodInsnNode(
                        Opcodes.INVOKESTATIC,
                        "java/lang/System",
                        "currentTimeMillis",
                        "()J",
                        false,
                    ),
                )
                // ladd
                insnListAfter.add(InsnNode(Opcodes.LADD))
                // 3 putstatic #5 <com/andoter/asm_example/part7/AddTimerMethodTree.timer : J>
                insnListAfter.add(FieldInsnNode(Opcodes.PUTSTATIC, classNode.name, "timer", "J"))
                inlist.insert(inNode.previous, insnListAfter) // 在当前的指令前面插入代码
            }
        }

        val beforeList = InsnList()
        beforeList.add(FieldInsnNode(Opcodes.GETSTATIC, classNode.name, "timer", "J"))
        beforeList.add(
            MethodInsnNode(
                Opcodes.INVOKESTATIC,
                "java/lang/System",
                "currentTimeMillis",
                "()J",
                false,
            ),
        )
        beforeList.add(InsnNode(Opcodes.LSUB))
        beforeList.add(FieldInsnNode(Opcodes.PUTSTATIC, classNode.name, "timer", "J"))
        inlist.insert(beforeList)
    }
}
// 不要遗漏增加我们的变量
val acc = Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC
classNode.fields.add(FieldNode(acc, "timer", "J", null, null))

val classWriter = ClassWriter(ClassWriter.COMPUTE_MAXS)
classNode.accept(classWriter)
// 输出文件查看
ClassOutputUtil.byte2File("asm_example/files/AddTimerMethodTree.class", classWriter.toByteArray())

另外还有一个展开frame的概念,多数情况下 他并没有什么用

classReader.accept(classNode, ClassReader.SKIP_DEBUG)

此时你打印日志 会发现:

image.png

我们的opcode 是对的 和jclasslib上可以一一对应上

但你如果传个默认值0,或者传了其他参数

classReader.accept(classNode, 0)

你就会看到这些展开frame的指令了, 一般情况下,我们skip debug 就足够了

image.png

Tree API - 在方法中 删除部分代码

看看下面这段代码 注意一下set 方法

public class BeanField {
    public int getF() {
        return f;
    }

    public void setF(int value) {
        this.f = f;
        this.f = value;
    }

    private int f =1;
}

看一下他的字节码:

image.png

显然这个setF的方法 是不对的。 我想把这个this.f=f 这个愚蠢的代码删掉

要删掉 这个代码 其实看下字节码就知道了,我们最简单的方式 就是把前面四条指令都删掉 就可以了

val iterator = classNode.methods.iterator()
while (iterator.hasNext()) {
    val methodNode = iterator.next()
    if (methodNode.name == "setF") {
        val insnList = methodNode.instructions
        val iterator = insnList.iterator()
        while (iterator.hasNext()) {
            var insnNode = iterator.next()
            if (insnNode.opcode == Opcodes.ALOAD) {
                val next1 = iterator.next()
                if (next1.opcode == Opcodes.ALOAD) {
                    val next2 = iterator.next()
                    if (next2.opcode == Opcodes.GETFIELD) {
                        val next3 = iterator.next()
                        if (next3.opcode == Opcodes.PUTFIELD) {
                            insnList.remove(insnNode)
                            insnList.remove(next1)
                            insnList.remove(next2)
                            insnList.remove(next3)
                        }
                    }
                }
            }
        }
    }
}

删除方法 其实没什么难的,主要就是 remove 这个node节点就可以, 要注意的就是 代码中的简简单单一条语句,对应着字节码层面是多条指令, 前后的逻辑关系 要搞清楚。 写法上其实不是唯一的。