what

ASM是多用途的字节码处理和分析框架,它可以用来修改已有的类,或者直接以二进制的方式动态生成类,相比于其他的字节码处理框架,比如Javassist、AspectJ,它拥有更好的性能。

how

使用ASM处理字节码,可以使用一些插件,比如ASM Bytecode Viewer,但是最好还是要对字节码常见指令有一些认识。下面是一个java类源代码

1
2
3
4
5
public class LoggerHelper {
public static void log(){
Log.e("sakurajiang","this is insert log by ASM");
}
}

下面是通过ASM Bytecode Viewer生成的ASM代码中关于log方法的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "log", "()V", null, null);
methodVisitor.visitCode();
Label label0 = new Label();
methodVisitor.visitLabel(label0);
methodVisitor.visitLineNumber(10, label0);
methodVisitor.visitLdcInsn("sakurajiang");
methodVisitor.visitLdcInsn("this is insert log by ASM");
methodVisitor.visitMethodInsn(INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
methodVisitor.visitInsn(POP);
Label label1 = new Label();
methodVisitor.visitLabel(label1);
methodVisitor.visitLineNumber(11, label1);
methodVisitor.visitInsn(RETURN);
methodVisitor.visitMaxs(2, 0);
methodVisitor.visitEnd();

下面的代码看着可能会有一些迷惑,这些都是ASM的一些东西。

ClassReader:该类的作用是解析class文件,并且将解析的数据传给ClassVisitor,使用了“访问者模式”,这里的“访问者模式”也比较特殊。稍后会分析。

ClassVisitor

1
2
3
4
5
6
7
8
9
/**
* A visitor to visit a Java class. The methods of this class must be called in the following order:
* {@code visit} [ {@code visitSource} ] [ {@code visitModule} ][ {@code visitNestHost} ][ {@code
* visitOuterClass} ] ( {@code visitAnnotation} | {@code visitTypeAnnotation} | {@code
* visitAttribute} )* ( {@code visitNestMember} | {@code visitInnerClass} | {@code visitField} |
* {@code visitMethod} )* {@code visitEnd}.
*
* @author Eric Bruneton
*/

直接看注释,很明显这个类的作用是负责访问ClassReader解析的class文件,需要注意的是它有两个构造方法,一个是public ClassVisitor(final int api),还有一个是public ClassVisitor(final int api, final ClassVisitor classVisitor),第二个构造函数表明,ClassVisitor自身可以形成一条链路,这样设计的原因就是为了可以分层处理class文件,让结构更加清晰。

ClassWriter

1
2
3
4
5
6
7
8
9
/**
* A {@link ClassVisitor} that generates a corresponding ClassFile structure, as defined in the Java
* Virtual Machine Specification (JVMS). It can be used alone, to generate a Java class "from
* scratch", or with one or more {@link ClassReader} and adapter {@link ClassVisitor} to generate a
* modified class from one or more existing Java classes.
*
* @see <a href="https://docs.oracle.com/javase/specs/jvms/se9/html/jvms-4.html">JVMS 4</a>
* @author Eric Bruneton
*/

从注释可以看出,ClassWriter继承自ClassVisitor,之前有说过,ClassVisitor会形成一条链路,但是所有的链路到最后肯定会有终点,而这个终点就是ClassWriter,这一点从ClassWriter也可以看出来,它并不接收任何ClassVisitor类。同时,可以直接通过ClassWriter类直接生成Java类,当然这个类是全新的,我们在使用的时候更多是搭配ClassReaderClassVisitor类去修改已有的类。

下面直接以常见的例子开始:

首先定义原始类

1
2
3
4
5
public class OriginClass {
public int add(int a,int b){
return a+b;
}
}
在类中插入变量

我们在类中插入一个int类型的变量,名字是sakurajiang。我们首先通过ASM Bytecode Viewer这个插件将OriginClass的ASM格式的代码编出来。即在编译后的OriginClass.class文件右键,点击ASM Bytecode Viewer就可以,然后再在类中增加int sakurajiang = 666;,编译后再点击ASM Bytecode Viewer,而后选择diff,就可以看到插入int sakurajiang = 666后代码的变化。本身不难,为了防止重复插入变量,需要增加一个判断条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
object InsertVariable2Class {
var isAlreadyInserted = false
fun changeByteCodeByASM(classReader: ClassReader,classWriter: ClassWriter):ByteArray?{
if("com/sakurajiang/asmsample/OriginClass"==classReader.className) {
val insertVariableClassVisitor = InsertVariableClassVisitor(Opcodes.ASM7, classWriter)
classReader.accept(insertVariableClassVisitor, ClassReader.EXPAND_FRAMES)
val result = classWriter.toByteArray()
DebugUtils.copy2Computer(classReader.className, result, "TestASM")
return result
}
return null
}

class InsertVariableClassVisitor(api:Int,val classWriter: ClassWriter) : ClassVisitor(api,classWriter){
var className:String? = null
override fun visit(
version: Int,
access: Int,
name: String?,
signature: String?,
superName: String?,
interfaces: Array<out String>?
) {
className = name
super.visit(version, access, name, signature, superName, interfaces)
}
override fun visitField(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
value: Any?
): FieldVisitor {
if(ConstantVariable.VARIABLE_INSERTED==name){
isAlreadyInserted = true
}
return super.visitField(access, name, descriptor, signature, value)
}

override fun visitMethod(
access: Int,
name: String?,
descriptor: String?,
signature: String?,
exceptions: Array<out String>?
): MethodVisitor {
val methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions)
if("<init>"==name && "()V"==descriptor){
return InitVariableAdviceAdapter(api,methodVisitor,access,name,descriptor,className)
}
return methodVisitor
}

override fun visitEnd() {
if(!isAlreadyInserted){
insertVariable(classWriter)
}
}
}

class InitVariableAdviceAdapter (
api: Int, val methodVisitor: MethodVisitor,
access: Int, name: String?, desc: String?,val classFullName: String?
) : AdviceAdapter(api, methodVisitor, access, name, desc){
override fun onMethodExit(opcode: Int) {
methodVisitor.visitVarInsn(Opcodes.ALOAD, 0)
methodVisitor.visitIntInsn(Opcodes.SIPUSH, 666)
methodVisitor.visitFieldInsn(
Opcodes.PUTFIELD,
classFullName,
"sakurajiang",
"I"
)
}
}

fun insertVariable(classWriter: ClassWriter){
val fieldVisitor = classWriter.visitField(0, "sakurajiang", "I", null, null);
fieldVisitor.visitEnd();
}
}
在类中新增方法

