asm字节码手册 - Tree API (三)
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 即可
可以看下他的字节码 实际是:
同时我们再把我们的 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 的字节码
再看下 testTimer2的字节码
我们比较了一下以后 很快就能发现其中的规律: 我们的方法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)
此时你打印日志 会发现:
我们的opcode 是对的 和jclasslib上可以一一对应上
但你如果传个默认值0,或者传了其他参数
classReader.accept(classNode, 0)
你就会看到这些展开frame的指令了, 一般情况下,我们skip debug 就足够了
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;
}
看一下他的字节码:
显然这个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节点就可以, 要注意的就是 代码中的简简单单一条语句,对应着字节码层面是多条指令, 前后的逻辑关系 要搞清楚。 写法上其实不是唯一的。
转载自:https://juejin.cn/post/7233626231579951163