Android ASM使用

ASM

ASM是一种基于java字节码层面的代码分析和修改工具,ASM的目标是生成,转换和分析已编译的java class文件,可使用ASM工具读/写/转换JVM指令集。通俗点讲就是来处理javac编译之后的class文件

Java字节码

Java字节码是Java虚拟机执行的一种指令格式。通俗来讲字节码就是经过javac命令编译之后生成的Class文件。Class文件包含了Java虚拟机指令集和符号表以及若干其他的辅助信息。Class是一组以8位字节为基础单位的二进制文件。各个数据项目严格按照顺序紧凑的排列在Class文件之中。中间没有任何分隔符,这使得整个Class文件中存储的内容几乎全是程序运行时的必要数据。

class文件有固定的结构,保留了几乎所有的源代码文件中的符号。class文件的结构:

20140730105614312.png
  • 描述类的modifier,name,父类,接口和注释
  • 描述类中变量的modfier,名字,类型和注释
  • 描述类中方法和构造函数的modifier,名字参数类型,返回类型,注释等信息,当然也包含已编译成java字节码指令序列的方法具体内容
  • class文件的静态池区域,用来保存所有的数字,字符串,类型的常量,这些常量只被定义过一次且被其他class中区域所引用

一个Java文件编译之后可能对应多个class文件。

字节码描述符

不同类型的Java类型使用不同的描述符


  • Class文件中使用全限定名来表示一个类的引用,即把类名所有“.”换成了“/”,例如:
    android.content.Context在class中文android/content/Context

  • 数据类型
    数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值以及代表无返回值的void类型都用一个大写字符来表示