在类中新增方法:

1
2
3
public void insertMethod(String value){
System.out.println("value ="+value);
}

新增方法是在visitend中增加。核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
fun insertMethod(classWriter: ClassWriter){
val methodVisitor =
classWriter.visitMethod(ACC_PUBLIC, "insertMethod", "(Ljava/lang/String;)V", null, null)
methodVisitor.visitCode()
val label0 = Label()
methodVisitor.visitLabel(label0)
methodVisitor.visitLineNumber(12, label0)
methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;")
methodVisitor.visitTypeInsn(NEW, "java/lang/StringBuilder")
methodVisitor.visitInsn(DUP)
methodVisitor.visitMethodInsn(
INVOKESPECIAL,
"java/lang/StringBuilder",
"<init>",
"()V",
false
)
methodVisitor.visitLdcInsn("value =")
methodVisitor.visitMethodInsn(
INVOKEVIRTUAL,
"java/lang/StringBuilder",
"append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;",
false
)
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitMethodInsn(
INVOKEVIRTUAL,
"java/lang/StringBuilder",
"append",
"(Ljava/lang/String;)Ljava/lang/StringBuilder;",
false
)
methodVisitor.visitMethodInsn(
INVOKEVIRTUAL,
"java/lang/StringBuilder",
"toString",
"()Ljava/lang/String;",
false
)
methodVisitor.visitMethodInsn(
INVOKEVIRTUAL,
"java/io/PrintStream",
"println",
"(Ljava/lang/String;)V",
false
)
val label1 = Label()
methodVisitor.visitLabel(label1)
methodVisitor.visitLineNumber(13, label1)
methodVisitor.visitInsn(RETURN)
val label2 = Label()
methodVisitor.visitLabel(label2)
methodVisitor.visitLocalVariable(
"this",
"Lcom/sakurajiang/asmsample/OriginClass;",
null,
label0,
label2,
0
)
methodVisitor.visitLocalVariable("value", "Ljava/lang/String;", null, label0, label2, 1)
methodVisitor.visitMaxs(3, 2)
methodVisitor.visitEnd()
}
在方法最开始插入字节码

