ASM(二)使用

ASM库使用

一.读取的起源:ClassReader

ClassReader位于org.objectweb.asm包下(基础类都在这个包),它是读取字节码的开始,通过它我们才能进行字节码解析。

首先是构造函数:
image.png

ClassReader有五种构造函数,这几种构造函数都是以传入数据为目标的:

  • 前两个,根据传入的byte数组(第二个指定了偏移量和长度)解析类;
  • 倒数第二个,通过InputStream传入;
  • 倒数第一个,根据类的全限定名获得对象;
  • 中间的则是不开放的API,可以忽略**。

下面是使用的例子:

ClassReader reader = new ClassReader("com/github/nickid2018/asm/TestClass");

ClassReader reader2 = new ClassReader(classBytes, 0, 3370);

ClassReader reader3 = new ClassReader(inputStreamClassFile);

说完了对象的构建,下面是它的用法。它最重要的方法是accept,其余的方法基本用不上(都内部自己用的)

image.png

accept方法

先抛开 Attribute[] 这个参数,这个以后可能会说。第一个参数ClassVisitor是你要传入的访问器:ASM整体是Visitor设计模式。最后一个参数int是代表读取模式,它有4个基本取值,这些值可以被or(|)连接:

image.png

4种基本的读取模式
下面是例子:

ClassVisitor cv = ...;

classReader.accept(cv, 0);

classReader.accept(cv, ClassWriter.SKIP_CODE);

关于ClassReader的使用到这里差不多结束了,下面先讲一下访问标志,然后再说ClassVisitor等类。

二.访问标志(Access Flag)

访问标志是用于JVM访问类、字段、方法检查和调用的一个int。这些标志既包含了我们常见的public这种访问限定符,还包含了static、final这种修饰符,除此之外还有声明类为接口的interface,为枚举的enum

完整的访问标志如下表:


image.png

JVM定义的Access Flags,真正我们能用到的不多,注释为使用范围,详见https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html#jvms-4.7.25
ASM自己定义的Access Flag。由于JVM定义的有效位只有16位,所以这两个标志不会与JVM的访问标志冲突,但是这些标志在写入类之前必须清除(用&操作即可)
下面简单说一下常用的访问标志(省略了前缀ACC_):

1. 访问限定(选择其中一个或无):public,private,protected

2. 类声明(选择一个):interface,enum,annotation(@interface定义注释类型,与interface、abstract连用),super(普通的类都有)

3. 类修饰(选择一个或没有):final(类为enum必选),abstract(类为interface必选)

4. 方法修饰(除了冲突外可以任选):static,final,abstract(在接口或抽象类里面使用,与static,final,native等冲突),synchronized,strict(关键词是strictfp,精度保留,只是口头保证罢了),native(本地方法,JNI调用)

5. 方法注释修饰(可无):varargs(注释@SafeVarargs,对于类型污染使用,必须与static或final连用)

6. 字段修饰:static,final,transient,volatile,enum(与static、final一同出现,枚举字段定义)

JVM构建生成修饰:synthetic(包括lambda表达式的提出方法、枚举的内定字段等)

这些常量可以用or叠加修饰,如果访问标志不合法(比如吧ACC_PUBLIC和ACC_PRIVATE用or联系起来当了访问标志),在ASM写入时是不会报错的,但是在JVM试图加载这个类的时候可能会抛出ClassFormatError。

三.解析类的信息:ClassVisitor

ClassVisitor是一个抽象类,它的构造函数仅需要ASM API版本(在Opcodes中可以找到,1-9),或者再加上另一个ClassVisitor用于一起解析,下面是一个模板:

实现类中的写法(由于抽象类定义了有参构造函数,子类必须显式调用)
当这个Visitor被传入accept之后,ClassReader会以下面的顺序调用:

visit [ visitSource ] [ visitModule ][ visitNestHost ][ visitPermittedSubclass ][ visitOuterClass ] 
( visitAnnotation | visitTypeAnnotation | visitAttribute )* ( visitNestMember | visitInnerClass | visitRecordComponent | visitField | visitMethod )* visitEnd

不够清晰?那么下面简单说一下流程:

1. 首先访问类的信息(visit),传入的是类文件的版本(version,从V1_1到V16)、访问标志(access),
类的全限定名(name),泛型签名(signature,可能为空),父类全限定名(无指定为java/lang/Object),实现接口列表(全限定名,可为空)

