文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

Android字节码的手术刀插桩初体验

2022-06-06 13:53

关注

本文有对其他博客的一些借鉴。

我们都知道Dalvik虚拟机运行的是.dex文件。.dex文件又是通过.class文件通过dx工具编译而来。今天要体验的就是一个非常有意思的技术,字节码的插桩。

大部分时候都会用埋点来介绍这个技术。原理就是,通过Transform这个类去获取项目中的.class文件。然后使用AMS提供的几个类去解析.class文件。通过对类名,方法名的判断,筛选出你需要修改的.class文件。然后在需要修改的地方插入你想要的被转成字节码的代码。

最复杂的部分是:.class文件有着自己很严格的格式,如果我们想注入代码时,不是直接插入相关的指令即可。我们还需要去找到相应的StackMapFrame,换句话说就是要找到对应的帧栈,因为我们插入的方法可能和已有的方法中的对象有引用关系,所以需要对帧栈进行计算,最后还要压缩剩下的帧。不过好在这步AMS已经处理完了,我们只需要进行调用就行。

首先要使用使用Transform就需要使用自定义插件。那么先去自定义一个插件。新建一个android library。把除了src/main/java和.gradle文件外的其他所有文件都删除了。

这样就行了。

然后我们需要用groovy语言去写插件所以需要一个groovy文件夹。在此之前先去把gradle重新写一下。把之前的都删了,然后插入下面的就行,这个写法基本上是固定了。因为我们要把插件发布到本地。

apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation  gradleApi()
    implementation  localGroovy()
    implementation 'com.android.tools.build:gradle:3.6.1'
    //ASM相关依赖
    implementation 'org.ow2.asm:asm-commons:7.1'
    implementation 'org.ow2.asm:asm:7.1'
}

然后在groovy文件夹下面自己新建一个.groovy文件。用AS编写groovy文件需要相当注意,因为这玩意大部分时候都不会报错。里面的代码意思是将自定义的transform注册到任务里,而且打印了一句话。

package my.test.lifecycle
import com.android.build.gradle.AppExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
class LifeCyclePlugin implements Plugin{
    @Override
    void apply(Project project) {
        System.out.println("register_LifeCyclePlugin")
        def android =project.extensions.getByType(AppExtension);
        LifeCycleTransForm lifeCycleTransForm=new LifeCycleTransForm();
        android.registerTransform(lifeCycleTransForm)
    }
}

然后在main文件夹下在新建一个resources文件夹

文件名一定不能错。在这个文件夹下新建一个.properties文件。my.test.lifecycle前面这段就是你的插件名了。

gradle文件中在写上group与version,然后直接运行uploadArchives这个任务。你会看到在工程下出现一个新的文件夹asm_lifecycle。

group='my.test.lifecycle'
version='1.0.0'
uploadArchives{
    repositories{
        mavenDeployer {
            //本地的Maven地址设置
            repository(url: uri('../asm_lifecycle'))
        }
    }
}

然后在app的gradle里把插件给导进来。

apply plugin: 'my.test.lifecycle'
buildscript {
    repositories {
        google()
        jcenter()
        maven { url '../asm_lifecycle' }
        //自定义插件maven地址
    }
    dependencies {
        //加载自定义插件 group + module + version
        classpath 'my.test.lifecycle:my_lifecycle_plugin:1.0.0'
    }
}

这个时候,我们的APP就可以使用自己的插件了。现在开始写我们的自定义Transform。

