[Android][ASM]指令注入入门(三)——实际需求实现(前)
前言
在上一篇中,我们搭建了基于Ubuntu(WSL2)下JAVA的开发环境,今天我们基于这个环境,来实现一个实际需求:
为特定方法的首尾分别插入Binder.clearCallingIdentity()
与Binder.restoreCallingIdentity()
产生这样需求的原因是如下这样一个情况:
某些功能需要system_server
内部各个服务间相互调用,但是开发人员在设计API时,无法考虑到调用方是否具备调用其他服务的权限,因此如果不在调用之前执行Binder.clearCallingIdentity()
,会导致调用失败,从而引起功能异常;
举个例子:
- 功能a需要借助NetworkStatsManager的API,获取一些网络统计数据;
- NetworkStatsManager的大部分API需要调用方为SYSTEM_UID,否则报错,或者返回空数据;
- 功能a本身实现为
system_server
内部的服务,uid是SYSTEM_UID,因此理应可以正常获取到数据; - 但是由于
system_server
内部的服务的相互调用不会走Binder,因此Binder.getCallingUid()
依旧为调用功能a时的调用方; - 设计功能a的开发者无法确保所有调用方都是SYSTEM_UID,且从功能设计上来说,这是SYSTEM_UID在请求网络统计数据,因此不应该被限制;
因此,如果在需要在方法首行进行Binder.clearCallingIdentity()
调用,在末尾通过调用Binder.restoreCallingIdentity()
恢复的方法,如果可以在编译时自动插入,那么既可以减少开发人员的代码量,也可以避免由于疏忽导致的功能异常;
但是,这个需求看似一句话,实际上实现起来并不容易;
接下来,我们对该需求进行一个拆解:
需求拆解
回到这个需求本身:
为特定方法的首尾分别插入Binder.clearCallingIdentity()
与Binder.restoreCallingIdentity()
要实现这个需求,需要明确:
- 特定方法如何界定?
- ASM是否可以实现?
关于第一点,比较简单,只需要与开发人员约定好一个注解即可(例如:@NoCallingIdentity),当ASM解析字节码时,如果检测到该方法被@NoCallingIdentity修饰,那么才进行操作,其余方法一律不作任何操作;
至于第二点,则比较麻烦,我们需要明确插入的这两行代码包含了哪些JVM指令;
首先,我们将需要实现的需求,用伪代码的形式展现出来:
import android.os.Binder;
public class Test{
public void test() {
long token = Binder.clearCallingIdentity;
//Do something
Binder.restoreCallingIdentity(token);
}
}
不难看出,要实现这个需求,需要在原有逻辑上添加:
android.os.Binder
包引入,这个在字节码阶段已经无法(也无需)实现,直接用类的完整名即可;- 方法首行添加一个
long
型变量,名字随意,但是为了避免与方法内出现的变量名冲突,需要做排重,或取一个生僻的变量名; - 调用
Binder.clearCallingIdentity
并将结果赋值给token
; - 在方法结束前,调用
Binder.restoreCallingIdentity
并将token
以参数传入;
在上一篇中,我们已经指导,方法内的逻辑都是可以在MethodNode
类中,名为instructions
,类型为InsnList
的成员变量中记录的,那么往里面插入新的指令,理论上是可行的;
至于变量的添加,这里需要使用、操作上一篇中沒有提到的本地变量表(LocalVariableTable),而这个区域仅会在使用javac
编译时,传入参数-g
时才会生成,因此如果要评估该项需求是否可行,需要确认AOSP编译环境下,编译services.core.unboosted/services.core.priorityboosted
时的编译参数是否包含-g
;
通过确认,可确认是有的:(Android P,分支名android-9.0.0_r46,其余基线请自行确认)
//build/soong/java/config/config.go
func init() {
...
pctx.StaticVariable("CommonJdkFlags", strings.Join([]string{
`-Xmaxerrs 9999999`,
`-encoding UTF-8`,
`-sourcepath ""`,
`-g`,
// Turbine leaves out bridges which can cause javac to unnecessarily insert them into
// subclasses (b/65645120). Setting this flag causes our custom javac to assume that
// the missing bridges will exist at runtime and not recreate them in subclasses.
// If a different javac is used the flag will be ignored and extra bridges will be inserted.
// The flag is implemented by https://android-review.googlesource.com/c/486427
`-XDskipDuplicateBridges=true`,
// b/65004097: prevent using java.lang.invoke.StringConcatFactory when using -target 1.9
`-XDstringConcat=inline`,
}, " "))
...
}
确认可行后,我们就可以继续使用上一篇用的环境编写demo了;
开发步骤
源码端
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());
}
private void call(int arg) {
System.out.println("call-->" + arg + 1);
}
}
然后,由于这个环境没有Android SDK,因此我们先手动创建一个Binder.java来模拟:
package android.os;
import java.util.Random;
public class Binder {
private static final Random R = new Random();
public static final long clearCallingIdentity() {
long token = R.nextLong();
System.out.println("clearCallingIdentity-->" + token);
return token;
}
public static final void restoreCallingIdentity(long token) {
System.out.println("restoreCallingIdentity-->" + token);
}
}
存放目录结构如下:
ryan ~/workspace/src $ tree
.
├── Test.java #这是上一篇Hello World使用的,此处无用
├── Test2.java
└── android
└── os
└── Binder.java
2 directories, 3 files
编译:
ryan ~/workspace/src $ javac -g Test2.java android/os/Binder.java
ryan ~/workspace/src $ tree ./
./
├── Test.java
├── Test2.class
├── Test2.java
└── android
└── os
├── Binder.class
└── Binder.java
2 directories, 5 files
运行一下:
ryan ~/workspace/src $ java Test2
call-->-18479534511
工具端
//tools/MethodRegionInjector.java
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() {
MethodNode mn = (MethodNode) mv;
//instructions为指令集合
InsnList instructions = mn.instructions;
//localVariables为局部变量集合,如果源码编译时javac没有带-g参数,此处列表内没有任何元素(空集合)
List<LocalVariableNode> variables = mn.localVariables;
int varIndex = -1;
//确认需要新加的局部变量在列表中的位置
if (variables != null) {
varIndex = variables.size();
//本地变量表中,double与long数据类型需要占用两个slot,因此此处需要避开
for (LocalVariableNode var : variables) {
if ("J".equals(var.desc) || "D".equals(var.desc)) {
varIndex ++;
}
}
}
if (varIndex > 0) {
//插入局部变量asmLocalToken的声明,变量名可以修改为更生僻,或者使用其他方法来避免重复,此处仅做展示用,所以就不写复杂逻辑了;
//注意,参数4与参数5决定了该参数的作用域,此处为了在方法内全局有效,选用了方法内首位两个标签;
LocalVariableNode varNode = new LocalVariableNode(
"asmLocalToken","J", null, getFirstLabel(mn), getLastLabel(mn), varIndex);
//将变量声明添加到MethodNode中;
variables.add(varNode);
//添加变量后,如下字段也需要增加,同理,long与double占用两个slot,因此需要+2
mn.maxLocals += 2;
mn.maxStack += 2;
//插入Binder.clearCallingIdentity()的函数调用指令
MethodInsnNode preCall = new MethodInsnNode(Opcodes.INVOKESTATIC,
"android/os/Binder","clearCallingIdentity", "()J", false);
//不要使用insertBefore插入到第一条指令之前,否则上方声明的局部变量作用域可能会被改变,导致运行错误;
instructions.insert(instructions.getFirst(), preCall);
//将函数调用结果赋值给局部变量asmLocalToken,由于是长整形long,所以使用LSTORE
VarInsnNode varStoreCall = new VarInsnNode(Opcodes.LSTORE, varIndex);
instructions.insert(preCalocalVariablesll, varStoreCall);
//遍历所有指令
AbstractInsnNode targetNode = null;
//由于需要定位return的语句,因此从末尾开始效率更高
for (int i = instructions.size() - 1; i >= 0; i--) {
AbstractInsnNode node = instruct前ions.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;
}
}
}
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;
}
}
}
与上一篇编写的StringModifier.java
主要的差异在MethodVisitor.visitEnd()
方法的实现上,主要有如下几个关键逻辑:
mn.localVariables
用于存放本地变量(局部变量)的信息,新加变量需要往里面添加元素,添加时需要注意以下几点:
a. 长整形long
与双精度浮点double
需要占用两个slot,因此计算时下标需要间隔一个;
b. 添加新变量需要构造LocalVariableNode
对象,并明确作用域;
c. 明确作用域后,不要再在作用域之外插入其他需要使用该变量的指令;mn.instructions
在上一篇已经操作过了,这里需要注意的是方法调用后,如果需要赋值给某个变量,需要再插入一条xSTORE指令(x表示类型,如果是长整形long
,此处为LSTORE
;如果为双精度浮点double
,此处即为DSTORE
,以此类推,具体定义详见API文档)
验证
编译工具:
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 $ mkdir android/os/ -p
ryan ~/workspace/out $ cp ../src/android/os/Binder.class android/os/Binder.class
运行产物:
ryan ~/workspace/out $ java Test2
clearCallingIdentity-->-3947030970469356260
call-->-8347958541
restoreCallingIdentity-->-3947030970469356260
后记
这只是实现了最简单的一种情况,但是事实上,还有如下情况需要考虑:
- 方法内有多个return;
- 方法具备返回值,返回值由分基本数据类型与引用数据类型;
- 方法内有
try-catch-finally
代码结构;
因此,预计还有两篇才能完全实现…