asm bytecode manual - Tree API (3)

Introduction to the Tree API

Compared with the previous visit api, the Tree API is relatively easier to understand and more in line with human thinking, but the disadvantage is that it requires more memory and slower execution efficiency, but for our android development, 99% In most cases, we use asm to read and write bytecodes during compilation. The writing efficiency at this stage is obviously greater than the execution efficiency. So after learning the Tree API, it is basically enough for daily work

Tree API - Create 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 - delete method

Create a new java class


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


Our goal is to delete the test method of this class

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 - add fields

  
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 - Add method

This example will be more complicated, let's first look at the java class

public class MakeMethodDemo {
    private int f = 0;

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

Very simple code, right? Let's mainly look at the bytecode implementation of the checkAndSetF method. You can install the jclasslib plug-in

After make project, just show bytecode

image.png

You can see that his bytecode is actually:

image.png

At the same time, let's modify our java class and delete this method


public class MakeMethodDemo {
    private int f = 0;
    
}

Now we use the Tree API to add a checkAndSetF method to this class

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 - add part of code in method

Write a simple java class

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();
    }
}

We mainly look at the difference between the testTimer method and testTimer2 in the bytecode. We will use the Tree Api to modify the testTimer method to make its function consistent with testTimer2.

First look at the bytecode of testTimerimage.png

Look at the bytecode of testTimer2

image.png

After we compare it, we can quickly find the law: The key to our method 2 is to insert a bytecode before the return statement of method 1, and then insert a bytecode before the sleep statement

After figuring out the difference, we can start to modify the code:

Let's restore the original method first

public class AddTimerMethodTree {

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

}

Then you can start using Tree api to modify him

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())

In addition, there is a concept of expanding frame, which is useless in most cases

classReader.accept(classNode, ClassReader.SKIP_DEBUG)

At this point you print the log and you will find:

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节点就可以, 要注意的就是 代码中的简简单单一条语句,对应着字节码层面是多条指令, 前后的逻辑关系 要搞清楚。 写法上其实不是唯一的。

Guess you like

Origin juejin.im/post/7233626231579951163
ASM