ASM字节码处理工具原理及实践(二)
0. 相关分享
ASM字节码处理工具原理及实践(一)
上一篇讲了ASM的简介、导入,以及字节码文件结构,并给出了ASM通过ClassVisitor对class进行访问的基础实战。本篇将进入MethodVisitor,尝试对方法进行访问、生成、转换。方法的代码存储为字节码指令序列。在此之前,我们需要先复习JVM栈结构,才能更好地理解方法中字节码指令的逻辑。
1. JVM栈结构
一个JVM栈中包含了若干个栈帧,表征着一个个方法的调用栈。一个栈帧中存储着:
- 局部变量表(Local Variables)
- 操作数栈(Operand Stack)
- 动态链接(Dynamic Linking)指向运行时常量池的方法引用(MethodRef)
- 方法返回地址(Return Address)方法正常退出或异常退出的定义
- 附加信息
每个线程都有自己各自的栈,栈是线程私有的。栈帧的大小主要由局部变量表和操作数栈决定。操作数栈的深度、局部变量表的长度在编译器就已经确定,并写入到字节码中。如果栈帧只展示局部变量表和操作数栈,一个执行堆栈可能会为如下形式:
1.1 局部变量表
局部变量表也称为 局部变量数组 或 本地变量表。最基本的存储单元是 Slot (变量槽),参数值的存放总是在局部变量数组的 index = 0 开始,直到 数组长度-1 的索引结束。
局部变量表中,32位以内的类型占用一个 slot, 64位的类型(long、double)占用两个 slot。上图中的 L1、L2 等都忽略了slot的个数,可能L2是double类型,那么它应当占用 2 个 slot。局部变量表的长度按 slot 的个数计算。
1.2 操作数栈
独立的栈帧除了包含有局部变量表之外,还包含一个后进先出的操作数栈(Operand Stack)。操作数栈,在方法执行的执行过程中,根据字节码指令,往栈中写入数据或者提取数据,即入栈(push)和出栈(pop)
某些字节码指令将值压入操作数栈,其余的字节码指令将操作数栈取出栈,使用它们之后再把执行结果压入栈。
与局部变量表类似的,栈中可以是任意类型的Java数据类型
- 32位的类型占用一个栈单位深度
- 64位的类型占用两个栈单位深度
2. 字节码指令
字节码指令由操作码 opcode 和操作数 arguments 表征:
- 操作码 opcode 是一个无符号的字节值,是代号,由助记符标识。
- 操作数 arguments 是定义精确指令行为的静态值。
根据字节码指令和操作数栈的关系,字节码指令可以分为两类:一类字节码指令设计用于将值从局部变量表转移到操作数栈中,反之亦然;另一类则只操作操作数栈,它们从操作数栈弹出一些值,根据这些值进行计算,将结果重新压入操作数栈栈顶。
例如 ILOAD、LLOAD、FLOAD、DLOAD和ALOAD指令读取一个局部变量的值,并将这个值压入到操作数栈中。由于这些指令操作的是局部变量表,需要提供表/数组的索引 i 作为参数,来表示读取哪一个局部变量。我们之前提到32位以内的类型都存入到一个 slot 中,反过来,提取的时候这里 ILOAD 可以用于加载 boolean、byte、char、short和int类型的局部变量。不仅如此,我们也说了 64位 的局部变量将会占用两个插槽 slot ,故LLOAD、DLOAD加载数据时候,实际上加载了 i 和 i+1 两个插槽的内容。ALOAD 用于加载其他类型,比引用类型、数组引用类型等。
我们可以观察到 xLOAD和 xSTORE指令都是由x表征类型的,者用来确保不进行非法的转换。
除了上述 xLOAD 和 xSTORE 的指令之外,其他字节码指令只在操作数栈上工作。这里给出字节码指令的汇总:
注意:
- a 和 b 表示 int、float、long、double类型
- o 和 p 表示 对象引用类型
- v 表示单位为1的类型
- w 表示long、double这样单位为2的类型
- i 和 j 和 n 表示 int 类型
2.1 Local variables 局部变量
指令 | 栈(原先) | 栈(指令执行后) |
---|---|---|
ILOAD,LLOAD,FLOAD,DLOAD var | … | …, a |
ALOAD var | … | …, o |
ISTORE,LSTORE,FSTORE,DSTORE var | …, a | … |
ASTORE var | …, o | … |
IINC var incr | … | … |
示例:
【局部变量压栈指令】将一个局部变量加载到操作数栈:xload 、xload_<n>
(其中x为i、l、f、d、a, n从0到3
aload_0 // 将局部变量表中0号局部变量的值压入到操作数栈
aload 5 // 将局部变量表中5号局部变量的值压入操作数栈
【出栈装入局部变量表指令】将一个数值从操作数栈存储到局部变量表:xstore、xstore_<n>
(其中x为i、l、f、d、a,n为0到3);xastore
(其中x为i、l、f、d、a、b、c、s)
2.2 Stack 操作数栈
指令 | 栈(原先) | 栈(指令执行后) |
---|---|---|
POP | …, v | … |
POP2 | …, v1, v2 | … |
…, w | … | |
DUP | …, v | …, v , v |
DUP2 | …, v1, v2 | …, v1, v2, v1, v2 |
…, w | …, w, w | |
SWAP | …, v1, v2 | …, v2, v1 |
DUP_X1 | …, v1, v2 | …, v2, v1, v2 |
DUP_X2 | …, v1, v2, v3 | …, v3, v1, v2, v3 |
…, w, v | …, v, w, v | |
DUP2_X1 | …, v1, v2, v3 | …, v2, v3, v1, v2, v3 |
…, v, w | …, w, v, w | |
DUP2_X2 | …, v1, v2, v3, v4 | …, v3, v4, v1, v2, v3, v4 |
…, w, v1, v2 | …, v1, v2, w, v1, v2 | |
…, v1, v2, w | …, w, v1, v2, w | |
…, w1, w2 | …, w2, w1, w2 |
2.3 Constants 常量操作
指令 | 栈(原先) | 栈(指令执行后) |
---|---|---|
ICONST_n (−1 ≤ n ≤ 5) | … | … , n |
LCONST_n (0 ≤ n ≤ 1) | … | … , nL |
FCONST_n (0 ≤ n ≤ 2) | … | … , nF |
DCONST_n (0 ≤ n ≤ 1) | … | … , nD |
BIPUSH b, −128 ≤ b < 127 | … | … , b |
SIPUSH s, −32768 ≤ s < 32767 | … | … , s |
LDC cst (int, float, long, double, String or Type) | … | … , cst |
ACONST_NULL | … | … , null |
【常量入栈指令】将一个常量加载到操作数栈:bipush、sipush、ldc、ldc_w、ldc2_w、aconst_null、iconst_m1、iconst_<i>、lconst_<l>)、fconst_<f>、dconst_<d>
示例:
ldc指令可以接受一个8位的参数,指向常量池中int、float或者String的索引,并将指定的内容压入操作数栈
例如:ldc #9 , 常量池表#9为“hello”,将这个字符串索引压入栈中
ldc_w 把常量池中的项压入栈,w表示使用宽索引,款缩影支持索引范围大于ldc,接收两个8位的参数
ldc2_w 把常量池中 long 或者 double 类型的项压入栈
aconst_null 把null压入操作数栈
fconst_0 把浮点数0压入栈
2.4 Arithmetic and logic 计算和逻辑运算
指令 | 栈(原先) | 栈(指令执行后) |
---|---|---|
IADD,LADD,FADD,DADD | …, a, b | …, a+b |
ISUB,LSUB,FSUB,DSUB | …, a, b | …, a- b |
IMUL, LMUL, FMUL, DMUL | … , a , b | … , a * b |
IDIV, LDIV, FDIV, DDIV | … , a , b | … , a / b |
IREM, LREM, FREM, DREM | … , a , b | … , a % b |
INEG, LNEG, FNEG, DNEG (negtive) | … , a | … , -a |
ISHL, LSHL (left) | … , a , n | … , a << n |
ISHR, LSHR (right) | … , a , n | … , a >> n |
IUSHR, LUSHR | … , a , n | … , a >>> n |
IAND, LAND | … , a , b | … , a & b |
IOR, LOR | … , a , b | … , a | b |
IXOR, LXOR | … , a , b | … , a ^ b |
LCMP | … , a , b | … , a == b ? 0 : (a < b ? -1 : 1) |
FCMPL, FCMPG | … , a , b | … , a == b ? 0 : (a < b ? -1 : 1) |
DCMPL, DCMPG | … , a , b | … , a == b ? 0 : (a < b ? -1 : 1) |
2.4 Cast类型转换
指令 | 栈(原先) | 栈(指令执行后) |
---|---|---|
I2B | … , i | … , (byte) i |
I2C | … , i | … , (char) i |
I2S | … , i | … , (short) i |
L2I, F2I, D2I | … , a | … , (int) a |
I2L, F2L, D2L | … , a | … , (long) a |
I2F, L2F, D2F | … , a | … , (float) a |
I2D, L2D, F2D | … , a | … , (double) a |
CHECKCAST class | … , o | … , (class) o |
从byte、char、short类型到int类型的宽化类型转换实际上是不存在的,虚拟机对这种类型转换并没有做实质性的转化处理,只是通过操作数栈交换了两个数据。
窄化数据转换可能发生精度丢失,可能丢失掉几个最低有效位上的值,转换后的浮点数值根据IEEE754最接近含入模式所得到的正确整数值。窄化类型转换可能会发生上限溢出、下限溢出和精度丢失等情况。
2.5 Objects对象、Field字段、Method方法
c: class类, f: field字段名, m:method方法名, t: description描述符
指令 | 栈(原先) | 栈(执行后) |
---|---|---|
NEW class | … | …, new class |
GETFIELD c f t | …, o | … , o.f |
PUTFIELD c f t | … , o , v | … |
GETSTATIC c f t | … | … , c.f |
PUTSTATIC c f t | … , v | … |
INVOKEVIRTUAL c m t | … , o , v1 , … , vn | … , o.m(v1, … vn) |
INVOKESPECIAL c m t | … , o , v1 , … , vn | … , o.m(v1, … vn) |
INVOKESTATIC c m t | … , v1 , … , vn | … , c.m(v1, … vn) |
INVOKEINTERFACE c m t | … , o , v1 , … , vn | … , o.m(v1, … vn) |
INVOKEDYNAMIC m t bsm | … , o , v1 , … , vn | … , o.m(v1, … vn) |
INSTANCEOF class | … , o | … , o instanceof class |
MONITORENTER | … , o | … |
MONITOREXIT | … , o | … |
2.6 Arrays集合
指令 | 栈(原先) | 栈(指令执行后) |
---|---|---|
NEWARRAY type (for any primitive type) | … , n | … , new type[n] |
ANEWARRAY class | … , n | … , new class[n] |
MULTIANEWARRAY […[t n | … , i1 , … , in | … , new t[i1]…[in]… |
BALOAD, CALOAD, SALOAD | … , o , i | … , o[i] |
IALOAD, LALOAD, FALOAD, DALOAD | … , o , i | … , o[i] |
AALOAD | … , o , i | … , o[i] |
BASTORE, CASTORE, SASTORE | … , o , i , j | … |
IASTORE, LASTORE, FASTORE, DASTORE | … , o , i , a | … |
AASTORE | … , o , i , p | … |
ARRAYLENGTH | … , o | … , o.length |
示例:
public class Student {public String[] infos;public String getInfo(int index){return infos[index];}
}
其中 getInfo(int index) 的字节码指令为:
0 aload_0 // 先将 this 压入栈
1 getfield #2 <asmcore/base/Student.infos> //拿到class下的infos变量,这是个 [Ljava/lang/String 类型的
4 iload_1 //将局部变量表 1号 变量 index 压入栈
5 aaload //..., o, i -> ..., o[i]
6 areturn //将 o[i] 返回出去
由于 getInfo 是非静态方法,所以局部变量表的0号局部变量为this引用
2.7 Jumps 跳转指令
指令 | 栈(原先) | 说明 |
---|---|---|
IFEQ | … , i | jump if i == 0 |
IFNE | … , i | jump if i != 0 |
IFLT | … , i | jump if i < 0 |
IFGE | … , i | jump if i >= 0 |
IFGT | … , i | jump if i > 0 |
IFLE | … , i | jump if i <= 0 |
IF_ICMPEQ | … , i , j | jump if i == j |
IF_ICMPNE | … , i , j | jump if i != j |
IF_ICMPLT | … , i , j | jump if i < j |
IF_ICMPGE | … , i , j | jump if i >= j |
IF_ICMPGT | … , i , j | jump if i > j |
IF_ICMPLE | … , i , j | jump if i <= j |
IF_ACMPEQ | … , o , p | jump if o == p |
IF_ACMPNE | … , o , p | jump if o != p |
IFNULL | … , o | jump if o == null |
IFNONNULL | … , o | jump if o != null |
GOTO | … | jump always |
TABLESWITCH | … , i | jump always |
LOOKUPSWITCH | … , i | jump always |
示例:
public class Student {public String[] infos;public String getInfo(int index){if (index < 0 ){return "nothing";}else{return infos[index];}}
}
编译后,代码优化为:
public class Student {public String[] infos;public Student() {}public String getInfo(int index) {return index < 0 ? "nothing" : this.infos[index];}
}
其中 getInfo(int index) 字节码指令为:
0 iload_1 //将1号局部变量index压入栈1 ifge 7 (+6) //如果栈顶元素大于0,跳转到7行4 ldc #2 <nothing> //从常量池中将 "nothing" 压入栈6 areturn //将栈顶 “nothing” 返回出去7 aload_0 //如果栈顶元素大于0,来到这里,将0号局部变量this压入栈8 getfield #3 <asmcore/base/Student.infos> //拿到其field
11 iload_1 //将1号局部变量index压入栈
12 aaload //...,o,i -> ..., o[i]
13 areturn //将栈顶 o[i] 返回出去
在 ASM 中字节码可以表示为:
public getInfo(I)Ljava/lang/String; //方法L0LINENUMBER 7 L0 //记录行号ILOAD 1 //获取1号int类型局部变量 index 压入栈IFGE L1 //如果 index >=0 跳转到 L1L2LINENUMBER 8 L2 //记录行号LDC "nothing" //将常量 "nothing" 压入栈ARETURN //栈顶返回引用类型对象L1LINENUMBER 10 L1 //记录行号FRAME SAME //帧ALOAD 0 //获取0号引用类型局部变量 this 压入栈GETFIELD asmcore/base/Student.infos : [Ljava/lang/String; //拿到 this 的String[]类型的 infos 字段ILOAD 1 //获取1号int类型局部变量 index 压入栈AALOAD //...,o,i -> ..., o[i]ARETURN //栈顶返回引用类型对象L3LOCALVARIABLE this Lasmcore/base/Student; L0 L3 0 //局部变量表中变量this索引为0LOCALVARIABLE index I L0 L3 1 //局部变量表中index变量索引为1MAXSTACK = 2 //操作数栈深度MAXLOCALS = 2 //局部变量表长度
2.8 Return 返回指令
指令 | 栈 | 说明 |
---|---|---|
IRETURN, LRETURN, FRETURN, DRETURN | … , a | 返回数据类型 |
ARETURN | … , o | 返回引用类型 |
RETURN | … | 返回类型为void |
ATHROW | …, o | 抛出异常结束执行 |
3. MethodVisitor
与 ClassVisitor类似,MethodVisitor也有访问回调顺序:
-
visitAnnotationDefalt 最多一次
-
( visitAnnotation | visitParameterAnnotation | visitAttribute ) 零或多次
-
( visitCode
(visitTryCatchBlock | visitLabel | visitFrame | visitXxxInsn | visitLocalVariable | visitLineNumber) 零或多次
visitMaxs ) 最多一次
-
visitEnd 固定一次
MethodVisitor 的相关方法为:
abstract class MethodVisitor { // public accessors ommitedMethodVisitor(int api);MethodVisitor(int api, MethodVisitor mv);AnnotationVisitor visitAnnotationDefault();AnnotationVisitor visitAnnotation(String desc, boolean visible);AnnotationVisitor visitParameterAnnotation(int parameter,String desc, boolean visible);void visitAttribute(Attribute attr);void visitCode();void visitFrame(int type, int nLocal, Object[] local, int nStack,Object[] stack);void visitInsn(int opcode);void visitIntInsn(int opcode, int operand);void visitVarInsn(int opcode, int var);void visitTypeInsn(int opcode, String desc);void visitFieldInsn(int opc, String owner, String name, String desc);void visitMethodInsn(int opc, String owner, String name, String desc);void visitInvokeDynamicInsn(String name, String desc, Handle bsm,Object... bsmArgs);void visitJumpInsn(int opcode, Label label);void visitLabel(Label label);void visitLdcInsn(Object cst);void visitIincInsn(int var, int increment);void visitTableSwitchInsn(int min, int max, Label dflt, Label[] labels);void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels);void visitMultiANewArrayInsn(String desc, int dims);void visitTryCatchBlock(Label start, Label end, Label handler,String type);void visitLocalVariable(String name, String desc, String signature,Label start, Label end, int index);void visitLineNumber(int line, Label start);void visitMaxs(int maxStack, int maxLocals);void visitEnd();
}
我们发现,annotations 和 attributes 必须要首先访问,然后再访问方法的方法体。按照上述的访问顺序进行访问。visitCode 和 visitMaxs 表征着当前来到方法体的开始、结束位置。和 ClassVisitor 类似, visitEnd 在最后被调用,表征着访问事件的结束。
我们并不能直接访问到方法,而是需要通过一些 MethodVisitor 相关的API 来访问:
- 首先通过 ClassReader 的 accept 方法来开启访问事件,将事件传递给 ClassVisitor
- 当访问到方法时,ClassVisitor会被调用 visitMethod 方法,这个方法返回了一个 MethodVisitor实例,接下去进入这个 MethodVisitor进行方法的访问事件的回调
- MethodVisitor 也把它接收到的所有方法转发给另一个 MethodVisitor 实例,所以它也可以被看做是一个事件过滤器。
最后通常还是会来到事件消费者 ClassWriter,当它接收到 MethodVisitor 传来的方法访问细节时,可能会面临计算帧、计算局部变量表、操作数栈大小的问题。 ASM 为我们提供了几种选择:
new Classwriter(0)
:不做任何自动化计算,程序员必须自己计算帧、局部变量表和操作数栈的大小new ClassWriter(ClassWriter.COMPUTE_MAXS)
:自动计算局部变量表和操作数栈的大小,你仍然必须调用 visitMaxs,但它的参数将不被使用。new ClassWriter(ClassWriter.COMPUTE_FRAMES)
:所有东西都被自动计算,你无需调用 visitFrame,但你仍然必须调用 visitMaxs(参数将忽略,不被使用)
自动计算有好处,就是方便了程序员的开发,但是它会带来性能损耗,官方给出:使用 COMPUTE_MAXS 会让性能降低 10% , 使用 COMPUTE_FRAMES 会让性能降低 20% 。
如果我们要自行计算帧,我们要使用 visitFrame(F_NEW, nLocals, locals, nStack, stack),其中 nLocals 和 nStack 是局部变量表和操作数栈的大小, locals 和 stack 是相应的集合。自动计算帧的时候,可能会加载父类到JVM,并通过反射的手段做一些事情,如果你正在生成的几个类相互之间有关联,可能关联的类此时还不存在,会出现自动计算错误。官方提示可以通过重写 getCommonSuperClass方法来解决这个问题。
3.1 生成一个 Method 方法 - ClassWriter+MethodVisitor
假设我们当前有一个类:
public class Bean{public int f;
}
我要加一个给f设置值的方法:
public class Bean{public int f;//添加一个方法:public void checkAndSet(int f){if(f >= 0){this.f = f;}else{throw new IllegalArgumentException();}}
}
我们可以在把访问事件分发给 ClassWriter 的时候,模拟分发一个原本不存在的MethodVisitor的访问事件,从而实现让 ClassWriter 添加一个方法的效果,首先根据 ASMPlugin 查看字节码形式,或者 jclasslib查看也行:
使用ASMPlugin查看
public checkAndSet(I)VL0LINENUMBER 8 L0ILOAD 1IFLT L1L2LINENUMBER 9 L2ALOAD 0ILOAD 1PUTFIELD asmcore/base/Bean.f : IGOTO L3L1LINENUMBER 11 L1FRAME SAMENEW java/lang/IllegalArgumentExceptionDUPINVOKESPECIAL java/lang/IllegalArgumentException.<init> ()VATHROWL3LINENUMBER 13 L3FRAME SAMERETURNL4LOCALVARIABLE this Lasmcore/base/Bean; L0 L4 0LOCALVARIABLE f I L0 L4 1MAXSTACK = 2MAXLOCALS = 2
使用 jclasslib 查看:
0 iload_11 iflt 12 (+11)4 aload_05 iload_16 putfield #2 <asmcore/base/Bean.f>9 goto 20 (+11)
12 new #3 <java/lang/IllegalArgumentException>
15 dup
16 invokespecial #4 <java/lang/IllegalArgumentException.<init>>
19 athrow
20 return
根据 ASMPlugin 的结果,我们来尝试构建MethodVisitor大概的代码结构,分析字节码,其逻辑大概是判断传入参数 f 是否非负,如果是,继续执行字节码,如果不是,跳转到另一个代码块(跳转到另一个label/跳转到另一个代码段起始位置)。需要注意 visitCode为方法代码的开始标志,visitEnd为方法代码的结束标志。
mv.visitCode();//模拟访问代码开始
mv.visitVarInsn(ILOAD, 1);//变量操作,将传入参数f压入操作数栈
Label label = new Label();
mv.visitJumpInsn(IFLT,label);//跳转标志,跳转判断的数字为操作数栈顶元素,如果符合条件,跳入else{}代码块的label标识
//如果上述判断成功,继续执行下面字节码指令
mv.visitVarInsn(ALOAD, 0);//把this压入栈
mv.visitVarInsn(ILOAD, 1);//把传入参数f压入操作数栈(局部变量表中并没有把f剔除,所以可以多次使用,例如压入操作数栈)
mv.visitFieldInsn(PUTFIELD,"asmcore/base/Bean","f","I");//访问field字段,Bean类的f字段,描述符为I,意为int类型数据
Label end = new Label();
mv.visitJumpInsn(GOTO, end);//if(){}的代码块执行完成,进入return,return这块代码由end标识
//来到else{}代码块的label部分
mv.visitLabel(label);//打上label标签
mv.visitFrame(F_SAME, 0, null, 0, null);//自行计算帧
mv.visitTypeInsn(NEW,"java/lang/IllegalArgumentException");//类相关,new一个对象
mv.visitInsn(DUP);//在操作数栈中复制一份栈顶元素
mv.visitMethodInsn(INVOKESPECIAL,"java/lang/IllegalArgumentException","<init>","()V");//取出栈顶元素,调用这个类的构造函数,得到的对象放入操作数栈顶
mv.visitInsn(ATHROW);//无参指令,ATHROW将栈顶对象作为异常对象抛出
//来到return代码块的label部分
mv.visitLabel(end);
mv.visitFrame(F_SAME, 0, null, 0, null);//自行计算帧
mv.visitInsn(RETURN);//直接返回
mv.visitMaxs(2,2);//局部变量表、操作数栈的深度计算
mv.visitEnd();//方法的代码段结束
我们来实践一下:
- ClassReader读取 Bean 类
- 将事件传递给 ClassVisitor,其中这个ClassVisitor的visitEnd()中,模拟转发这个方法访问事件的转发,交给ClassWriter去消费/记录
首先,我们写一个 AddMethodAdapter 用来添加模拟转发方法事件,且将模拟转发方法访问事件设计在ClassVisitor的 visitEnd 调用返回之前完成。
public class AddMethodAdapter extends ClassVisitor {public AddMethodAdapter(ClassVisitor downstream) {super(ASM4,downstream);}@Overridepublic void visitEnd() {//模拟添加一个方法MethodVisitor mv = super.visitMethod(0x1,"checkAndSet","(I)V",null,new String[]{});mv.visitCode();//模拟访问代码开始mv.visitVarInsn(ILOAD, 1);//变量操作,将传入参数f压入操作数栈Label label = new Label();mv.visitJumpInsn(IFLT,label);//跳转标志,跳转判断的数字为操作数栈顶元素,如果符合条件,跳入else{}代码块的label标识
//如果上述判断成功,继续执行下面字节码指令mv.visitVarInsn(ALOAD, 0);//把this压入栈mv.visitVarInsn(ILOAD, 1);//把传入参数f压入操作数栈(局部变量表中并没有把f剔除,所以可以多次使用,例如压入操作数栈)mv.visitFieldInsn(PUTFIELD,"asmcore/base/Bean","f","I");//访问field字段,Bean类的f字段,描述符为I,意为int类型数据Label end = new Label();mv.visitJumpInsn(GOTO, end);//if(){}的代码块执行完成,进入return,return这块代码由end标识
//来到else{}代码块的label部分mv.visitLabel(label);//打上label标签mv.visitFrame(F_SAME, 0, null, 0, null);//自行计算帧mv.visitTypeInsn(NEW,"java/lang/IllegalArgumentException");//类相关,new一个对象mv.visitInsn(DUP);//在操作数栈中复制一份栈顶元素mv.visitMethodInsn(INVOKESPECIAL,"java/lang/IllegalArgumentException","<init>","()V",false);//取出栈顶元素,调用这个类的构造函数,得到的对象放入操作数栈顶mv.visitInsn(ATHROW);//无参指令,ATHROW将栈顶对象作为异常对象抛出
//来到return代码块的label部分mv.visitLabel(end);mv.visitFrame(F_SAME, 0, null, 0, null);//自行计算帧mv.visitInsn(RETURN);//直接返回mv.visitMaxs(2,2);//局部变量表、操作数栈的深度计算mv.visitEnd();//方法的代码段结束super.visitEnd();}
}
接下去就可以按我们的老套路,开始利用这个代码增强的性质,进行链式转发:
ClassWriter cw= new ClassWriter(0);
AddMethodAdapter addMethodAdapter = new AddMethodAdapter(cw);
try {ClassReader cr = new ClassReader("asmcore.base.Bean");cr.accept(addMethodAdapter,0);byte[] b = cw.toByteArray();//将byte[]写入文件save(b,"Bean");
} catch (IOException e) {e.printStackTrace();
}
最后我们查看生成的字节码文件的反编译结果:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//package asmcore.base;public class Bean {public int f;public Bean() {}public void checkAndSet(int var1) {if (var1 >= 0) {this.f = var1;} else {throw new IllegalArgumentException();}}
}
符合我们的预期。
3.2 转变方法 Transforming Methods
除了增加、删除一个方法,我们可能还要对原有方法进行修改,例如在原有代码的基础上进行代码增强。我们尝试在方法的开头和结束位置加上一个执行时间的记录。假设我们有一个工具类,其中代码增强部分为该方法的耗时计算:
public class Util {//耗时操作时间记录public void doSomething() throws Exception {//----增加的代码begin----long begin = System.currentTimeMillis();//----增加的代码end----Thread.sleep(100);//----增加的代码begin----System.out.println(System.currentTimeMillis() - begin);//----增加的代码end----}
}
我们来看一下这个字节码是什么样的,先来看一下没有增加代码的时候的情况:
public doSomething()V throws java/lang/Exception L0LINENUMBER 8 L0LDC 100INVOKESTATIC java/lang/Thread.sleep (J)VL1LINENUMBER 10 L1RETURNL2LOCALVARIABLE this Lasmcore/base/Util; L0 L2 0MAXSTACK = 2MAXLOCALS = 1
然后是增加代码之后的情况,在其中标注出了新增加的内容:
public doSomething()V throws java/lang/Exception L0//新代码LINENUMBER 7 L0//新增加了方法调用INVOKESTATIC java/lang/System.currentTimeMillis ()J//将返回值存入var1(也就是 begin 这个本地变量)LSTORE 1L1//原有代码LINENUMBER 8 L1LDC 100INVOKESTATIC java/lang/Thread.sleep (J)VL2//新代码LINENUMBER 9 L2//获取PrintStream对象GETSTATIC java/lang/System.out : Ljava/io/PrintStream;//调用System.currentMillisINVOKESTATIC java/lang/System.currentTimeMillis ()J//将begin的值存入操作数栈LLOAD 1//操作数栈减法LSUB//调用打印(传入参数就一个,就是操作数栈栈顶元素)INVOKEVIRTUAL java/io/PrintStream.println (J)VL3//原有代码LINENUMBER 10 L3RETURNL4//局部变量表信息LOCALVARIABLE this Lasmcore/base/Util; L0 L4 0LOCALVARIABLE begin J L1 L4 1MAXSTACK = 5MAXLOCALS = 3
由于这里引入了新的局部变量,所以我们需要处理局部变量表,这部分我们直接使用 LocalVariablesSorter 这个封装好的 MethodVisitor 的实现类,帮我们处理局部变量。ClassWriter 使用 ClassWriter.COMPUTE_MAXS 来自动计算局部变量表。其他部分尽量使用原生API,保持与上文描述一致:
public class AddTimerAdapter extends ClassVisitor {public AddTimerAdapter(ClassVisitor downstream) {super(ASM4,downstream);}//如果是 doSomething 方法,就对这个方法进行代码增强@Overridepublic MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {if (name.equals("doSomething")){return new TimerMethodVisitor(ASM4,access,descriptor,cv.visitMethod(access, name, descriptor, signature, exceptions));}return cv.visitMethod(access, name, descriptor, signature, exceptions);}public static class TimerMethodVisitor extends LocalVariablesSorter {int beginIndex;protected TimerMethodVisitor(int api, int access, String descriptor, MethodVisitor methodVisitor) {super(api, access, descriptor, methodVisitor);}@Overridepublic void visitCode() {//在原来MethodVisitor的基础上,在代码开始的地方插入内容mv.visitCode();//调用System.currentMillis方法mv.visitMethodInsn(INVOKESTATIC,"java/lang/System","currentMillis","()J",false);//由于这里无法处理局部变量表、操作数栈的下标,所以需要让Writer自己去做//借助 AdviceAdapter 帮我们封装好新增局部变量的方法进行新增局部变量beginIndex = newLocal(Type.LONG_TYPE);//index为本地变量下标//存入操作数栈mv.visitVarInsn(LSTORE,beginIndex);}//在方法退出之前计算时间@Overridepublic void visitInsn(int opcode) {//需要注意的是owner给的是类,descriptor是描述符//如果是return,或者是throw exception,就提前打印时间if ((opcode >= IRETURN && opcode <= RETURN ) || opcode == ATHROW){//获取printStream对象,存入操作数栈mv.visitFieldInsn(GETSTATIC,"java/lang/System","out","Ljava/io/PrintStream;");//调用System.currentMillis方法mv.visitMethodInsn(INVOKESTATIC,"java/lang/System","currentMillis","()J",false);//将begin的值存入操作数栈mv.visitVarInsn(LLOAD,beginIndex);//操作数栈减法mv.visitInsn(LSUB);//调用打印mv.visitMethodInsn(INVOKEVIRTUAL,"java/io/PrintStream","println","(J)V",false);}super.visitInsn(opcode);}@Overridepublic void visitMaxs(int maxStack, int maxLocals) {//其实这里没有用了,因为ClassWriter设置了 COMPUTE_MAXSsuper.visitMaxs(maxStack+2, maxLocals);}}
}
我们来调用一下,查看结果:
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
AddTimerAdapter addTimerAdapter = new AddTimerAdapter(cw);
try {ClassReader cr = new ClassReader("asmcore.base.Util");cr.accept(addTimerAdapter,0);byte[] b = cw.toByteArray();save(b,"Util");
} catch (IOException e) {e.printStackTrace();
}
运行后得到的字节码为:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//package asmcore.base;public class Util {public Util() {}public void doSomething() throws Exception {long var1 = System.currentMillis();Thread.sleep(100L);System.out.println(System.currentMillis() - var1);}
}
符合预期。
相关文章:

ASM字节码处理工具原理及实践(二)
0. 相关分享 ASM字节码处理工具原理及实践(一) 上一篇讲了ASM的简介、导入,以及字节码文件结构,并给出了ASM通过ClassVisitor对class进行访问的基础实战。本篇将进入MethodVisitor,尝试对方法进行访问、生成、转换。…...

Golang每日一练(leetDay0030)
目录 88. 合并两个有序数组 Merge Sorted Array 🌟 89. 格雷编码 Gray Code 🌟🌟 90. 子集 II Subsets II 🌟🌟 🌟 每日一练刷题专栏 🌟 Golang每日一练 专栏 Python每日一练 专栏 C/…...

QT5.15.2 在线安装下载速度慢的解决办法
系列文章目录 文章目录系列文章目录前言一、解决前言 QT对5.15以及以上版本已经停止提供离线安装包,在线安装网速慢如蜗牛,而且一旦断了又得从头下载,不支持断点续传 由于Qt5.15及以上版本不提供离线安装包,则需要使用在线安装进…...
Cadence Allegro 导出Waived Design Rules Check Report报告详解
⏪《上一篇》 🏡《上级目录》 ⏩《下一篇》 目录 1,概述2,Waived Design Rules Check Report作用3,Waived Design Rules Check Report示例4,Waived Design Rules Check Report导出方法4.1,方法14.2,方法2B站关注“硬小二”浏览更多演示视频...

Java阶段一Day19
Java阶段一Day19 文章目录Java阶段一Day19对象流字符流WriterReader转换流缓冲字符流BufferedWriter与PrintWriterBufferedReader异常Throwable、Error、Exception异常处理机制throwthrowstry-catchfinally面试题教师总结新单词JAVA IO对象流对象输入流构造器方法例transient关…...

radmin远程控制软件怎么样,有没有替代品
Radmin 是流行的、屡获殊荣的安全远程控制软件,它使您能够在远程计算机上实时工作,就像使用它自己的键盘和鼠标一样。 您可以从多个地方远程访问同一台计算机,是网络和管理类别中流行的远程桌面工具。 Radmin 是外国软件,在国内使…...
Java反射面试总结(一)
什么是反射? Java的反射是指在程序运行时,对于任意一个类,都可以获取到这个类的所有属性和方法,并能够对其进行操作。通过反射机制,可以在程序运行时动态地创建对象、调用方法、获取属性值等。反射可以帮助我们更轻松…...

【论文阅读】3D-LaneNet
【论文阅读】3D-LaneNet 主要要做的事情就是 lane detection。这里提一下 BEV(Bird‘s Eye View) 感知算法,为了将 2D 图像映射到 3D 空间中,能够更准确的检测物体位置,需要 BEV 感知的结果。后续还会继续了解这方面内…...

Kafka的概念|架构|搭建|查看命令
Kafka的概念|架构|搭建|查看命令一 Kafka 概述二 使用消息队列的好处三Kafka 定义3.1Kafka 简介3.2Kafka 的特性3.3 Kafka 系统架构3.4 Partation 数据路由规则四 kafka的架构五 搭建kafka5.1环境准备5.2安装kafka5.3 修改配置文件5.4 编辑其他二台虚拟机的配置文件5.5 编辑三台…...

大数据项目实战之数据仓库:电商数据仓库系统——第5章 数据仓库设计
第5章 数据仓库设计 5.1 数据仓库分层规划 优秀可靠的数仓体系,需要良好的数据分层结构。合理的分层,能够使数据体系更加清晰,使复杂问题得以简化。以下是该项目的分层规划。 5.2 数据仓库构建流程 以下是构建数据仓库的完整流程。 5.2.1 …...

OpenHarmony社区运营报告(2023年3月)
目录 本月快讯 一、代码贡献 二、生态进展 三、社区治理 五、社区活动 六、社区及官网运营 本月快讯 • 《OpenHarmony 2022年度运营报告》于3月正式发布,2022年OpenAtom OpenHarmony(以下简称“OpenHarmony”)开源项目潜心务实、深耕发展&am…...

杰林码图像增强算法——超分辨率、图像放大、轮廓和色彩强化算法(二)
一、前言 2023-03-23我发布了基于加权概率模型(杰林码的理论模型)的图像颜色增强和轮廓预测的应用方法。效果还不太明显,于是我又花了2周的时间进行了技术优化。下面仅提供了x86下的BMP和JPG对应的lib和dll,本文中的算法属于我国…...

在three.js中废置对象
基于three.js子如何废置对象(How to dispose of objects) 前言: 为了提高性能,并避免应用程序中的内存泄露,一个重要的方面是废置未使用的类库实体。 每当创建一个three.js中的实例时,都会分配一定数量的内存。然而,three.js会创建在渲染中所必需的特定对象, 例如几何…...

Java中的String类真的不可变吗?
其实在Java中,String类被final修饰,主要是为了保证字符串的不可变性,进而保证了它的安全性。那么final到底是怎么保证字符串安全性的呢?接下来就让我们一起来看看吧。 一. final的作用 1. final关键词修饰的类不可以被其他类继…...

电脑重装了系统开不了机怎么办?
我们的电脑办公用久后也会出现故障问题,例如卡顿反应慢等等,这时候就要进行重装系统了,但是很多小伙伴重装系统后会出现开不了机的问题,其实我们比较常见的也就是电脑重装系统开不了机的情况。有很多小伙伴反映自己不知道应该怎么…...
SPOJ-NSUBSTR - Substrings(SAM求所有长度子串的最大出现次数)
NSUBSTR - Substrings 题面翻译 你得到了一个最多由 250000250000250000 个小写拉丁字母组成的字符串 SSS。定义 F(x)F(x)F(x) 为 SSS 的某些长度为 xxx 的子串在 SSS 中的最大出现次数。即 F(x)max{times(T)}F(x)max\{times(T)\}F(x)max{times(T)},满足 TTT 是 S…...

Mariadb10.5基于同服务器多实例主从配置
本次部署环境:Centos8stream 本次部署mariadb版本: mariadb:10.5 本次部署方式:rpm包直接安装,并通过systemd直接托管 可以参考 /usr/lib/systemd/system/mariadb.service 该文件 # Multi instance version of mariadb. For i…...

linux 修改主机名称
1、hostname命令进行临时更改 如果只需要临时更改主机名,可以使用hostname命令: sudo hostname <new-hostname> 例如: 只需重新打开session终端,就能生效, 但是,重启计算机后会回到旧的主机名。…...

学校的地下网站(学校的地下网站1080P高清)
这个问题本身就提得有问题,为什么这么说,这是因为YouTube本身就不是一个视频网站或者说YouTube不是一个传统的视频网站!!! YouTube能够一家独大,可不仅仅是因为有了Google 这个亲爹,还有一点&am…...
勒索病毒是什么?如何防勒索病毒
勒索病毒并不是某一个病毒,而是一类病毒的统称,主要以邮件、程序、木马、网页挂马的形式进行传播,利用各种加密算法对文件进行加密,被感染者一般无法解密,必须拿到解密的私钥才有可能破解。 已知最早的勒索软件出现于 …...
SpringBoot+VUE+Axios 【链接超时】 后端正常返回结果,前端却出现错误无法接收数据
一、错误原因及解决思路 错误提示表明前端发送的请求在默认的 2500ms 超时时间内没有得到服务器的响应,导致请求失败。尝试以下方法来解决这个问题: 增加请求超时时间:可以通过配置 Axios 请求对象的 timeout 属性来增加请求的超时时间&…...

【状态估计】基于增强数值稳定性的无迹卡尔曼滤波多机电力系统动态状态估计(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...

快速排序的简单理解
详细描述 快速排序通过一趟排序将待排序列分割成独立的两部分,其中一部分序列的关键字均比另一部分序列的关键字小,则可分别对这两部分序列继续进行排序,以达到整个序列有序的目的。 快速排序详细的执行步骤如下: 从序列中挑出…...

短视频多平台发布软件功能详解
随着移动互联网的普及和短视频的兴起,短视频发布软件越来越受到人们的关注。短视频发布软件除了常规的短视频发布功能,还拥有智能创作、帐号绑定、短视频一键发布、视频任务管理和数据统计等一系列实用功能。下面我们将分步骤详细介绍一下这些功能。 …...

谷歌人机验证Google reCAPTCHA
reCAPTCHA是Google公司推出的一项验证服务,使用十分方便快捷,在国外许多网站上均有使用。它与许多其他的人机验证方式不同,它极少需要用户进行各种识图验证。 它的使用方式如下如所示,只需勾选复选框即可通过人机验证。 虽然简单…...

VB+ACCESS电脑销售系统的设计与实现
为了使此系统简单易学易用、功能强大、软件费用支出低、见效快等特点,我们选择Visual Basic6.0开发此系统。Visual Basic6.0起代码有效率以达到Visual c的水平。在面向对象程序设计方面,Visual Basic6.0全面支持面向对你程序设计包括数据抽象、封装、对象…...

嵌入式开发:硬件和软件越来越接近
从前,硬件和软件工程师大多生活在自己的世界里。硬件团队设计了芯片,调试了从铸造厂返回的第一批样本,让软件团队测试他们的代码。随着虚拟平台和其他可执行模型变得越来越普遍,软件团队可以在芯片制造之前开始,有时甚…...

亲测:腾讯云轻量应用服务器性能如何?
腾讯云轻量应用服务器性能评测,轻量服务器CPU主频、处理器型号、公网带宽、月流量、Ping值测速、磁盘IO读写及使用限制,轻量应用服务器CPU内存性能和标准型云服务器CVM处于同一水准,所以大家不要担心轻量应用服务器的性能,腾讯云百…...

编程语言,TIOBE 4 月榜单:黑马出现了
TIOBE 4 月榜单已经发布了,一起来看看这个月编程语言排行榜有什么变化吧! C 发展依旧迅猛 在本月榜单中,TOP 20 的变动不大,Python、C、Java 、 C 和C#依然占据前五。甚至排名顺序都和上个月一样没有变动。 同时,Rus…...

基于DSP+FPGA的机载雷达伺服控制系统(二)电源仿真
板级电源分配网络的分析与仿真在硬件电路设计中,电源系统的设计是关键步骤之一,良好的电源系统为电路板 上各种信号的传输提供了保障。本章将研究电源完整性的相关问题,并提出一系列改 进电源质量的措施。 3.1 电源完整性 电源完整性…...