2. 之后访问注释信息(visitAnnotation),传入的是注释描述符(descriptor,这里可能包含有@Repeatable的注释类型,所以这里不是全限定名)和可见性(visible,@Retention定义的作用范围,为CLASS传入false,为RUNTIME传入true,为SOURCE不会写入类文件),该方法返回AnnotationVisitor。
同时,访问泛型注释信息(visitTypeAnnotation),传入的是注释引用类型(typeRef,可能为TypeReference定义的几个值:CLASS_TYPE_PARAMETER<以泛型类的类型参数为目标的类型引用的类型,常量值0>,CLASS_EXTENDS<以泛型类的超类或它实现的接口之一为目标的类型引用的类型,常量值16>,CLASS_TYPE_PARAMETER_BOUND<以泛型类的类型参数的绑定为目标的类型引用的类型,常量值17>),泛型类引用路径(可为空),注释描述符和可见性,返回AnnotationVisitor。

3.接着,访问字段、方法和内部类。

字段调用visitField方法,传入访问标志,字段名,描述符,泛型签名和默认值,返回FieldVisitor。

方法调用visitMethod方法,传入访问标志,方法名,描述符,泛型签名和异常列表(全限定名),返回MethodVisitor。

内部类调用visitInnerClass方法,传入内部类全限定名,外部类全限定名,内部类名称(不带包路径,也就是没有“.”的名称,如果这个写错了IDE无法识别到这个类,但是不影响调用),和访问标志(这个和类声明定义的标志不同,可以有static,这样类里面就不会带有this$0)。内部类调用指的不只是类中定义了内部类,还包括引用到了其他类的内部类。

当所有信息都访问结束,调用visitEnd。

这里的内容只是简单介绍了一下,具体的下文和接下来几篇专栏会写。

四.解析注释信息:AnnotationVisitor

AnnotationVisitor用于解析注释信息,除了最后会调用的visitEnd外,其他都与注释类型本身定义的方法返回值有关。下面是不同的类型:

visit方法:

传入注释方法名称和值,值必须是基本类型(基本数字、char及其数组,String和类)

visitArray方法:

传入注释方法名称,返回另一个AnnotationVisitor。这个新的Visitor会被传入数组内的值,所有的name传入都为null。注意:visit一个基本数字或char数组等价于使用visitArray,但是在ClassReader解析中不会调用visitArray而是直接调用visit。

visitAnnotation方法:

传入注释方法名称和值的描述符,返回的是值的AnnotationVisitor。

visitEnum方法:

传入注释方法名、值的描述符和枚举名称。

对于带有@Repeatable注释的注释类型,在Java使用反射时会返回容器注释,也就是在普通编写时有两种等价的编写方式。在ASM中,这两种方式也等价,写入按照第一种处理:

对于带有@Repeatable注释的注释类型,这两种使用方式在反射和ASM中完全等价(T.Ts是T的注释容器)

五.解析字段:FieldVisitor

FieldVisitor的构成比较简单,除了visitEnd在最后调用外,比较常用的就是visitAnnotation和visitTypeAnnotation。这些方法的使用都和ClassVisitor的使用差不多,唯一的不同是visitTypeAnnotation的注释引用类型必为FIELD(常量值19)

到此简单的解析就讲完了。什么?还差一个MethodVisitor?这是我们之后要说的重要内容,所以这里不会提到它。接下来,是应用ASM的例子。

六.使用范例:解析一个类

解析一个类需要从文章最开始说的ClassReader写起,它能将一个类的字节码解析并且进行Visitor模式调用。在下面的范例中,我们将尝试读取一个类的名称、字段和注释。

首先是一个测试类的编写,之后用javac编译。

这是一个简单的类,只包括了字段和注释
接着,我们尝试读取这个类的信息,因为测试类和运行ASM的类在同一个项目之下,可以用它的全限定名初始化ClassReader。

ClassReader reader = new ClassReader("com/github/nickid2018/asm/TestClass");

之后我们需要继承三个Visitor:ClassVisitor、FieldVisitor和AnnotationVisitor。我们只需要一些信息,所以不需要将它们的所有方法进行覆盖。

创建一个ClassParser继承ClassVisitor,选择要覆盖的方法。在访问类的时候,我们只需要类名,所以需要覆盖visit;又因为需要解析字段,我们还需要覆盖visitField,并且将我们的字段访问器作为返回值。

在ClassParser里面覆盖的方法
创建FieldParser继承FieldVisitor解析字段。在读取字段时,我们还需要读取字段中的注释,所以需要覆盖visitAnnotation,返回我们自己的AnnotationVisitor。

FieldParser进行覆盖的方法
由于@Deprecated不具有任何的注释方法,我们创建的AnnotationParser可以不覆盖任何方法。

这些访问器写完之后,就要递呈给ClassReader开始解析,代码如下:

ClassParser cv = new ClassParser();

reader.accept(cv, ClassReader.SKIP_CODE);

现在,我们的解析程序就完成了。运行结果如下:

