返回顶部
首页 > 资讯 > 移动开发 >Android字节码的手术刀插桩初体验
  • 915
分享到

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

Android 2022-06-06 13:06:50 915人浏览 安东尼
摘要

本文有对其他博客的一些借鉴。 我们都知道Dalvik虚拟机运行的是.dex文件。.dex文件又是通过.class文件通过dx工具编译而来。今天要体验的就是一个非常有意思的技术,

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

我们都知道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


--结束END--

本文标题: Android字节码的手术刀插桩初体验

本文链接: https://lsjlt.com/news/29502.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作