package my.test.lifecycle
import asm.test.plugin.LifecycleClassVisitor
import asm.test.plugin.LifecycleMethodVisitor
import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import groovy.io.FileType
import org.apache.commons.io.FileUtils
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
class LifeCycleTransForm extends Transform {
    //自定义的TransForm名称
    @Override
    String getName() {
        return "LifeCycleTransForm"
    }
    //设置自定义TransForm接收的文件类型
    @Override
    Set getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }
    //设置自定义TransForm检索范围
    @Override
    Set getScopes() {
        return TransformManager.PROJECT_ONLY
    }
    //是否支持增量编译
    @Override
    boolean isIncremental() {
        return false
    }
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        //拿到所有的class文件
        Collection transformInputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        if (outputProvider != null) {
            outputProvider.deleteAll()
        }
        transformInputs.each { TransformInput transformInput ->
            // 遍历directoryInputs(文件夹中的class文件) directoryInputs代表着以源码方式参与项目编译的所有目录结构及其目录下的源码文件
            // 比如我们手写的类以及R.class、BuildConfig.class以及MainActivity.class等
            transformInput.directoryInputs.each { DirectoryInput directoryInput ->
                File dir = directoryInput.file
                if (dir) {
                    dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->
                        def name = file.name;
                        if (name.endsWith(".class") && !name.startsWith("R\$")
                                && !"R.class".equals(name) && !"BuildConfig.class".equals(name)) {
                            System.out.println("find class: " + file.name)
                            //对class文件进行读取与解析
                            ClassReader classReader = new ClassReader(file.bytes)
                            //对class文件的写入
                            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                            //访问class文件相应的内容,解析到某一个结构就会通知到ClassVisitor的相应方法
                            ClassVisitor classVisitor = new LifecycleClassVisitor(classWriter)
                            //依次调用 ClassVisitor接口的各个方法
                            classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                            //toByteArray方法会将最终修改的字节码以 byte 数组形式返回。
                            byte[] bytes = classWriter.toByteArray()
                            //通过文件流写入方式覆盖掉原先的内容,实现class文件的改写。
//                            FileOutputStream outputStream = new FileOutputStream( file.parentFile.absolutePath + File.separator + fileName)
                            FileOutputStream outputStream = new FileOutputStream(file.path)
                            outputStream.write(bytes)
                            outputStream.close()
                        }
                    }
                }
                //处理完输入文件后把输出传给下一个文件
                def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes,
                        directoryInput.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(directoryInput.file, dest)
            }
        }
    }
}

注释写的还是很详细的。


ClassReader是用于解析class文件

ClassWriter是用于写入你要插入的字节码,并以流的形式返回

ClassVisitor用于访问class文件的类,需要自定义去继承

所以我们新建一个class的访问类,与一个方法的访问类

package asm.test.plugin;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class LifecycleClassVisitor extends ClassVisitor {
    private String className;
    private String superName;
    public LifecycleClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }
    
    @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 desc, String signature, String[] exceptions) {
        System.out.println("ClassVisitor visitMethod name-------" + name + ", superName is " + superName);
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        if(superName.equals("android/app/Activity")){
            if(name.startsWith("onCreate")){
                return new LifecycleMethodVisitor(mv,className,name);
            }
        }
        return mv;
    }
    @Override
    public void visitEnd() {
        super.visitEnd();
    }
}

注释写的很详细了,就不在解释

package asm.test.plugin;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class LifecycleMethodVisitor extends MethodVisitor {
    private String className;
    private String methodName;
    public LifecycleMethodVisitor(MethodVisitor methodVisitor, String className, String methodName) {
        super(Opcodes.ASM5, methodVisitor);
        this.className = className;
        this.methodName = methodName;
    }
    
    @Override
    public void visitCode() {
        super.visitCode();
        mv.visitLdcInsn("TAG");
        mv.visitTypeInsn(Opcodes.NEW, "java/lang/StringBuilder");
        mv.visitInsn(Opcodes.DUP);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/StringBuilder", "", "()V", false);
        mv.visitLdcInsn("Activity=");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/Object;)Ljava/lang/StringBuilder;", false);
        mv.visitLdcInsn(" method=onCreate");
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(Ljava/lang/String;)Ljava/lang/StringBuilder;", false);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/StringBuilder", "toString", "()Ljava/lang/String;", false);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "d", "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
    }
    
    @Override
    public void visitInsn(int opcode) {
        super.visitInsn(opcode);
    }
}

看到visitCode是不是感觉很难,没关系他们提供了一个工具

插件里安装这个插件

右边会多一块区域

先写好你想插入的代码,然后右击鼠标

然后他就会把相应的字节码写法展示给你

拷贝这段就行。

这样在oncreate方法前插入一个日志的事儿就完成了。

插桩可以做的事情太多了,各种监控,插件化,或者当做一个过滤器。而且这个技术相当好玩,因为不会去修改源码,你就可以实现自己想做的事情。


作者:woailqy


阅读原文内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-移动开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