运行结果
代码样例:https://paste.ubuntu.com/p/8d6jN8jVzr/

七.使用范例:生成一个类

生成类我们用到的是ClassWriter,它本质上就是ClassVisitor,我们只要用可以构建类的数据按照刚才的格式传给它就能生成对应的类。

它的构造函数有两个,一个只传入一个int,它的值可为三个数:0、COMPUTE_MAXS和COMPUTE_FRAMES。那两个常量值是自动计算方法visitMaxs和visitFrame的,对于现在来说还用不到。另一个构造函数还需要传入ClassReader,这是下一部分可能用到的。

首先确定我们要构建产生的类:

即将生成的类,字段hi有警告是因为unused
首先创建ClassWriter实例:

ClassWriter cw = new ClassWriter(0);

接着,创建类,用到的正是visit方法。由于没有指定父类,这个类的父类将被强行指定为java/lang/Object,接口、抽象类、注释类型也如此。这个类没有实现任何接口,所以interfaces可以传null。同理,它没有泛型,所以泛型的signature为null。访问标志是public,再加上super,整体下来就是这句:

cw.visit(V1_8, ACC_PUBLIC + ACC_SUPER,

"com/github/nickid2018/asm/WillGenerate", null, "java/lang/Object",null);

接下来我们需要创建默认构造函数。javac编译时会把没有定义构造函数的普通类加入默认的构造函数。这种构造函数里面包括了父类构造函数调用和本身的非基本类型字段赋值。如果没有非基本类型字段赋值,那么它的代码就像这样:

默认构造函数的代码
由于这篇专栏主要是有关于类、字段、注释的解析,方法的解析暂时先不讲,所以这里只给出它的写入代码,不做讲解。

默认构造函数用ASM写入的实现代码
接下来写入HELLO这个字段。它的访问标志是public+static+final,由于它是弃用的,它也可以加上deprecated这个ASM自己定义的Access Flag。它的类型是int,所以描述符是I。没有泛型,所以signature为null。有默认值,为0。所以它的写入像这样:

FieldVisitor fv = cw.visitField(ACC_PUBLIC + ACC_FINAL + ACC_STATIC, "HELLO", "I", null, (Integer) 0);

保留这个FieldVisitor,因为它还具有一个注释@Deprecated。注释类型的描述符为Ljava/lang/Deprecated;。又因为@Deprecated的作用范围是RUNTIME,所以可见性为true,代码如下:

AnnotationVisitor av = fv.visitAnnotation("Ljava/lang/Deprecated;", true);

这时,这个字段就写入信息就完成了,调用visitEnd。

av.visitEnd();
fv.visitEnd();

下面写hi这个字段,和上面的差不多,直接给代码:

fv = cw.visitField(ACC_PRIVATE, "hi", "Ljava/lang/String;", null, null);
fv.visitEnd();

这时候类的所有信息都已经写完了,调用ClassWriter的visitEnd。

cw.visitEnd();

接下来调用ClassWriter的toByteArray获得字节码信息,写入到文件中就能得到类。

运行之后调用反编译器的结果:

使用JD-GUI反编译的结果
代码样例:https://paste.ubuntu.com/p/cqfDPVZbsH/

八.使用范例:修改一个类
修改类需要ClassReader和ClassWriter互相配合。利用ClassVisitor等进行数据的转移和修改。

接下来用字节码改一下我们的TestClass。

计划的修改
首先,创建ClassReader和ClassWriter。

ClassWriter cw = new ClassWriter(0);
ClassReader cr = new ClassReader("com/github/nickid2018/asm/TestClass");

之后在我们的ClassParser里面改一下,传入一个ClassWriter,使用父类的第二个构造函数:以int,ClassVisitor为参数的构造函数。这样,ClassReader传入的信息可以直接写到ClassWriter里面,我们只需要修改我们所需要的方法就可以达到修改的效果,而不用将所有ClassVisitor的方法实现。

ClassParser的构造函数
接下来解决第一个修改:改为抽象类。这个我们可以在visit里面修改,将原先的访问标志加一个abstract就好。

修改类为抽象类
第二个修改是重命名字段。这个在visitField里面判断就行,像下面一样:

在visitField里面写入这句话
第三个就是修改为final和加默认值,也是在visitField里面改动:

修改访问标志和默认值
最后用accept传入ClassParser,输出文件就是改好的类文件。

传入值进行修改
生成之后,用反编译器看一下结果。

反编译之后的结果
代码样例:https://paste.ubuntu.com/p/yXVvdJs3WH/

原文
https://www.bilibili.com/read/cv9803401?spm_id_from=333.999.0.0

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

推荐阅读更多精彩内容