接下来在方法最开始插入调用其他方法的代码,最常见的就是打印日志。以下是打印日志的方法

1
2
3
4
5
6
7
8
9
public class LoggerHelper {
public static void log(boolean b){
if(b) {
System.out.println("print to console");
}else {
System.out.println("write 2 file");
}
}
}

还是跟之前一样的步骤生成ASM的代码,这里就不赘述了。核心代码:

1
2
3
4
5
6
7
8
methodVisitor.visitInsn(Opcodes.ICONST_1)
methodVisitor.visitMethodInsn(
Opcodes.INVOKESTATIC,
"com/sakurajiang/asmsample/LoggerHelper",
"log",
"(Z)V",
false
)
在方法最后插入字节码

和在方法前面插入代码类似,核心代码都是一样的,区别就是需要在onMethodExit中插入代码。就不赘述了。

验证

如何验证最后的结果呢?我们可以将字节码输出到文件中,而后通过该工具解析成java代码。上述代码都放在github

important

对于COMPUTE_MAXS的选项,以下是官网的解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* A flag to automatically compute the maximum stack size and the maximum number of local
* variables of methods. If this flag is set, then the arguments of the {@link
* MethodVisitor#visitMaxs} method of the {@link MethodVisitor} returned by the {@link
* #visitMethod} method will be ignored, and computed automatically from the signature and the
* bytecode of each method.
*
* <p><b>Note:</b> for classes whose version is {@link Opcodes#V1_7} of more, this option requires
* valid stack map frames. The maximum stack size is then computed from these frames, and from the
* bytecode instructions in between. If stack map frames are not present or must be recomputed,
* used {@link #COMPUTE_FRAMES} instead.
*
* @see #ClassWriter(int)
*/

即用于自动计算方法的最大堆栈大小和最大局部变量数。如果设置了该参数,visitMaxs参数将被忽略,但是这个方法还是要被调用。但是经过实践,发现不调用visitMaxs对于LocalVariableTable和StackMapTable都没有影响。

对于COMPUTE_FRAMES的选项,以下是官网的解释

1
2
3
4
5
6
7
8
9
/**
* A flag to automatically compute the stack map frames of methods from scratch. If this flag is
* set, then the calls to the {@link MethodVisitor#visitFrame} method are ignored, and the stack
* map frames are recomputed from the methods bytecode. The arguments of the {@link
* MethodVisitor#visitMaxs} method are also ignored and recomputed from the bytecode. In other
* words, {@link #COMPUTE_FRAMES} implies {@link #COMPUTE_MAXS}.
*
* @see #ClassWriter(int)
*/

即不需要调用visitFrame就可以生成StackMapTable,当然visitMaxs还是需要调用,经过实践,发现如果开启了这个模式,不调用visitFrame的同时,也不调用visitMaxs,会导致无法生成StackMapTable。

但是经过实践,发现一个很有意思的事,在jdk1.7之后,即使使用没有StackMapTable属性以及LocalVariableTable的类,在运行的时候也不会报错。表示迷惑

但是为了保险起见,还是按照官网文档所说,即设置COMPUTE_MAXS的时候,visitLocalVariable和visitMaxs以及visitFrame都需要调用。

而设置COMPUTE_FRAMES的时候,则可以不调用visitFrame,其他的还是要调用。

需要注意如果继承了AdviceAdapter,则会默认调用visitLocalVariable以及visitMaxs。