[Android][ASM]指令注入入门(四)——实际需求实现(中)
前言
在上一篇中,我们实现了无返回值的方法收尾插入Binder.clearCallingIdentity()/Binder.restoreCallingIdentity()
方法;
而这一篇,会在此基础上,将使我们的工具支持带返回值的方法;
开发步骤
源码端
```java
import java.util.Random;
public class Test2 {
private static final Random R = new Random();
public static void main(String[] args) {
Test2 instance = new Test2();
instance.call(R.nextInt());
//Add begin
System.out.println("getInt:" + instance.getInt());
System.out.println("getInt:" + instance.getInt(R.nextInt()));
System.out.println("getInt2:" + instance.getInt2(R.nextInt()));
//Add end
}
private void call(int arg) {
System.out.println("call-->" + arg + 1);
}
//Add begin
private int getInt() {
return -1;
}
private int getInt(int intVal) {
return intVal * -1;
}
private int getInt2(int intVal) {
int ret = intVal + 1;
return ret;
}
//Add end
}
这里先用返回值为int
基本数据类型的来尝试,并写出了三种常见的结构:
- 返回一个固定值;
- 基于传入参数进行简单计算后返回;
- 基于传入参数进行复杂计算后返回,期间涉及多个变量赋值情况;
编译:
ryan ~/workspace/src $ javac -g Test2.java android/os/Binder.java
对生成的Test2.class
进行反汇编:(关于javap
的使用会单独写一篇介绍,在此先不展开介绍)
ryan ~/workspace/src $ javap -c -s -l -p Test2
得到如下结果(节选):
private int getInt();
descriptor: ()I
Code:
0: iconst_m1
1: ireturn
LineNumberTable:
line 28: 0
LocalVariableTable:
Start Length Slot Name Signature
0 2 0 this LTest2;
private int getInt(int);
descriptor: (I)I
Code:
0: iload_1
1: iconst_m1
2: imul
3: ireturn
LineNumberTable:
line 32: 0
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this LTest2;
0 4 1 intVal I
private int getInt2(int);
descriptor: (I)I
Code:
0: iload_1
1: iconst_1
2: iadd
3: istore_2
4: iload_2
5: ireturn
LineNumberTable:
line 37: 0
line 39: 4
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this LTest2;
0 6 1 intVal I
4 2 2 ret I
通过观察,可以得出上述三种方法的规律:
- 返回一个固定值——
ireturn
指令之前即为iconst_m?
; - 基于传入参数进行简单计算后返回——
ireturn
指令之前会有iload_?
,但两者之间存在其他运算指令; - 基于传入参数进行复杂计算后返回,期间涉及多个变量赋值情况——
ireturn
指令之前即为iload_?
;
针对第一种情况,实际上是不需要处理的,但是为了展示,我们还是尝试处理下:
在不考虑效率问题的前提下,以下两个方法实现在逻辑上是等效的:
private int getInt() {
return -1;
}
private int getInt() {
int ret = -1;
return ret;
}
而后者通过javap
拆解字节码后得到的结果如下:
private int getInt();
descriptor: ()I
Code:
0: iconst_m1
1: istore_1
2: iload_1
3: ireturn
LineNumberTable:
line 28: 0
line 29: 2
LocalVariableTable:
Start Length Slot Name Signature
0 4 0 this LTest2;
2 2 1 ret I
因此可以认为,第一种情况可以通过在ireturn
之前插入istore_?
与iload_?
指令,将其转化为第三种情况;
至于第二、第三种情况,则需要分情况处理:
ireturn
与iload_?
之间有间隔,则在ireturn
之前插入Binder.restoreCallingIdentity()
,并再次插入iload_?
指令;ireturn
与iload_?
之间无间隔,则在iload_?
之前插入Binder.restoreCallingIdentity()
;
工具端
基于上一篇的代码状态,修改visitEnd()
,并添加必要的方法封装:
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.LabelNode;
import org.objectweb.asm.tree.LocalVariableNode;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.VarInsnNode;
public class MethodRegionInjector {
//与之前StringModifier.java基本一致,仅改变输入、输出文件名
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream("../src/Test2.class");
FileOutputStream fos = new FileOutputStream("../out/Test2.class");
convert(fis, fos);
fos.flush();
fis.close();
fos.close();
}
//与之前StringModifier.java基本一致,仅改变visitor类型
private static void convert(InputStream in, OutputStream out)
throws IOException {
ClassReader reader = new ClassReader(in);
ClassWriter writer = new ClassWriter(0);
MethodRegionInjectorClassVisitor visitor = new MethodRegionInjectorClassVisitor(writer);
reader.accept(visitor, 0);
byte[] data = writer.toByteArray();
out.write(data);
}
//与之前StringModifier.StringModificationClassVisitor基本一致,仅改变visitMethod的实现
private static class MethodRegionInjectorClassVisitor extends ClassVisitor {
private String currentClassName = null;
private MethodRegionInjectorClassVisitor(ClassWriter writer) {
super(Opcodes.ASM6, writer);
}
@Override
public void visit(int version, int access, String name, String signature, String superName,
String[] interfaces) {
currentClassName = name;
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature,
String[] exceptions) {
MethodVisitor chain = super.visitMethod(access, name, desc, signature, exceptions);
//过滤类名、方法名,忽略构造方法、静态代码块
if ("Test2".equals(currentClassName) && !"<init>".equals(name) && !"<clinit>".equals(name) && (access & Opcodes.ACC_STATIC) == 0) {
MethodNode mn = new MethodNode(Opcodes.ASM6, access, name, desc, signature, exceptions);
return new MethodRegionInjectorMethodVisitor(currentClassName, chain, mn);
} else {
return chain;
}
}
}
//visitEnd方法是这个类与StringModifier.java的主要差异,也是核心代码实现的地方
private static class MethodRegionInjectorMethodVisitor extends MethodVisitor {
private MethodVisitor chain;
private String owner;
private MethodRegionInjectorMethodVisitor(String owner, MethodVisitor chain, MethodNode mn) {
super(Opcodes.ASM6, mn);
this.owner = owner;
this.chain = chain;
}
public void visitEnd() {
...
if (varIndex > 0) {
...
//由于需要定位return的语句,因此从末尾开始效率更高
for (int i = instructions.size() - 1; i >= 0; i--) {
AbstractInsnNode node = instructions.get(i);
int opCode = node.getOpcode();
//如果遇到无参的返回指令,则在此之前插入两条指令:LLOAD与INVOKESTATIC
if (opCode == Opcodes.RETURN) {
VarInsnNode varLoadCall = new VarInsnNode(Opcodes.LLOAD, varIndex);
instructions.insertBefore(node, varLoadCall);
MethodInsnNode postCall = new MethodInsnNode(Opcodes.INVOKESTATIC,
"android/os/Binder","restoreCallingIdentity", "(J)V", false);
instructions.insert(varLoadCall, postCall);
break;
//Add begin
//如果是带返回值的返回指令,则需要额外处理
} else if (isReturnInstruction(opCode)) {
//获取该条返回指令之前的一条指令;
AbstractInsnNode previousNode = instructions.get(i - 1);
// 如果为为ICONST/BIPUSH/SIPUCH/LDC,则插入xSTORE与xLOAD,再在xLOAD之前插入restoreCallingIdentity(第一种情况)
int prevousOpcode = previousNode.getOpcode();
if (isConstOpcode(prevousOpcode)) {
//需要先创建一个局部变量来保存该值
int tmpVarIndex = varIndex + 2;
String tmpVarType = getParamTypeByOpcode(prevousOpcode);
if (tmpVarType == null) {
continue;
}
LocalVariableNode tmpVarNode = new LocalVariableNode(
"asmLocalTmpVar", tmpVarType, null, getFirstLabel(mn), getLastLabel(mn), tmpVarIndex);
variables.add(tmpVarNode);
mn.maxLocals += 2;
mn.maxStack += 2;
//插入xSTORE Opcodes.ILOAD
VarInsnNode tmpVarStoreCall = new VarInsnNode(getStoreOpcodeByType(tmpVarType), tmpVarIndex);
instructions.insertBefore(node, tmpVarStoreCall);
//插入xLOAD
VarInsnNode tmpVarLoadCall = new VarInsnNode(getLoadOpcodeByType(tmpVarType), tmpVarIndex);
instructions.insertBefore(node, tmpVarLoadCall);
//在xLOAD之前插入restoreCallingIdentity
VarInsnNode varLoadCall = new VarInsnNode(Opcodes.LLOAD, varIndex);
instructions.insertBefore(tmpVarLoadCall, varLoadCall);
MethodInsnNode postCall = new MethodInsnNode(Opcodes.INVOKESTATIC,
"android/os/Binder","restoreCallingIdentity", "(J)V", false);
instructions.insert(varLoadCall, postCall);
}
// 如果为xLOAD,则在xLOAD之前插入restoreCallingIdentity(第三种情况)
else if (isLoadInstruction(prevousOpcode)) {
System.out.println("maxLocals = " + mn.maxLocals + ",maxStack = " + mn.maxStack);
VarInsnNode varLoadCall = new VarInsnNode(Opcodes.LLOAD, varIndex);
instructions.insertBefore(previousNode, varLoadCall);
MethodInsnNode postCall = new MethodInsnNode(Opcodes.INVOKESTATIC,
"android/os/Binder","restoreCallingIdentity", "(J)V", false);
instructions.insertBefore(previousNode, postCall);
}
// 如果为其他情况,则在返回指令之前插入restoreCallingIdentity,并在此之后插入xLOAD(第二种情况)
else {
AbstractInsnNode lastLoadNode = findLastLoadNode(instructions, i - 2);
if (lastLoadNode == null) {
continue;
}
VarInsnNode varLoadCall = new VarInsnNode(Opcodes.LLOAD, varIndex);
instructions.insertBefore(node, varLoadCall);
MethodInsnNode postCall = new MethodInsnNode(Opcodes.INVOKESTATIC,
"android/os/Binder","restoreCallingIdentity", "(J)V", false);
instructions.insert(varLoadCall, postCall);
AbstractInsnNode clonedLoadNode = lastLoadNode.clone(null);
instructions.insert(postCall, clonedLoadNode);
}
}
//Add end
}
}
super.visitEnd();
mn.accept(chain);
}
private LabelNode getFirstLabel(MethodNode mn) {
for (int i = 0; i < mn.instructions.size(); i++) {
AbstractInsnNode s = mn.instructions.get(i);
if (s instanceof LabelNode) {
return (LabelNode)s;
}
}
return null;
}
private LabelNode getLastLabel(MethodNode mn) {
for (int i = mn.instructions.size() - 1; i >= 0 ; i--) {
AbstractInsnNode s = mn.instructions.get(i);
if (s instanceof LabelNode) {
return (LabelNode)s;
}
}
return null;
}
//Add begin
private AbstractInsnNode findNodeByOpcode(MethodNode mn, int opcode) {
if (opcode <= 0 || mn == null) {
return null;
}
for (int i = mn.instructions.size() - 1; i >= 0 ; i--) {
AbstractInsnNode s = mn.instructions.get(i);
if (s.getOpcode() == opcode) {
return s;
}
}
return null;
}
private boolean isReturnInstruction(int opCode) {
switch (opCode) {
case Opcodes.ARETURN:
case Opcodes.DRETURN:
case Opcodes.FRETURN:
case Opcodes.IRETURN:
case Opcodes.LRETURN:
return true;
default:
return false;
}
}
private boolean isConstOpcode(int opCode) {
switch (opCode) {
case Opcodes.ICONST_0:
case Opcodes.ICONST_1:
case Opcodes.ICONST_2:
case Opcodes.ICONST_3:
case Opcodes.ICONST_4:
case Opcodes.ICONST_5:
case Opcodes.ICONST_M1:
case Opcodes.BIPUSH:
case Opcodes.SIPUSH:
case Opcodes.LDC:
return true;
default:
//TODO
return false;
}
}
private String getParamTypeByOpcode(int opCode) {
switch (opCode) {
case Opcodes.ICONST_0:
case Opcodes.ICONST_1:
case Opcodes.ICONST_2:
case Opcodes.ICONST_3:
case Opcodes.ICONST_4:
case Opcodes.ICONST_5:
case Opcodes.ICONST_M1:
case Opcodes.BIPUSH:
case Opcodes.SIPUSH:
case Opcodes.LDC:
return "I";
default:
//TODO
return null;
}
}
private int getLoadOpcodeByType(String type) {
switch (type) {
case "I":
return Opcodes.ILOAD;
default:
//TODO
return -1;
}
}
private int getStoreOpcodeByType(String type) {
switch (type) {
case "I":
return Opcodes.ISTORE;
default:
//TODO
return -1;
}
}
private AbstractInsnNode findLastLoadNode(InsnList instructions, int index) {
for (;index >= 0; index--) {
AbstractInsnNode node = instructions.get(index);
System.out.println("findLastLoadNode-->" + node);
if (node == null) {
continue;
}
if (isLoadInstruction(node.getOpcode())) {
return node;
}
}
return null;
}
private boolean isLoadInstruction(int opCode) {
switch (opCode) {
case Opcodes.ALOAD:
case Opcodes.DLOAD:
case Opcodes.FLOAD:
case Opcodes.ILOAD:
case Opcodes.LLOAD:
return true;
default:
return false;
}
}
private int getLoadOpCodeByReturnOpcode(int returnOpcode) {
switch (returnOpcode) {
case Opcodes.ARETURN:
return Opcodes.ALOAD;
case Opcodes.DRETURN:
return Opcodes.DLOAD;
case Opcodes.FRETURN:
return Opcodes.FLOAD;
case Opcodes.IRETURN:
return Opcodes.ILOAD;
case Opcodes.LRETURN:
return Opcodes.LLOAD;
}
return -1;
}
private int getConstOpCodeByReturnOpcode(int returnOpcode) {
switch (returnOpcode) {
case Opcodes.ARETURN:
return Opcodes.ALOAD;
case Opcodes.DRETURN:
return Opcodes.DLOAD;
case Opcodes.FRETURN:
return Opcodes.FLOAD;
case Opcodes.IRETURN:
return Opcodes.ILOAD;
case Opcodes.LRETURN:
return Opcodes.LLOAD;
}
return -1;
}
//Add end
}
}
验证
编译工具端:
ryan ~/workspace/tools $ javac MethodRegionInjector.java -cp ../asm/asm-6.0.jar:../asm/asm-tree-6.0.jar
运行:
ryan ~/workspace/tools $ java -cp ../asm/asm-6.0.jar:../asm/asm-tree-6.0.jar:./ MethodRegionInjector
运行修改后的代码:
ryan ~/workspace/tools $ cd ../out/
ryan ~/workspace/out $ java Test2
clearCallingIdentity-->-5716791008867365700
call-->10731590751
restoreCallingIdentity-->-5716791008867365700
clearCallingIdentity-->2147832164099559499
restoreCallingIdentity-->2147832164099559499
getInt:-1
clearCallingIdentity-->93009951661183094
restoreCallingIdentity-->93009951661183094
getInt:-1352438556
clearCallingIdentity-->1599954344744641456
restoreCallingIdentity-->1599954344744641456
getInt2:1786603818
后记
至此,我们完成了对返回类型为int
的方法进行代码注入;
理论上其他基本数据类型是可以类推的,这里暂不展开了;
但是还有如下几个问题没有考虑:
- 方法内有多个return;
- 方法具备返回值,返回值为引用数据类型;
- 方法内有
try-catch-finally
代码结构;