Java类型 class描述符
byte B
char C
double D
float F
int I
long J
short S
boolean Z
void V
对象类型 L
数组 [

如:

String[] -> [Ljava/lang/String;
int[][] -> [[I;

  • 方法

方式使用(), 按照参数列表,返回值的顺序表示。 例如:

void init() -> ()V
void test(object) -> (Ljava/lang/object;)V
String[] getArray(String s) -> (Ljava/lang/String;)[Ljava/lang/String;

ASM修改Class流程

ASM处理Class文件使用了生产-消费者模式,基本有三个部分组成:

  • Reader, 如ClassReader, 读取class
  • Adapter, 转换器,对class文件就行修改
  • writer, 将修改后的class写入到文件

时序图:

5251070-4ba0865b79cc47f7.png

ASM中提供一个ClassReader类,调用accept方法,接受一个实现了抽象类ClassVisitor的对象实例作为参数,然后依次调用ClassVisitor的各个方法。字节码空间上的偏移被转成各种visitXXX方法。使用者只需要在对应的的方法上进行需求操作即可,无需考虑字节偏移

这个过程中ClassReader可以看作是一个事件生产者,ClassWriter继承自ClassVisitor抽象类,负责将对象化的class文件内容重构成一个二进制格式的class字节码文件,ClassWriter可以看作是一个事件的消费者。

例如ClassVisitor类的结构:

public abstract class ClassVisitor {
    public ClassVisitor(int api);
    public ClassVisitor(int api, ClassVisitor cv);
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
    public void visitSource(String source, String debug);
    public void visitOuterClass(String owner, String name, String desc); 
    AnnotationVisitor visitAnnotation(String desc, boolean visible); 
    public void visitAttribute(Attribute attr);
    public void visitInnerClass(String name, String outerName, String innerName, int access);
    public FieldVisitor visitField(int access, String name, String desc,
    String signature, Object value);
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions); 
    void visitEnd();
}

访问一个class文件的顺序为:
visitvisitSource -> visitOuterClass -> isitAnnotation | visitAttribute -> visitInnerClass | visitField | visitMethod -> visitEnd

使用:

ClassPrinter cp = new ClassPrinter();
ClassReader cr = new ClassReader("java.lang.Runnable");
cr.accept(cp, 0);

输出:

java/lang/Runnable extends java/lang/Object {
    run()V
}

各个 ClassVisitor通过职责链 (Chain-of-responsibility) 模式,可以非常简单的封装对字节码的各种修改,而无须关注字节码的字节偏移,因为这些实现细节对于用户都被隐藏了,用户要做的只是覆写相应的 visit 函数。

ClassAdaptor类实现了 ClassVisitor接口所定义的所有函数,当新建一个 ClassAdaptor对象的时候,需要传入一个实现了ClassVisitor接口的对象,作为职责链中的下一个访问者 (Visitor),这些函数的默认实现就是简单的把调用委派给这个对象,然后依次传递下去形成职责链。当用户需要对字节码进行调整时,只需从 ClassAdaptor类派生出一个子类,覆写需要修改的方法,完成相应功能后再把调用传递下去。这样,用户无需考虑字节偏移,就可以很方便的控制字节码。

每个 ClassAdaptor类的派生类可以仅封装单一功能,比如删除某函数、修改字段可见性等等,然后再加入到职责链中,这样耦合更小,重用的概率也更大,但代价是产生很多小对象,而且职责链的层次太长的话也会加大系统调用的开销,用户需要在低耦合和高效率之间作出权衡。用户可以通过控制职责链中 visit 事件的过程。

ASM 的最终的目的是生成可以被正常装载的 class 文件,因此其框架结构为客户提供了一个生成字节码的工具类: ClassWriter。它实现了 ClassVisitor接口,而且含有一个 toByteArray()函数,返回生成的字节码的字节流,将字节流写回文件即可生产调整后的 class 文件。一般它都作为职责链的终点,把所有 visit 事件的先后调用(时间上的先后),最终转换成字节码的位置的调整(空间上的前后)

使用举例

  • 删除类的字段、方法、指令

删除类的字段、方法、指令:只需在职责链传递过程中中断委派,不访问相应的 visit 方法即可,比如删除方法时只需直接返回 null,而不是返回由 visitMethod方法返回的 MethodVisitor对象

例如删除类里面的test方法

class RemoveTestClassAdapter extends ClassAdapter { 
    public DelLoginClassAdapter(ClassVisitor cv) { 
        super(cv); 
    } 
 
    public MethodVisitor visitMethod(final int access, final String name, 
        final String desc, final String signature, final String[] exceptions) { 
        if (name.equals("test")) { 
            return null; 
        } 
        return cv.visitMethod(access, name, desc, signature, exceptions); 
    } 
}
  • 修改类、字段、方法的名字或修饰符

只需要在对应的Adapter 中替换调用参数

class ModifyClassAdapter extends ClassAdapter { 
    public AccessClassAdapter(ClassVisitor cv) { 
        super(cv); 
    } 
 
    public FieldVisitor visitField(final int access, final String name, 
       final String desc, final String signature, final Object value) { 
       int privateAccess = Opcodes.ACC_PRIVATE; 
       return cv.visitField(privateAccess, name, desc, signature, value); 
   } 
}
  • 在方法执行时插入代码
class addCodeClassAdapter extends ClassAdapter {
 
     //Responsechain 的下一个 ClassVisitor,这里cv是ClassWriter,
    public AddSecurityCheckClassAdapter(ClassVisitor cv) {
        // 负责改写后代码的输出
        super(cv); 
    } 
     
    public MethodVisitor visitMethod(final int access, final String name, 
        final String desc, final String signature, final String[] exceptions) { 
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);
        MethodVisitor wrappedMv = mv; 
        if (mv != null) { 
            if (name.equals("test")) { 
                wrappedMv = new TestMethodAdapter(mv); 
            } 
        } 
        return wrappedMv; 
    } 
}
class TestMethodAdapter extends MethodAdapter { 
    public AddSecurityCheckMethodAdapter(MethodVisitor mv) { 
        super(mv); 
    } 
 
    public void visitCode() { 
        visitMethodInsn(Opcodes.INVOKESTATIC, "com/test/TestHelper”, 
           "test”, "()V"); 
    } 
}

visitMethodInsn就是访问一个方法的指令,Opcodes.INVOKESTATIC:调用静态方法,后面参数分别是类名,方法名和方法参数

  • 添加继承
class ModifyClassAdapter extends ClassAdapter { 
    public AccessClassAdapter(ClassVisitor cv) { 
        super(cv); 
    } 
 
    public void visit(final int version, final int access, final String name, 
        final String signature, final String superName, 
        final String[] interfaces) { 
        String enhancedName = "Test$Enhanced";  // 改变类命名
        enhancedSuperName = “Test”; // 改变父类
        super.visit(version, access, enhancedName, signature, 
        enhancedSuperName, interfaces); 
   }
}

修改构造函数

public MethodVisitor visitMethod(final int access, final String name, 
    final String desc, final String signature, final String[] exceptions) { 
    MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions); 
    MethodVisitor wrappedMv = mv; 
    if (mv != null) { 
         if (name.equals("<init>")) { 
                wrappedMv = new ChangeChildConstructorMethodAdapter(mv, 
                enhancedSuperName); 
        } 
    } 
    return wrappedMv; 
}
class ChangeChildConstructorMethodAdapter extends MethodAdapter { 
    private String superClassName; 
 
    public ChangeToChildConstructorMethodAdapter(MethodVisitor mv, 
        String superClassName) { 
        super(mv); 
        this.superClassName = superClassName; 
    } 
 
    public void visitMethodInsn(int opcode, String owner, String name, 
        String desc) { 
        // 调用父类的构造函数时
        if (opcode == Opcodes.INVOKESPECIAL && name.equals("<init>")) { 
            owner = superClassName; 
        } 
        super.visitMethodInsn(opcode, owner, name, desc);// 改写父类为 superClassName 
    } 
}

在java中使用修改的类:

首先当然要实现一个ClassLoader

class TestClassLoader extends ClassLoader {
        public Class defineClassFromClassFile(String className, 
            byte[] classFile) throws ClassFormatError { 
            return defineClass("Test$Enhanced", classFile, 0, 
            classFile.length());
        } 
    } 

然后使用ClassLoader加载该类

 private static Class testClass; 
 public Test testEnhancedt() throws ClassFormatError, 
        InstantiationException, IllegalAccessException { 
        if (null == testClass) {            
            ClassReader cr = new ClassReader("Test"); 
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS); 
            ClassAdapter classAdapter = new AddSecurityCheckClassAdapter(cw);
            cr.accept(classAdapter, ClassReader.SKIP_DEBUG); 
            byte[] data = cw.toByteArray(); 
            secureAccountClass = classLoader.defineClassFromClassFile( 
               "Test$Enhanced",data); 
        } 
        return (Test) testClass.newInstance(); 
    } 


Android中使用ASM

android 一般在gradle 插件中使用ASM

class TestPlugin implements Plugin<Project>{
    @Override
    void apply(Project project) {
           //在App module使用Transform
           if (project.plugins.hasPlugin(AppPlugin)) { 
              TestTransform transform = new TestTransform(project)
               android.registerTransform(transform)
           }
    }
}

Android中添加了Transform,Transform允许 第三方插件在class文件转为为dex文件前操作编译好的class文件,这个API的目标是简化自定义类操作,而不必处理Task。

Transform的结构

class TestTransform extends Transform{

    @Override
    String getName() {
        return "Test"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, 
                              InterruptedException, IOException {
    }
}

InputType是指输入的类型,即要扫面的文件类型,支持CLASS和RESOURCES
Scopes是作用域,SCOPE_FULL_PROJECT即整个project
isIncremental是否是增量任务
transform 就是执行修改class的关键方法

TransformInvocation,看名字就知道,就是变化的调用。里面包含以下关键信息:

Inputs:需要处理的输入,有两种类型JarInput和DirectoryInput, JarInput就是要处理jar中的class,
DirectoryInput就是在文件夹下的class
ReferencedInputs: 仅仅是引用的输入
OutputProvider: 输出提供器,即将输入转换为输出的过程

修改的class替换

class是以文件的形式存在的,ASM本质只是修改了class文件的内容,要使修改生效,还需要一个文件替换的过程。要替换文件要先找到要修改的class文件对象或路径。 获取方法:

 File destFile = transformInvocation.outputProvider.getContentLocation(
                destName, jarInput.contentTypes, jarInput.scopes, Format.JAR)

Format.JAR是在JAR中的位置,若要获取文件夹下class的位置传入Fommat.DIRECTORY

jar 处理

jar其实也是ZIP,使用java JarFile处理

def jar = new JarFile(src)
Enumeration enumeration = jar.entries()
while (enumeration.hasMoreElements()) {
     JarEntry jarEntry = (JarEntry) enumeration.nextElement()
     String entryName = jarEntry.getName()
     /**
     *  entryName就是jar下的class名称,安装需求处理即可
     */
     …
     jar.close()
 }

DirectoryInput就简单了,遍历文件夹的class文件即可

修改Class文件

ClassReader cr = new ClassReader(new FileInputStream(srcFile))
TestClassVisitor cv = new TestClassVisitor()
cr.accept(cv, 0)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272