[Android][ASM]代码注入入门(一)—— 从services.jar开始说起
前言
- 以AOSP android-9.0.0_r46分支为例,这个模块改动不大,理论上适用于后续版本,但不保证;
- 从实战入手,必要时讨论实现原理,适合直接上手实操,不适合系统学习;
起因
熟悉services.jar编译步骤的人应该都知道,services.jar编译实际上是静态引入多个jar包而成的:
而services.core又是通过静态引入services.core.unboosted.jar而成的:
对编译步骤了解更深入一点的同学可能知道,这个services.core.priorityboosted,实际上是与services.core.unboosted相对应的,那么,问题来了:
- 为什么要存在这两个阶段?
- services.core.priorityboosted到底boost了个啥?
带着这两个疑问,我们就可以开始了,这将是一趟非常有趣的探索之路(至少对我来说是这样);
编译过程
要想知道services.core.priorityboosted是怎么从services.core.unboosted转化而来的,首先最容易想到的方法就是:看services.core.priorityboosted模块的编译规则,即Android.mk/Android.bp文件中定义的规则:
java_genrule {
name: "services.core.priorityboosted",
srcs: [":services.core.unboosted"],
tools: ["lockedregioncodeinjection"],
cmd: "$(location lockedregioncodeinjection) " +
" --targets \"Lcom/android/server/am/ActivityManagerService;,Lcom/android/server/wm/WindowHashMap;\" " +
" --pre \"com/android/server/am/ActivityManagerService.boostPriorityForLockedSection,com/android/server/wm/WindowManagerService.boostPriorityForLockedSection\" " +
" --post \"com/android/server/am/ActivityManagerService.resetPriorityAfterLockedSection,com/android/server/wm/WindowManagerService.resetPriorityAfterLockedSection\" " +
" -o $(out) " +
" -i $(in)",
out: ["services.core.priorityboosted.jar"],
}
java_library {
name: "services.core",
static_libs: ["services.core.priorityboosted"],
}
虽然很幸运,这个方向是对的,但是…这到底是啥?
或许从build/soong/java/genrule.go
中对该规则的定义描述中可以知道这个规则的含义:
// java_genrule is a genrule that can depend on other java_* objects.
//
// By default a java_genrule has a single variant that will run against the device variant of its dependencies and
// produce an output that can be used as an input to a device java rule.
//
// Specifying `host_supported: true` will produce two variants, one that uses device dependencie sand one that uses
// host dependencies. Each variant will run the command.
//
// Use a java_genrule instead of a genrule when it needs to depend on or be depended on by other java modules, unless
// the dependency is for a generated source file.
//
// Examples:
//
// Use a java_genrule to package generated java resources:
//
// java_genrule {
// name: "generated_resources",
// tools: [
// "generator",
// "soong_zip",
// ],
// srcs: ["generator_inputs/**/*"],
// out: ["generated_android_icu4j_resources.jar"],
// cmd: "$(location generator) $(in) -o $(genDir) " +
// "&& $(location soong_zip) -o $(out) -C $(genDir)/res -D $(genDir)/res",
// }
//
// java_library {
// name: "lib_with_generated_resources",
// srcs: ["src/**/*.java"],
// static_libs: ["generated_resources"],
// }
这个举例是不是和services.core.priorityboosted一模一样?
下面归纳下java_genrule规则的语法和作用:
- 用于生成一个目标文件,并且该目标文件可作为一个JAVA模块被其他JAVA模块引用;
- $(in)是将srcs中定义的内容转化而成的绝对路径(srcs通常为本地的相对路径)
- $(out)是指将out中定义的内容转化而成的绝对路径(中间文件路径,通常在out/soong/.intermediates下)
- $(location xxx)是指xxx工具存放的绝对路径(通常也是在out/soong/.intermediates下)
总的来说,java_genrule也是一种编译规则,可借助工具对一个已经编译好的模块进行处理,并将其处理后的产物作为其他JAVA模块的依赖;
那么这里我们至少明白了一件事:services.core.unboosted借助一个名为lockedregioncodeinjection的工具,转化为了services.core.priorityboosted;
那么接下来我们来看看这个lockedregioncodeinjection是何方神圣:
转化过程
首先我们想到的,仍然是去定位lockedregioncodeinjection的编译规则在哪里定义的。
但在此之前,我们对刚才services.core.priorityboosted编译规则中的cmd部分进行一个可读性整理,以便更好理解:
$(location lockedregioncodeinjection)
--targets "Lcom/android/server/am/ActivityManagerService;,Lcom/android/server/wm/WindowHashMap;"
--pre "com/android/server/am/ActivityManagerService.boostPriorityForLockedSection,com/android/server/wm/WindowManagerService.boostPriorityForLockedSection"
--post "com/android/server/am/ActivityManagerService.resetPriorityAfterLockedSection,com/android/server/wm/WindowManagerService.resetPriorityAfterLockedSection"
-o $(out)
-i $(in)
可见,这是一个带参数的命令行;
同时,如果我们已经编译过一次,那么在out/soong/build.ninja中搜索services.core.priorityboosted模块的编译指令,可以看到真实的命令行
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# Module: services.core.priorityboosted
# Variant: android_common
# Type: java_genrule
# Factory: android/soong/android.ModuleFactoryAdaptor.func1
# Defined: frameworks/base/services/core/Android.bp:49:1
...
rule m.services.core.priorityboosted_android_common.generator
# 这句就是编译的实际指令
command = ${g.genrule.sboxCmd} --sandbox-path out/soong/.temp --output-root out/soong/.intermediates/frameworks/base/services/core/services.core.priorityboosted/android_common/gen -c 'out/soong/host/linux-x86/bin/lockedregioncodeinjection --targets "Lcom/android/server/am/ActivityManagerService;,Lcom/android/server/wm/WindowHashMap;" --pre "com/android/server/am/ActivityManagerService.boostPriorityForLockedSection,com/android/server/wm/WindowManagerService.boostPriorityForLockedSection" --post "com/android/server/am/ActivityManagerService.resetPriorityAfterLockedSection,com/android/server/wm/WindowManagerService.resetPriorityAfterLockedSection" -o __SBOX_OUT_FILES__ -i ${in}' ${allouts}
build $
...
那么现在,我们可以去找找lockedregioncodeinjection这个工具的编译规则了:
//路径:frameworks/base/tools/locked_region_code_injection/Android.bp
java_binary_host {
name: "lockedregioncodeinjection",
manifest: "manifest.txt",
srcs: ["src/**/*.java"],
static_libs: [
"asm-6.0",
"asm-commons-6.0",
"asm-tree-6.0",
"asm-analysis-6.0",
"guava-21.0",
],
}
这是一个运行在host端的java二进制,这也符合上面我们格式化cmd属性后得出的“这是一个命令行”的结论。
可以看到,这个可执行文件依赖了asm6.0,我们似乎已经可以提前得到答案了:这是一个使用ASM框架做代码注入的工具;
再结合这个工具名称,我们甚至可以大胆猜测一下这个工具注入代码的依据与上锁的代码区域有关;
ASM入门
根据官网介绍,ASM为一个为开发者提供操作JAVA字节码可能性的框架,其不仅为第三方开发者提供操作JAVA字节码的能力,还为OpenJDK、Gradle等这种主流软件提供对应的功能,因此其稳定性是可以保障的。
此外,需要注意一点,ASM适用于操作JAVA字节码文件,即.class文件,并不能直接操作dex文件。
在有了这个基本认知以后,我们来看看lockedregioncodeinjection这个工具到底做了什么;
常规操作,既然是jar包,那么就从main方法入手:(已略部分不重要的逻辑、分叉)
public class Main {
public static void main(String[] args) throws IOException {
String inJar = null;
String outJar = null;
String legacyTargets = null;
String legacyPreMethods = null;
String legacyPostMethods = null;
//解析传入参数
for (int i = 0; i < args.length; i++) {
//-i参数传入的是待处理的jar包的路径;
if ("-i".equals(args[i].trim())) {
i++;
inJar = args[i].trim();
//-o参数传入的是处理后输出jar包的路径;
} else if ("-o".equals(args[i].trim())) {
i++;
outJar = args[i].trim();
//--targets参数传入的是需要处理的类
} else if ("--targets".equals(args[i].trim())) {
i++;
legacyTargets = args[i].trim();
//--pre参数传入的是在满足条件时,语句前插入的方法
} else if ("--pre".equals(args[i].trim())) {
i++;
legacyPreMethods = args[i].trim();
//--post参数传入的是在满足条件时,语句末插入的方法
} else if ("--post".equals(args[i].trim())) {
i++;
legacyPostMethods = args[i].trim();
}
}
...
ZipFile zipSrc = new ZipFile(inJar);
ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(outJar));
List<LockTarget> targets = null;
if (legacyTargets != null) {
/*
* Utils.getTargetsFromLegacyJackConfig主要完成如下工作:
* 1. 将三者按照逗号(",")拆分,并确保拆分后元素个数一致
* 2. 三者中每个元素成为一组,依次用于构造LockTarget
* 3. 将所有构造的LockTarget实例以List<LockTarget>返回
*/
targets = Utils.getTargetsFromLegacyJackConfig(legacyTargets, legacyPreMethods,
legacyPostMethods);
} else {
...
}
Enumeration<? extends ZipEntry> srcEntries = zipSrc.entries();
//遍历jar包中的所有元素
while (srcEntries.hasMoreElements()) {
ZipEntry entry = srcEntries.nextElement();
ZipEntry newEntry = new ZipEntry(entry.getName());
zos.putNextEntry(newEntry);
BufferedInputStream bis = new BufferedInputStream(zipSrc.getInputStream(entry));
//如果是.class字节码文件,则进入convert方法进行后续的解析、注入
if (entry.getName().endsWith(".class")) {
convert(bis, zos, targets);
//其他类型文件则原封不动输出到-o参数指定路径的jar包中
} else {
while (bis.available() > 0) {
zos.write(bis.read());
}
zos.closeEntry();
bis.close();
}
}
zos.finish();
zos.close();
zipSrc.close();
}
private static void convert(InputStream in, OutputStream out, List<LockTarget> targets)
throws IOException {
/*
* 用输入流构建ClassReader对象,ClassReader的作用有:
* 1. 基于Java Virtual Machine Specification (JVMS)定义的class结构,对文件进行解析;
* 2. 调用accept方法,将解析后的数据结构提供给Visitor进行操作、改动;
*/
ClassReader cr = new ClassReader(in);
/*
* 用于将修改后的数据以流的方式输出
*/
ClassWriter cw = new ClassWriter(0);
//核心类,定义了如何操作、修改字节码,修改后的class数据结构会输出到cw中
LockFindingClassVisitor cv = new LockFindingClassVisitor(targets, cw);
/*
* 将解析后的class原文件按照内部定义的顺序,逐一通过visit类方法,提供给
* LockFindingClassVisitor进行操作(感兴趣的详见ClassReader.accept(3)方法)
*/
cr.accept(cv, 0);
// 将最终修改完成后的数据结构写到目标文件中
byte[] data = cw.toByteArray();
out.write(data);
}
}
大致流程图如下:
不难看出,LockFindingClassVisitor 才是操作字节码的具体实现。
快速过一下LockFindingClassVisitor 的内部实现,发现其继承自ClassVisitor,且内部还有一个内部类LockFindingMethodVisitor,前者作为ClassReader.accept()方法调用时的参数,后者负责在LockFindingClassVisitor 接收到ClassReader的visitMethod方法调用时被构造,并完成方法区的解析、修改;
具体来讲,流程应该是这样:
- ClassReader调用accept方法时,会遍历所有已经解析出来的class数据结构,不仅包括类声明、源码文件名等编译生成的信息,也包括成员变量、方法等结构;
- 逐一遍历过程中,会根据类型调用ClassVisitor的对应visit方法,例如遍历到成员变量时,会调用ClassVisitor.visitField()方法;遍历到方法时,会调用ClassVisitor.visitMethod()方法;
- 如果开发者需要对成员变量进行修改,则在自己继承的ClassVisitor中重写visitField()方法,并将返回值指向自己构建的FieldVisitor实现类,这样即可在该实现类中通过visitAnnotation()/visitTypeAnnotation()/visitAttribute()/visitEnd()等回调方法进行捕获、修改;而如果需要修改方法,则类似重写visitMethod()方法即可;
关于这部分,画个示意图如下:
回到今天的议题,我们看到LockFindingClassVisitor 只重写了visitMethod()方法,因此我们推测,lockedregioncodeinjection这个工具只针对方法进行注入;
class LockFindingClassVisitor extends ClassVisitor {
//此轮解析的类的类名
private String className = null;
//待注入的目标,即前面通过Utils.getTargetsFromLegacyJackConfig解析构建的List
private final List<LockTarget> targets;
public LockFindingClassVisitor(List<LockTarget> targets, ClassVisitor chain) {
super(Utils.ASM_VERSION, chain);
this.targets = targets;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature,
String[] exceptions) {
assert this.className != null;
/*
* MethodNode继承自MethodVisitor,表征一个方法;
* TryCatchBlockSorter继承自MethodNode,为try-catch代码块进行排序,排名顺序是有最内部到
* 最外部;
* 使用TryCatchBlockSorter的原因会在后面解答;
*/
MethodNode mn = new TryCatchBlockSorter(null, access, name, desc, signature, exceptions);
MethodVisitor chain = super.visitMethod(access, name, desc, signature, exceptions);
return new LockFindingMethodVisitor(this.className, mn, chain);
}
@Override
public void visit(int version, int access, String name, String signature, String superName,
String[] interfaces) {
this.className = name;
super.visit(version, access, name, signature, superName, interfaces);
}
class LockFindingMethodVisitor extends MethodVisitor {
...
}
}
那么接下来我们来看看LockFindingMethodVisitor这个内部类:
class LockFindingMethodVisitor extends MethodVisitor {
//即此轮解析的类的类名
private String owner;
private MethodVisitor chain;
public LockFindingMethodVisitor(String owner, MethodNode mn, MethodVisitor chain) {
super(Utils.ASM_VERSION, mn);
assert owner != null;
this.owner = owner;
this.chain = chain;
}
@SuppressWarnings("unchecked")
@Override
public void visitEnd() {
/*
* 这即是构造LockFindingMethodVisitor时传入的参数2,声明类型为MethodVisitor,
* 实际为MethodNode
*/
MethodNode mn = (MethodNode) mv;
/*
* 构造一个分析器,用于识别方法中的栈帧、try-catch代码块等信息
*/
Analyzer a = new Analyzer(new LockTargetStateAnalysis(targets));
LockTarget ownerMonitor = null;
// 如果方法本身由synchronized修饰,则直接从targets中遍历,看本方法是否在targets中
if ((mn.access & Opcodes.ACC_SYNCHRONIZED) != 0) {
for (LockTarget t : targets) {
if (t.getTargetDesc().equals("L" + owner + ";")) {
ownerMonitor = t;
}
}
}
//调用Analyzer.analyze进行分析
try {
a.analyze(owner, mn);
} catch (AnalyzerException e) {
throw new RuntimeException("Locked region code injection: " + e.getMessage(), e);
}
//获取方法内所有的指令(instruction)
InsnList instructions = mn.instructions;
//获取分析出的方法内所有栈帧,记不得栈帧是啥的,可以暂时无视,不影响后续理解
Frame[] frames = a.getFrames();
//使用LinkedList是为了长度与instructions指令数量对齐,且允许空元素;
List<Frame> frameMap = new LinkedList<>();
frameMap.addAll(Arrays.asList(frames));
//同样,这里使用LinkedList也是为了长度与instructions指令数量对齐,且允许空元素;
List<List<TryCatchBlockNode>> handlersMap = new LinkedList<>();
for (int i = 0; i < instructions.size(); i++) {
handlersMap.add(a.getHandlers(i));
}
/*
* 如果ownerMonitor不为null,即表示当前方法存在于targets中,且是由synchronized修饰,
* 则表示该方法为全局同步执行,因此需要从方法第一行执行指令前就插入--pre参数指定的方法调用;
*/
if (ownerMonitor != null) {
AbstractInsnNode s = instructions.getFirst();
MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC,
ownerMonitor.getPreOwner(), ownerMonitor.getPreMethod(), "()V", false);
insertMethodCallBefore(mn, frameMap, handlersMap, s, 0, call);
}
//遍历所有指令
for (int i = 0; i < instructions.size(); i++) {
AbstractInsnNode s = instructions.get(i);
/*
* 如果指令的操作码(OpCode)为MONITORENTER,表示这一行指令是synchronized代码块的开始,
* 需要在这行指令之后插入--pre参数指定的方法调用;
*
*/
if (s.getOpcode() == Opcodes.MONITORENTER) {
Frame f = frameMap.get(i);
BasicValue operand = (BasicValue) f.getStack(f.getStackSize() - 1);
//这里判断是决定synchronized代码块上锁的凭据,如果不是本类实例对象(即this)则不处理;
if (operand instanceof LockTargetState) {
LockTargetState state = (LockTargetState) operand;
for (int j = 0; j < state.getTargets().size(); j++) {
LockTarget target = state.getTargets().get(j);
MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC,
target.getPreOwner(), target.getPreMethod(), "()V", false);
insertMethodCallAfter(mn, frameMap, handlersMap, s, i, call);
}
}
}
/*
* 如果指令的操作码(OpCode)为MONITOREXIT,表示这一行指令是synchronized代码块的结束,
* 需要在这行指令之前插入--post参数指定的方法调用;
*
*/
if (s.getOpcode() == Opcodes.MONITOREXIT) {
Frame f = frameMap.get(i);
BasicValue operand = (BasicValue) f.getStack(f.getStackSize() - 1);
//同上,如果不是本类实例对象(即this)则不处理;
if (operand instanceof LockTargetState) {
LockTargetState state = (LockTargetState) operand;
for (int j = 0; j < state.getTargets().size(); j++) {
// The instruction after a monitor_exit should be a label for the end of the implicit
// catch block that surrounds the synchronized block to call monitor_exit when an exception
// occurs.
checkState(instructions.get(i + 1).getType() == AbstractInsnNode.LABEL,
"Expected to find label after monitor exit");
int labelIndex = i + 1;
checkElementIndex(labelIndex, instructions.size());
LabelNode label = (LabelNode)instructions.get(labelIndex);
checkNotNull(handlersMap.get(i));
checkElementIndex(0, handlersMap.get(i).size());
checkState(handlersMap.get(i).get(0).end == label,
"Expected label to be the end of monitor exit's try block");
LockTarget target = state.getTargets().get(j);
MethodInsnNode call = new MethodInsnNode(Opcodes.INVOKESTATIC,
target.getPostOwner(), target.getPostMethod(), "()V", false);
insertMethodCallAfter(mn, frameMap, handlersMap, label, labelIndex, call);
}
}
}
/*
* 如果指令的操作码(OpCode)为线面几种xRETURN,表示这一行指令是方法的return语句,此时
* 需要在这行指令之前插入--post参数指定的方法调用;
*/
if (ownerMonitor != null && (s.getOpcode() == Opcodes.RETURN
|| s.getOpcode() == Opcodes.ARETURN || s.getOpcode() == Opcodes.DRETURN
|| s.getOpcode() == Opcodes.FRETURN || s.getOpcode() == Opcodes.IRETURN)) {
MethodInsnNode call =
new MethodInsnNode(Opcodes.INVOKESTATIC, ownerMonitor.getPostOwner(),
ownerMonitor.getPostMethod(), "()V", false);
insertMethodCallBefore(mn, frameMap, handlersMap, s, i, call);
i++; // Skip ahead. Otherwise, we will revisit this instruction again.
}
}
super.visitEnd();
mn.accept(chain);
}
}
好了,大致看一下就明白lockedregioncodeinjection这个工具是干什么的了:
- 作用于–targets中指定的类中所有的包含同步代码块的方法;
- 在同步代码块内开头插入–pre中指定的静态方法调用;
- 在同步代码块内末尾,或遇到return时,插入–post中指定的静态方法调用;
- 只会处理synchronize(targets的类实例对象)的同步代码块;
为了验证猜想,我们先来看看反编译出来的services.core.unboosted.jar与services.core.priorityboosted.jar在ActivityManagerService.java中的差异:
可见,基本符合我们的推测;
唯一不同是,注入代码的时候还添加了try-catch代码块,这部分从上面的代码无法看出,是因为在insertMethodCallBefore和insertMethodCallAfter方法中通过调用updateCatchHandler完成的,为了不影响理解,因此略去了这部分,感兴趣的同学可以跟跟看:
public static void insertMethodCallBefore(MethodNode mn, List<Frame> frameMap,
List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index,
MethodInsnNode call) {
List<TryCatchBlockNode> handlers = handlersMap.get(index);
InsnList instructions = mn.instructions;
LabelNode end = new LabelNode();
instructions.insert(node, end);
frameMap.add(index, null);
handlersMap.add(index, null);
instructions.insertBefore(node, call);
frameMap.add(index, null);
handlersMap.add(index, null);
LabelNode start = new LabelNode();
instructions.insert(node, start);
frameMap.add(index, null);
handlersMap.add(index, null);
updateCatchHandler(mn, handlers, start, end, handlersMap);
}
public static void insertMethodCallAfter(MethodNode mn, List<Frame> frameMap,
List<List<TryCatchBlockNode>> handlersMap, AbstractInsnNode node, int index,
MethodInsnNode call) {
List<TryCatchBlockNode> handlers = handlersMap.get(index + 1);
InsnList instructions = mn.instructions;
LabelNode end = new LabelNode();
instructions.insert(node, end);
frameMap.add(index + 1, null);
handlersMap.add(index + 1, null);
instructions.insert(node, call);
frameMap.add(index + 1, null);
handlersMap.add(index + 1, null);
LabelNode start = new LabelNode();
instructions.insert(node, start);
frameMap.add(index + 1, null);
handlersMap.add(index + 1, null);
updateCatchHandler(mn, handlers, start, end, handlersMap);
}
@SuppressWarnings("unchecked")
public static void updateCatchHandler(MethodNode mn, List<TryCatchBlockNode> handlers,
LabelNode start, LabelNode end, List<List<TryCatchBlockNode>> handlersMap) {
if (handlers == null || handlers.size() == 0) {
return;
}
InsnList instructions = mn.instructions;
List<TryCatchBlockNode> newNodes = new ArrayList<>(handlers.size());
for (TryCatchBlockNode handler : handlers) {
if (!(instructions.indexOf(handler.start) <= instructions.indexOf(start)
&& instructions.indexOf(end) <= instructions.indexOf(handler.end))) {
TryCatchBlockNode newNode =
new TryCatchBlockNode(start, end, handler.handler, handler.type);
newNodes.add(newNode);
for (int i = instructions.indexOf(start); i <= instructions.indexOf(end); i++) {
if (handlersMap.get(i) == null) {
handlersMap.set(i, new ArrayList<>());
}
handlersMap.get(i).add(newNode);
}
} else {
for (int i = instructions.indexOf(start); i <= instructions.indexOf(end); i++) {
if (handlersMap.get(i) == null) {
handlersMap.set(i, new ArrayList<>());
}
handlersMap.get(i).add(handler);
}
}
}
mn.tryCatchBlocks.addAll(0, newNodes);
}
篇幅有限,这一篇主要是入门介绍,如果对ASM感兴趣,或者有需要借助ASM注入代码才能完成的需求,可以关注下下一篇(预计一周内)