如何使用ASM给android的某个函数做插桩?
源码:https://github.com/shinecjj/AMStest
1、AMStest项目创建
直接在Android Studio中,new project 就行,等待项目第一次编译完成
2.gradle插件创建
在项目的根目录中,创建buildSrc文件夹,然后构建一下项目,然后在buildSrc文件夹中创建build.gradle配置文件,如下:
代码语言:javascript复制plugins{
//使用 java groovy 插件
id 'java'
id 'groovy'
}
group 'com.julive.sam'
version '0.0.1'
sourceCompatibility = 1.8
repositories{
//使用阿里云的maven代理
maven { url 'https://maven.aliyun.com/repository/google' }
maven { url 'https://maven.aliyun.com/repository/public' }
// maven {
// url 'http://maven.aliyun.com/nexus/content/groups/public/'
// }
// maven {
// url 'http://maven.aliyun.com/nexus/content/repositories/jcenter'
// }
}
def asmVersion = '8.0.1'
dependencies {
//引入gradle api
implementation gradleApi()
implementation localGroovy()
//引入android studio扩展gradle的相关api
implementation "com.android.tools.build:gradle:4.1.0"
//引入apache io
implementation 'org.apache.directory.studio:org.apache.commons.io:2.4'
//引入ASM相关api,这是我们插桩的关键,要靠他实现方法插桩
implementation "org.ow2.asm:asm:$asmVersion"
implementation "org.ow2.asm:asm-util:$asmVersion"
implementation "org.ow2.asm:asm-commons:$asmVersion"
}
接下来创建插件代码目录,由于我们使用java写的插件,所以需要选中buildSrc,然后鼠标右键选择new,再选择directory,最后出现的对话框中选择 src/main/java,下图中是因为我的项目已经创建完了,所以只有groovy目录,如果你需要写groovy的实现就创建下图中文件夹路径,创建完这个下一步就是创建插件。
在java目录中,创建包名com.julive.sam,在该包路径下创建Plugins插件,代码如下:
代码语言:javascript复制public class Plugins implements Plugin<Project> {
@Override
public void apply(Project project) {
//registerTransform
AppExtension android = project.getExtensions().getByType(AppExtension.class);
android.registerTransform(new TransformTest());
}
}
然后创建插件的配置resources文件夹,和java文件夹同级,在resources下创建文件夹META-INF/gradle-plugins/,最终在gradle-plugins中创建com.julive.sam.properties,意思是你的包名.properties ,一定要对应好包名,然后在该文件中加入代码
代码语言:javascript复制implementation-class=com.julive.sam.Plugins
com.julive.sam.Plugins 你点击后,看能否跳转至 上面创建的Plugins插件中,如果可以直接跳转那就ok了。
3.下一步在App的build.gradle中配置插件
4.创建gradle的Transform实现
Transform是在.class -> .dex转换期间,用来修改.class文件的一套标准API,所以你现在应该知道了,在transform中我们肯定要调用ASM的实现,来实现.class文件的修改,最终转换为.dex文件。创建Transform的实现如下:
代码语言:javascript复制public class TransformTest extends Transform {
@Override
public String getName() {
// 随便起个名字
return "TransformSam";
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
//代表处理的 java 的 class 文件
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
//要处理所有的class字节码
return TransformManager.SCOPE_FULL_PROJECT;
}
@Override
public boolean isIncremental() {
// 是否增量编译,我们先不考虑,返回false
return false;
}
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
try {
//待实现
doTransform(transformInvocation); // hack
} catch (Exception e) {
e.printStackTrace();
}
}
看上面注释是不是就对Transform有了一定的了解呢,那么如何处理.class文件呢?我们来实现doTransform函数,来看如何处理
代码语言:javascript复制 private void doTransform(TransformInvocation transformInvocation) throws IOException {
System.out.println("doTransform =======================================================");
//inputs中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。
Collection<TransformInput> inputs = transformInvocation.getInputs();
//获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
//删除之前的输出
if (outputProvider != null)
outputProvider.deleteAll();
inputs.forEach(transformInput -> {
//遍历directoryInputs
transformInput.getDirectoryInputs().forEach(directoryInput -> {
ArrayList<File> list = new ArrayList<>();
getFileList(directoryInput.getFile(), list);
list.forEach(file -> {
System.out.println("getDirectoryInputs =======================================================" file.getName());
// 判断是.class文件
if (file.isFile() && file.getName().endsWith(".class")) {
try {
//ASM提供的读取类信息的对象
ClassReader classReader = new ClassReader(new FileInputStream(file));
//ASM提供的类修改对象,并将读到的信息交给classWriter
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
//创建修改规则,TestClassVisitor
ClassVisitor visitor = new TestClassVisitor(classWriter);
//将修改规则给classReader
classReader.accept(visitor, ClassReader.EXPAND_FRAMES);
//通过toByteArray方法,将变更后信息转成byte数组
byte[] bytes = classWriter.toByteArray();
//放入输出流中往原文件中写入
FileOutputStream fileOutputStream = new FileOutputStream(file.getAbsolutePath());
fileOutputStream.write(bytes);
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
if (outputProvider != null) {
File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
try {
//将该文件放入到目标目录中,这步骤必须实现,否则会导致dex文件找不到该文件
FileUtils.copyDirectory(directoryInput.getFile(), dest);
} catch (IOException e) {
e.printStackTrace();
}
}
});
//jarInputs
transformInput.getJarInputs().forEach(jarInput -> {
ArrayList<File> list = new ArrayList<>();
getFileList(jarInput.getFile(), list);
list.forEach(file -> {
System.out.println("getJarInputs =======================================================" file.getName());
});
if (outputProvider != null) {
File dest = outputProvider.getContentLocation(
jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
//将该文件放入到目标目录中,这步骤必须实现,否则会导致dex文件找不到该文件
try {
FileUtils.copyFile(jarInput.getFile(), dest);
} catch (IOException e) {
e.printStackTrace();
}
}
});
});
}
//递归查找该文件夹下所有文件,因为我们修改的是.class 文件,而不关系文件夹
void getFileList(File file, ArrayList<File> fileList) {
if (file.isFile()) {
fileList.add(file);
} else {
File[] list = file.listFiles();
for (File value : list) {
getFileList(value, fileList);
}
}
}
从transformInvocation的api中,我们获取了两个东西,一个是inputs,一个是outputProvider,我们遍历inputs后发现,他有两个api getDirectoryInputs和getJarInputs 这俩是什么东西呢?我描述不太好,我加了日志,来看下日志输出:
这下是不是看明白了,其实我对getDirectoryInputs做了一层文件筛选处理
代码语言:javascript复制transformInput.getDirectoryInputs().forEach(directoryInput -> {
ArrayList<File> list = new ArrayList<>();
getFileList(directoryInput.getFile(), list);
});
//递归查找该文件夹下所有文件,因为我们修改的是.class 文件,而不关系文件夹
void getFileList(File file, ArrayList<File> fileList) {
if (file.isFile()) {
fileList.add(file);
} else {
File[] list = file.listFiles();
for (File value : list) {
getFileList(value, fileList);
}
}
}
好,从上面我们看出,已经找到了MainActivity的class文件,那么接下来给MainActivity.class的onCreate函数,插入两行代码,
5. 现在开始操作ASM的api
首先要实现ASM的 ClassVisitor 类来操作我们想要操作的类,它可以访问class文件的各个部分,比如方法、变量、注解等
基本的实现如下:
代码语言:javascript复制public class TestClassVisitor extends ClassVisitor{
private String className;
private String superName;
TestClassVisitor(final ClassVisitor classVisitor) {
super(Opcodes.ASM6, classVisitor);
}
/**
* 这里可以拿到关于.class的所有信息,比如当前类所实现的接口类表等
*
* @param version 表示jdk的版本
* @param access 当前类的修饰符 (这个和ASM 和 java有些差异,比如public 在这里就是ACC_PUBLIC)
* @param name 当前类名
* @param signature 泛型信息
* @param superName 当前类的父类
* @param interfaces 当前类实现的接口列表
*/
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, name, signature, superName, interfaces);
this.className = name;
this.superName = superName;
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
//委托函数
MethodVisitor methodVisitor = cv.visitMethod(access, name, descriptor, signature, exceptions);
//找到我们需要修改的类,注意这里是/ 斜杠来表示文件的路径,并不是java代码中的.
if (className.equals("com/julive/samtest/MainActivity")) {
// 判断方法name是onCreate
if (name.startsWith("onCreate")) {
//插桩函数的实现,同样用到ASM提供的对象,下面看具体实现代码
return new TestMethodVisitor(Opcodes.ASM6, methodVisitor, access, name, descriptor, className, superName);
}
}
return methodVisitor;
}
}
这里集成AdviceAdapter,其实AdviceAdapter是继承自MethodVisitor,这是不是就跟ClassVisitor一一呼应呢,使用它是因为它比较方便的实现,提供了onMethodEnter,onMethodExit,正好是我们的需求。在onCreate的函数的前后各插入一行代码。但仔细看onMethodEnter的函数实现,你会发现一脸懵逼,不知道是啥玩意。往下看
代码语言:javascript复制public class TestMethodVisitor extends AdviceAdapter {
private String className;
private String superName;
protected TestMethodVisitor(int i, MethodVisitor methodVisitor, int i1, String s, String s1,String className,String superName) {
super(i, methodVisitor, i1, s, s1);
this.className = className;
this.superName = superName;
}
@Override
protected void onMethodEnter() {
super.onMethodEnter();
mv.visitLdcInsn("TAG");
mv.visitLdcInsn(className "---->" superName);
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
}
@Override
protected void onMethodExit(int opcode) {
mv.visitLdcInsn("TAG");
mv.visitLdcInsn("this is end");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
mv.visitInsn(Opcodes.POP);
super.onMethodExit(opcode);
}
}
在这里推荐一个插件,https://plugins.jetbrains.com/plugin/14860-asm-bytecode-viewer-support-kotlin ,用插件测试代码如下:
代码语言:javascript复制public class Test {
void aa() {
Log.i("TAG", "this is end");
}
}
转换ASM代码如下:
代码语言:javascript复制public static byte[] dump() throws Exception {
ClassWriter classWriter = new ClassWriter(0);
FieldVisitor fieldVisitor;
MethodVisitor methodVisitor;
AnnotationVisitor annotationVisitor0;
classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, "com/julive/samtest/Test", null, "java/lang/Object", null);
classWriter.visitSource("Test.java", null);
{
methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(5, label0);
methodVisitor.visitVarInsn(ALOAD, 0);
methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
methodVisitor.visitInsn(RETURN);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLocalVariable("this", "Lcom/julive/samtest/Test;", null, label0, label1, 0);
methodVisitor.visitMaxs(1, 1);
methodVisitor.visitEnd();
}
{
methodVisitor = classWriter.visitMethod(0, "aa", "()V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(8, label0);
methodVisitor.visitLdcInsn("TAG");
methodVisitor.visitLdcInsn("this is end");
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
methodVisitor.visitInsn(POP);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(9, label1);
methodVisitor.visitInsn(RETURN);
Label label2 = new Label();
methodVisitor.visitLabel(label2);
methodVisitor.visitLocalVariable("this", "Lcom/julive/samtest/Test;", null, label0, label2, 0);
methodVisitor.visitMaxs(2, 1);
methodVisitor.visitEnd();
}
classWriter.visitEnd();
return classWriter.toByteArray();
}
是不是很长,哈哈,这段代码其实是将整个Test类的东西,都通过ASM的方式生成,我们只需要找到对应的日志如下:
代码语言:javascript复制 methodVisitor.visitLdcInsn("TAG");
methodVisitor.visitLdcInsn("this is end");
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "i", "(Ljava/lang/String;Ljava/lang/String;)I", false);
methodVisitor.visitInsn(POP);
然后将其放入到onMethodExit函数中,就可以了。
6.Tranfrom结合ASM实现
现在万事具备只欠东风,就是将Tranform拿到的class文件通过ASM做修改,具体如何关联,请看,回到刚才的doTransform中,改成如下代码:
代码语言:javascript复制 private void doTransform(TransformInvocation transformInvocation) throws IOException {
System.out.println("doTransform =======================================================");
//inputs中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。
Collection<TransformInput> inputs = transformInvocation.getInputs();
//获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
//删除之前的输出
if (outputProvider != null)
outputProvider.deleteAll();
inputs.forEach(transformInput -> {
//遍历directoryInputs
transformInput.getDirectoryInputs().forEach(directoryInput -> {
ArrayList<File> list = new ArrayList<>();
getFileList(directoryInput.getFile(), list);
list.forEach(file -> {
System.out.println("getDirectoryInputs =======================================================" file.getName());
// 判断是.class文件
if (file.isFile() && file.getName().endsWith(".class")) {
try {
//ASM提供的读取类信息的对象
ClassReader classReader = new ClassReader(new FileInputStream(file));
//ASM提供的类修改对象,并将读到的信息交给classWriter
ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
//创建修改规则,TestClassVisitor
ClassVisitor visitor = new TestClassVisitor(classWriter);
//将修改规则给classReader
classReader.accept(visitor, ClassReader.EXPAND_FRAMES);
//通过toByteArray方法,将变更后信息转成byte数组
byte[] bytes = classWriter.toByteArray();
//放入输出流中往原文件中写入
FileOutputStream fileOutputStream = new FileOutputStream(file.getAbsolutePath());
fileOutputStream.write(bytes);
fileOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
if (outputProvider != null) {
File dest = outputProvider.getContentLocation(directoryInput.getName(), directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
try {
//将该文件放入到目标目录中,这步骤必须实现,否则会导致dex文件找不到该文件
FileUtils.copyDirectory(directoryInput.getFile(), dest);
} catch (IOException e) {
e.printStackTrace();
}
}
});
//jarInputs
transformInput.getJarInputs().forEach(jarInput -> {
ArrayList<File> list = new ArrayList<>();
getFileList(jarInput.getFile(), list);
list.forEach(file -> {
System.out.println("getJarInputs =======================================================" file.getName());
});
if (outputProvider != null) {
File dest = outputProvider.getContentLocation(
jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
//将该文件放入到目标目录中,这步骤必须实现,否则会导致dex文件找不到该文件
try {
FileUtils.copyFile(jarInput.getFile(), dest);
} catch (IOException e) {
e.printStackTrace();
}
}
});
});
}
//递归查找该文件夹下所有文件,因为我们修改的是.class 文件,而不关系文件夹
void getFileList(File file, ArrayList<File> fileList) {
if (file.isFile()) {
fileList.add(file);
} else {
File[] list = file.listFiles();
for (File value : list) {
getFileList(value, fileList);
}
}
}
7.反编译检查代码
好了,通过ASM的一顿操作,已经将代码插入到了MainActivity的onCreate函数中,我们如何验证?可以通过反编译来看,也可以通过日志,日志不太合理,因为一般我们不会插入很多日志来验证我们插入的正确性,太多了,照顾不过来,下面我们就反编译来看:这里推荐使用https://github.com/skylot/jadx 它提供了可视化操作,首先做如下操作:
代码语言:javascript复制git clone https://github.com/skylot/jadx.git
cd jadx
./gradlew dist
执行成功后,可以执行如下:
代码语言:javascript复制jadx-gui
然后就会打来工具,如下:
然后将 app的debug apk包拖到这个窗口就行,如下: 我们找到MainActivity如下:
而我们原代码是这样,跟我们预想的效果一致。
源码:https://github.com/shinecjj/AMStest