( visitParameter )* [ visitAnnotationDefault ]
( visitAnnotation | visitAnnotableParameterCount | visitParameterAnnotation visitTypeAnnotation | visitAttribute )*
[ visitCode
( visitFrame | visit<i>X</i>Insn | visitLabel | visitInsnAnnotation | visitTryCatchBlock | visitTryCatchAnnotation | visitLocalVariable | visitLocalVariableAnnotation | visitLineNumber )*
visitMaxs
visitEnd
AdviceAdapter
的父类是GeneratorAdapter
和LocalVariablesSorter
,在MethodVisitor
类的基础上封装了非常多的便捷方法,同时还为我们做了非常有必要的计算,所以我们应该尽可能的使用AdviceAdapter
来修改字节码。
AdviceAdapter
类实现了一些非常有价值的方法,如:onMethodEnter
(方法进入时回调方法)、onMethodExit
(方法退出时回调方法),如果我们自己实现很容易掉进坑里面,因为这两个方法都是根据条件推算出来的。比如我们如果在构造方法的第一行直接插入了我们自己的字节码就可能会发现程序一运行就会崩溃,因为Java语法中限制我们第一行代码必须是super(xxx)
。
GeneratorAdapter
封装了一些栈指令操作的方法,如loadArgArray
方法可以直接获取方法所有参数数组、invokeStatic
方法可以直接调用类方法、push
方法可压入各种类型的对象等。
比如LocalVariablesSorter
类实现了计算本地变量索引位置的方法,如果要在方法中插入新的局部变量就必须计算变量的索引位置,我们必须先判断是否是非静态方法、是否是long/double
类型的参数(宽类型占两个位),否则计算出的索引位置还是错的。使用AdviceAdapter
可以直接调用mv.newLocal(type)
计算出本地变量存储的位置,为我们省去了许多不必要的麻烦。
读取类/成员变量/方法信息
为了学习ClassVisitor
,我们写一个简单的读取类、成员变量、方法信息的一个示例,需要重写ClassVisitor
类的visit
、visitField
和visitMethod
方法。
ASM读取类信息示例代码:
package com.anbai.sec.bytecode.asm;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.MethodVisitor;
import java.io.IOException;
import java.util.Arrays;
import static org.objectweb.asm.ClassReader.EXPAND_FRAMES;
import static org.objectweb.asm.Opcodes.ASM9;
public class ASMClassVisitorTest {
public static void main(String[] args) {
String className = "com.anbai.sec.bytecode.TestHelloWorld";
try {
final ClassReader cr = new ClassReader(className);
System.out.println(
"解析类名:" + cr.getClassName() + ",父类:" + cr.getSuperName() +
",实现接口:" + Arrays.toString(cr.getInterfaces())
System.out.println("-----------------------------------------------------------------------------");
cr.accept(new ClassVisitor(ASM9) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
System.out.println(
"变量修饰符:" + access + "\t 类名:" + name + "\t 父类名:" + superName +
"\t 实现的接口:" + Arrays.toString(interfaces)
System.out.println("-----------------------------------------------------------------------------");
super.visit(version, access, name, signature, superName, interfaces);
@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
System.out.println(
"变量修饰符:" + access + "\t 变量名称:" + name + "\t 描述符:" + desc + "\t 默认值:" + value
return super.visitField(access, name, desc, signature, value);
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
System.out.println(
"方法修饰符:" + access + "\t 方法名称:" + name + "\t 描述符:" + desc +
"\t 抛出的异常:" + Arrays.toString(exceptions)
return super.visitMethod(access, name, desc, signature, exceptions);
}, EXPAND_FRAMES);
} catch (IOException e) {
e.printStackTrace();
程序执行后输出:
解析类名:com/anbai/sec/bytecode/TestHelloWorld,父类:java/lang/Object,实现接口:[java/io/Serializable]
-----------------------------------------------------------------------------
变量修饰符:131105 类名:com/anbai/sec/bytecode/TestHelloWorld 父类名:java/lang/Object 实现的接口:[java/io/Serializable]
-----------------------------------------------------------------------------
变量修饰符:26 变量名称:serialVersionUID 描述符:J 默认值:-7366591802115333975
变量修饰符:2 变量名称:id 描述符:J 默认值:null
变量修饰符:2 变量名称:username 描述符:Ljava/lang/String; 默认值:null
变量修饰符:2 变量名称:password 描述符:Ljava/lang/String; 默认值:null
方法修饰符:1 方法名称:<init> 描述符:()V 抛出的异常:null
方法修饰符:1 方法名称:hello 描述符:(Ljava/lang/String;)Ljava/lang/String; 抛出的异常:null
方法修饰符:9 方法名称:main 描述符:([Ljava/lang/String;)V 抛出的异常:null
方法修饰符:1 方法名称:getId 描述符:()J 抛出的异常:null
方法修饰符:1 方法名称:setId 描述符:(J)V 抛出的异常:null
方法修饰符:1 方法名称:getUsername 描述符:()Ljava/lang/String; 抛出的异常:null
方法修饰符:1 方法名称:setUsername 描述符:(Ljava/lang/String;)V 抛出的异常:null
方法修饰符:1 方法名称:getPassword 描述符:()Ljava/lang/String; 抛出的异常:null
方法修饰符:1 方法名称:setPassword 描述符:(Ljava/lang/String;)V 抛出的异常:null
方法修饰符:1 方法名称:toString 描述符:()Ljava/lang/String; 抛出的异常:null
通过这个简单的示例,我们可以通过ASM实现遍历一个类的基础信息。
修改类名/方法名称/方法修饰符示例
使用ClassWriter
可以实现类修改功能,使用ASM修改类字节码时如果插入了新的局部变量、字节码,需要重新计算max_stack
和max_locals
,否则会导致修改后的类文件无法通过JVM校验。手动计算max_stack
和max_locals
是一件比较麻烦的事情,ASM为我们提供了内置的自动计算方式,只需在创建ClassWriter
的时候传入COMPUTE_FRAMES
即可:new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
ASM修改类字节码示例代码:
package com.anbai.sec.bytecode.asm;
import org.javaweb.utils.FileUtils;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import java.io.File;
import java.io.IOException;
import static org.objectweb.asm.ClassReader.EXPAND_FRAMES;
import static org.objectweb.asm.ClassWriter.COMPUTE_FRAMES;
import static org.objectweb.asm.Opcodes.*;
public class ASMClassWriterTest {
public static void main(String[] args) {
String className = "com.anbai.sec.bytecode.TestHelloWorld";
final String newClassName = "JavaSecTestHelloWorld";
try {
final ClassReader cr = new ClassReader(className);
final ClassWriter cw = new ClassWriter(cr, COMPUTE_FRAMES);
cr.accept(new ClassVisitor(ASM9, cw) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
super.visit(version, access, newClassName, signature, superName, interfaces);
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (name.equals("hello")) {
access = access & ~ACC_PUBLIC | ACC_PRIVATE;
return super.visitMethod(access, "hi", desc, signature, exceptions);
return super.visitMethod(access, name, desc, signature, exceptions);
}, EXPAND_FRAMES);
File classFilePath = new File(new File(System.getProperty("user.dir"), "javaweb-sec-source/javase/src/main/java/com/anbai/sec/bytecode/asm/"), newClassName + ".class");
byte[] classBytes = cw.toByteArray();
FileUtils.writeByteArrayToFile(classFilePath, classBytes);
} catch (IOException e) {
e.printStackTrace();
修改成功后将会生成一个名为JavaSecTestHelloWorld.class
的新的class文件,反编译JavaSecTestHelloWorld
类会发现该类的hello
方法也已被修改为了hi
,修饰符已被改为private
,如下图:
修改类方法字节码
大多数使用ASM库的目的其实是修改类方法的字节码,在原方法执行的前后动态插入新的Java代码,从而实现类似于AOP的功能。修改类方法字节码的典型应用场景如:APM和RASP;APM需要统计和分析每个类方法的执行时间,而RASP需要在Java底层API方法执行之前插入自身的检测代码,从而实现动态拦截恶意攻击。
假设我们需要修改com.anbai.sec.bytecode.TestHelloWorld
类的hello方法,实现以下两个需求:
在原业务逻辑执行前打印出该方法的参数值;
修改该方法的返回值;
原业务逻辑:
public String hello(String content) {
String str = "Hello:";
return str + content;
修改之后的业务逻辑代码:
public String hello(String content) {
System.out.println(content);
String var2 = "javasec.org";
String str = "Hello:";
String var4 = str + content;
System.out.println(var4);
return var2;
借助ASM我们可以实现类方法的字节码编辑。
修改类方法字节码实现代码:
package com.anbai.sec.bytecode.asm;
import org.javaweb.utils.FileUtils;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.AdviceAdapter;
import java.io.File;
import java.io.IOException;
import static org.objectweb.asm.ClassReader.EXPAND_FRAMES;
import static org.objectweb.asm.Opcodes.ASM9;
public class ASMMethodVisitorTest {
public static void main(String[] args) {
String className = "com.anbai.sec.bytecode.TestHelloWorld";
try {
final ClassReader cr = new ClassReader(className);
final ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);
cr.accept(new ClassVisitor(ASM9, cw) {
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (name.equals("hello")) {
MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions);
return new AdviceAdapter(api, mv, access, name, desc) {
int newArgIndex;
private final Type stringType = Type.getType(String.class);
@Override
protected void onMethodEnter() {
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
newArgIndex = newLocal(stringType);
mv.visitLdcInsn("javasec.org");
storeLocal(newArgIndex, stringType);
@Override
protected void onMethodExit(int opcode) {
dup();
int returnValueIndex = newLocal(stringType);
storeLocal(returnValueIndex, stringType);
mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
mv.visitVarInsn(ALOAD, returnValueIndex);
mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
loadLocal(newArgIndex);
mv.visitInsn(ARETURN);
return super.visitMethod(access, name, desc, signature, exceptions);
}, EXPAND_FRAMES);
File classFilePath = new File(new File(System.getProperty("user.dir"), "javaweb-sec-source/javase/src/main/java/com/anbai/sec/bytecode/"), "TestHelloWorld.class");
byte[] classBytes = cw.toByteArray();
FileUtils.writeByteArrayToFile(classFilePath, classBytes);
} catch (IOException e) {
e.printStackTrace();
程序执行后会在com.anbai.sec.bytecode
包下创建一个TestHelloWorld.class
文件:
命令行运行TestHelloWorld
类,可以看到程序执行的逻辑已经被成功修改,输出结果如下:
动态创建Java类二进制
在某些业务场景下我们需要动态一个类来实现一些业务,这个时候就可以使用ClassWriter
来动态创建出一个Java类的二进制文件,然后通过自定义的类加载器就可以将我们动态生成的类加载到JVM中。假设我们需要生成一个TestASMHelloWorld
类,代码如下:
示例TestASMHelloWorld类:
package com.anbai.sec.classloader;
public class TestASMHelloWorld {
public static String hello() {
return "Hello World~";
使用ClassWriter生成类字节码示例:
package com.anbai.sec.bytecode.asm;
import org.javaweb.utils.HexUtils;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
public class TestASMHelloWorldDump implements Opcodes {
private static final String CLASS_NAME = "com.anbai.sec.classloader.TestASMHelloWorld";
private static final String CLASS_NAME_ASM = "com/anbai/sec/classloader/TestASMHelloWorld";
public static byte[] dump() throws Exception {
ClassWriter cw = new ClassWriter(0);
MethodVisitor mv;
cw.visit(V1_7, ACC_PUBLIC + ACC_SUPER, CLASS_NAME_ASM, null, "java/lang/Object", null);
cw.visitSource("TestHelloWorld.java", null);
mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(5, l0);
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
mv.visitInsn(RETURN);
Label l1 = new Label();
mv.visitLabel(l1);
mv.visitLocalVariable("this", "L" + CLASS_NAME_ASM + ";", null, l0, l1, 0);
mv.visitMaxs(1, 1);
mv.visitEnd();
mv = cw.visitMethod(ACC_PUBLIC + ACC_STATIC, "hello", "()Ljava/lang/String;", null, null);
mv.visitCode();
Label l0 = new Label();
mv.visitLabel(l0);
mv.visitLineNumber(8, l0);
mv.visitLdcInsn("Hello World~");
mv.visitInsn(ARETURN);
mv.visitMaxs(1, 0);
mv.visitEnd();
cw.visitEnd();
return cw.toByteArray();
public static void main(String[] args) throws Exception {
final byte[] classBytes = dump();
System.out.println(new String(HexUtils.hexDump(classBytes)));
ClassLoader classLoader = new ClassLoader(TestASMHelloWorldDump.class.getClassLoader()) {
@Override
protected Class<?> findClass(String name) {
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
return defineClass(CLASS_NAME, classBytes, 0, classBytes.length);
System.out.println("-----------------------------------------------------------------------------");
System.out.println("hello方法执行结果:" + classLoader.loadClass(CLASS_NAME).getMethod("hello").invoke(null));
程序执行结果如下:
0000019F CA FE BA BE 00 00 00 33 00 14 01 00 2B 63 6F 6D .......3....+com
000001AF 2F 61 6E 62 61 69 2F 73 65 63 2F 63 6C 61 73 73 /anbai/sec/class
000001BF 6C 6F 61 64 65 72 2F 54 65 73 74 41 53 4D 48 65 loader/TestASMHe
000001CF 6C 6C 6F 57 6F 72 6C 64 07 00 01 01 00 10 6A 61 lloWorld......ja
000001DF 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 07 00 va/lang/Object..
000001EF 03 01 00 13 54 65 73 74 48 65 6C 6C 6F 57 6F 72 ....TestHelloWor
000001FF 6C 64 2E 6A 61 76 61 01 00 06 3C 69 6E 69 74 3E ld.java...<init>
0000020F 01 00 03 28 29 56 0C 00 06 00 07 0A 00 04 00 08 ...()V..........
0000021F 01 00 04 74 68 69 73 01 00 2D 4C 63 6F 6D 2F 61 ...this..-Lcom/a
0000022F 6E 62 61 69 2F 73 65 63 2F 63 6C 61 73 73 6C 6F nbai/sec/classlo
0000023F 61 64 65 72 2F 54 65 73 74 41 53 4D 48 65 6C 6C ader/TestASMHell
0000024F 6F 57 6F 72 6C 64 3B 01 00 05 68 65 6C 6C 6F 01 oWorld;...hello.
0000025F 00 14 28 29 4C 6A 61 76 61 2F 6C 61 6E 67 2F 53 ..()Ljava/lang/S
0000026F 74 72 69 6E 67 3B 01 00 0C 48 65 6C 6C 6F 20 57 tring;...Hello W
0000027F 6F 72 6C 64 7E 08 00 0E 01 00 04 43 6F 64 65 01 orld~......Code.
0000028F 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C ..LineNumberTabl
0000029F 65 01 00 12 4C 6F 63 61 6C 56 61 72 69 61 62 6C e...LocalVariabl
000002AF 65 54 61 62 6C 65 01 00 0A 53 6F 75 72 63 65 46 eTable...SourceF
000002BF 69 6C 65 00 21 00 02 00 04 00 00 00 00 00 02 00 ile.!...........
000002CF 01 00 06 00 07 00 01 00 10 00 00 00 2F 00 01 00 ............/...
000002DF 01 00 00 00 05 2A B7 00 09 B1 00 00 00 02 00 11 .....*..........
000002EF 00 00 00 06 00 01 00 00 00 05 00 12 00 00 00 0C ................
000002FF 00 01 00 00 00 05 00 0A 00 0B 00 00 00 09 00 0C ................
0000030F 00 0D 00 01 00 10 00 00 00 1B 00 01 00 00 00 00 ................
0000031F 00 03 12 0F B0 00 00 00 01 00 11 00 00 00 06 00 ................
0000032F 01 00 00 00 08 00 01 00 13 00 00 00 02 00 05 ...............
-----------------------------------------------------------------------------
hello方法执行结果:Hello World~
程序执行后会在TestASMHelloWorldDump
类同级的包下生成一个TestASMHelloWorld
类,如下图:
IDEA/Eclipse插件
初学ASM,读写ASM字节码对我们来说是非常困难的,但是我们可以借助开发工具的ASM插件,可以极大程度的帮助我们学习ASM。
IDEA - ASM Bytecode Outline
在IDEA中插件中心搜索:ASM Bytecode Outline
,就可以找到ASM字节码插件,如下图:
安装完ASM Bytecode Outline
后选择任意Java类,右键菜单中会出现Show Bytecode outline
选项,点击之后就可以看到该类对应的ASM和Bytecode代码,如下图:
Eclipse - Bytecode Outline
Eclipse同IDEA,在插件中心搜索bytecode就可以找到Bytecode Outline
插件,值得一提的是Eclipse的Bytecode Outline
相比IDEA
而言更加的方便,打开任意Java类会在Bytecode
窗体中生成对应的ASM代码,点击任意行代码还能自动对应到高亮对应的ASM代码。
安装Bytecode Outline
如果您使用的Eclipse版本相对较低(低版本的Eclipse自带了ASM依赖,如Eclipse Photon Release (4.8.0)
)可直接在插件中心安装Bytecode Outline
,否则需要先安装ASM依赖,点击Help
->Eclipse Marketplace...
,如下图:
然后搜索bytecode
,找到Bytecode Outline
,如下图:
点击Instal
->I accept the terms of the license agreement
->Finish
:
提示安全警告,直接点击Install anyway
:
安装完成后重启Eclipse即可。
安装Eclipse ASM依赖库
如果您是使用的Eclipse版本较新可能会无法安装,提示:Cannot complete the install because one or more required items could not be...
,这是因为新版本的Eclipse不自带ASM依赖库,需要我们先安装好ASM依赖然后才能安装Bytecode Outline
插件。
点击Help
->Install New Software...
然后在https://download.eclipse.org/tools/orbit/downloads/drops/选择对应的Eclipse版本:
复制仓库地址:
然后在Work with
输入框中输入:https://download.eclipse.org/tools/orbit/downloads/drops/I20200904215518/repository
,点击Add..
,填入仓库名字,如下图:
选择All Bundles
或者找到ASM
相关依赖,并按照提示完成依赖安装,如下图:
Bytecode Outline配置
安装好Bytecode Outline
插件以后默认没有Bytecode
窗体,需要再视图中添加Bytecode
,点击Window
->Show View
->Other
,如下图:
然后在弹出的视图窗体中输入bytecode
后点击open
,如下图:
随便写一个测试类,在Bytecode
窗体中可以看到对应的Bytecode
,如果需要看ASM代码,点击右侧菜单的ASM图标
即可,如下图:
如果想对照查看Java和ASM代码,只需点击对应的Java代码就会自动高亮ASM部分的代码,如下图:
我们可以借助Bytecode Outline
插件学习ASM,也可以直接使用Bytecode Outline
生成的ASM代码来实现字节码编辑。