场景
通常我们要修改一个java函数的方法都是修改这个函数的源码文件,然后重新编译生成修改后的字节码文件,如果代码我们自己写的还好,我们可以很方便的修改源码,但是考虑另外一个场景,如果代码不是我们自己写的,比如我们只有一个可执行jar包,但没有这个jar包的源码,然后想要修改这个类中的某个函数的功能,再比如一般而言jar包的开发者在在开发该jar包时肯定会有一些调试日志打印的,但是在发布该jar包的时候一般会通过混淆规则去除这些调试日志,我们在分析这些jar包的某些逻辑的时候可能需要像其开发者一样在运行时打印一些关键参数作为分析的线索。
这2个场景都有一个共同的特点就是只有java的字节码文件,而无其源码但是想要达到修改类/函数的功能,那么能够做到吗?理论上肯定是可以的,因为和任何一门语言一样,java的字节码文件最终执行也是通过指令的形式执行的,只不过在不同的语言中这些指令不一样,比如在java中是jvm 字节码指令,因此只要我们修改这些函数的指令就能够达到修改该函数功能的效果,但是直接从字节码指令层面去进行修改这个学习成本太高,一方面需要我们对jvm字节码指令非常了解,另一方面java的class文件是有着自己的格式要求的,修改指令之后还需要修复偏移以保持正确的字节码格式。这个过程过于繁琐。但是好在已经有这样的框架供我们开发者从API层面去修改java字节码,而无须过多关心底层的字节码指令集,比如ASM,javassit等。ASM功能更强大,本文重点讲讲通过ASM直接修改java字节码文件。
ASM
ASM的官网:http://asm.ow2.org/ , 使用需要从该网站下载依赖的jar包,下载地址:https://forge.ow2.org/projects/asm/ 需要注意的是不同版本的jar包其定义的接口可能是不一样的,但是对功能没影响,本博文讲解基于asm-5.2.jar版本。
就像java字节码是以类为基本单位进行组织一样,ASM操作也是以类作为基础,对于类的操作ASM框架提供了ClassReader,ClassWriter,ClassVisitor三个重要接口,顾名思义ClassReader就是用来获取字节码内容的类,ClassVisitor就是用来访问该字节码内容的类,ClassWriter就是将我们在访问字节码内容过程中获取到的字节码内容行写操作的类。在这3个类中提供了对类进行操作的方法,其中ClassWriter继承自ClassVisitor对象。下面介绍下这几个类及其相关重要函数。
ClassReader顾名思义就是用来获取类原字节码内容的接口,该类构造函数如下:
public ClassReader(byte[] bytes)
其构造函数的参数是一个字节数组,即需要传入字节码的内容,通常构造一个ClassReader的代码如下:
byte[] bytes = Utils.getByte(new File(classPath));//classPath为字节码文件路径,getByte为读取文件内容转为字节数组的函数 ClassReader cReader = new ClassReader(bytes);
该类包含了一个重要的函数accept,用来将ClassWrite对象和ClassReader对象关联起来,即表明该Reader对象接受哪个Writer的写操作,其定义如下:
public void accept(ClassVisitor classVisitor, int opcode)
第一个参数是一个ClassVisitor对象,第二个参数是一个int类型的操作码,通常第一个参数传入我们构造的ClassWriter对象。
ClassVisitor是用来访问类的接口,也是这3个类中最重要的类,我们对于字节码的操作基本上都是通过该类访问类字节码时的回调函数进行处理。先看下该类的构造函数,该类提供了2个重载构造函数,如图所示:
其中一个包含一个int型参数和ClassVisitor对象,一般而言我们自定义的继承该ClassVisitor的Visitor适配器都需要调用下包含2个参数的父类构造器,传入一个ClassWriter对象(ClassWriter是ClassVisitor的子类)。否则对类的操作的ClassVisitor为空,因为可以看到在2个参数的构造器中会把传入的ClassVisitor对象cv赋值给自己的成员cv。如果不调用2个形参的构造函数则成员cv未被赋值。即类似如下代码:
public class RemoveMethodVisitor extends ClassVisitor {
public RemoveMethodVisitor(ClassWriter classWriter) {
super(Opcodes.ASM4, classWriter);//调用父类的2个参数的构造器
// TODO Auto-generated constructor stub
}
}
另外在该类中提供了一系列重要的回调函数。比如访问类时的回调visit,访问函数时的回调visitMethod,访问域时的回调visitField,我们要对哪部分逻辑进行处理就只需要重载该函数,然后在里面实现自己的逻辑即可,比如,如果我们想要获取某个类中的所有函数的名称和签名信息,则我们只需要重载visitMethod函数,在该函数中打印函数名称和签名即可,当ClassVisitor遍历到函数时会自动回调visitMethod函数。在这些重要的回调函数中,visitMethod是最常用的,因为修改类功能本质上就是修改类的函数的代码。看一下该回调函数的定义:
@Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions)
其参数和返回值的意义如下:
- access:函数的访问权限,如public,private,static,native等
- name :函数名称
- desc函数参数和返回类型的描述,如(Ljava/lang/String;)V
- signature:函数签名,一般为null
- exceptions函数抛出的异常数组,一般为null,如果访问的函数通过throw抛出过异常则不为null
- 返回值MethodVisitor:MethodVisitor对象,当我们要修改某个函数时需要定义一个MethodVisitor对象,将该对象进行返回,如果返回null,则表明会将类函数代码抽空。
比如如下示例是将类除了构造函数外的其他函数都清空的示例:
public class RemoveMethodCodeVisitor extends ClassVisitor {
public RemoveMethodCodeVisitor(ClassVisitor classWriter) {
super(Opcodes.ASM4, classWriter);
// TODO Auto-generated constructor stub
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
// TODO Auto-generated method stub
//注意不能加上下面这句,否则虽然会去除函数体部分但是会保留函数定义部分
//cv.visitMethod(access, name, desc, signature, exceptions);
if (name.equals("<init>")) {//对于构造函数使用原本的MethodVisitor对象,也就是不修改构造函数
return super.visitMethod(access, name, desc, signature, exceptions);
}
return null;//返回null则将方法体清空
}
}
ClassWriter继承自ClassVisitor对象,用来将在遍历类过程中做的修改进行写操作保存起来,该类中有一个重要函数toByteArray()用来将前面访问类时的内容转为字节数组,然后就可以用I/O操作将字节数组进行写操作保存起来。该类的构造函数如下:
public ClassWriter(int i)
其构造函数是一个int类型常量,一般而言我们传入ClassWriter.COMPUTE_MAXS即可,表示让系统自动计算栈帧大小和局部变量大小。
使用简单示例
前面讲解了ClassReader,ClassVisitor和ClassWriter的一些关键函数,下面是一个极其简单的使用示例,越是简单越能说明核心的使用步骤。
//通过I/O操作获取字节码.class文件的内容,将其转为字节数组 byte[]bytes = Utils.getByte(new File(classPath)); //利用前面的字节数组创建ClassReader对象 ClassReader cReader = new ClassReader(bytes); //创建ClassWriter对象 ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS); //创建ClassVisitor对象,其构造器参数是前面创建的ClassWriter对象 ClassVisitor removeMethodCode=new RemoveMethodCodeVisitor(writer); //调用ClassReader的accept函数传入创建的ClassVisitor对象将Reader和Visitor关联起来 cReader.accept(removeMethodCode, Opcodes.ASM4); //调用ClassWriter的toByteArray将前面一系列操作后得到的字节码内容转为字节数组 byte[] code = writer.toByteArray(); ////通过I/O操作将字节数组内容保存起来 FileOutputStream fos = new FileOutputStream(outpath); fos.write(code); fos.close();
从上面可以归纳出使用ASM的几个核心步骤:
- 通过I/O操作获取字节码文件内容,用字节数组创建ClassReader对象
- 创建ClassWriter对象,用创建的ClassWriter实例创建ClassVisitor对象
- 调用ClassRreader的accept方法传入ClassVisitor实例,将ClassReader和ClassVisitor关联起来
- 调用ClassWriter实例的toByteArray方法将修改后的字节码内容转为字节数组
- 通过I/O操作将字节数组内容写到新的字节码文件中
其中我们对原字节码的所有操作都是在创建的ClassVisitor类的回调函数中进行处理,如对字段,方法修改等只需要重写其对应的回调函数即可。
修改类示例代码
下面是修改原字节码方法的代码
public class MyClassVisitor extends ClassVisitor {
public MyClassVisitor() {
super(Opcodes.ASM5);
// TODO Auto-generated constructor stub
}
//该方法在ClassVisitor访问类的时候回调
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
// TODO Auto-generated method stub
super.visit(version, access, name, signature,superName, interfaces);
}
//该方法在ClassVisitor访问类中方法时回调
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
// TODO Auto-generated method stub
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
//打印函数相关信息
System.out.println("[method] access:" + access + " name:" + name + " desc:" + desc + " sig:" + signature+
" return Type:"+Type.getReturnType(desc));
if (!name.equals("<init>")){//过滤构造函数
// 返回新的MethodVisitor对方法进行修改
MethodVisitor methodModify = new MethodModify();
return methodModify;
}
return super.visitMethod(access, name, desc, signature, exceptions);
}
}
//下面是对方法进行修改的类,继承自MethodVisitor
public class MethodModify extends MethodVisitor implements Opcodes {
public MethodModify() {
super(Opcodes.ASM4);
// TODO Auto-generated constructor stub
}
//该方法在进入方法执行原方法代码前调用,只会被调用一次
@Override
public void visitCode() {
// TODO Auto-generated method stub
super.visitCode();
addSomeCodeInMehtod();
}
public void addSomeCodeInMehtod(){
//在这里插入ASM字节码指令语句
}
}
从上面可以看到要修改方法(主要是增加新的语句)只需要在我们创建的ClassVisitor的visitMethod回调中创建一个新的MethodVisitor实例,然后返回该实例,对方法的修改是被委托给了MethodVisitor对象来完成的,只需要在该对象的visitCode回调中增加新的ASM字节码指令语句即可。可以看到在设计模式上ASM设计的非常友好,能够让我们很容易学会使用其API。
注意:通过ASM的ClassVisitor的回调函数操作原字节码内容本质都是在原字节码的基础上增加内容,不会删除原函数已经存在的指令内容,如果我们不仅仅希望在原函数中增加代码,如打印参数,而是彻底改写原函数内容,那么必须先清空原函数的指令,然后插入新的指令,但是ASM框架未提供清空原函数的相关API,所以我们可以采用先遍历原字节码中所有函数,将原类中所有函数的名称及其签名保存起来,然后在ClassVisitor的visitMethod回调中return null将原类的所有函数都去除,然后将之前保存的所有函数通过字节码插入,然后就可以在函数中增加新的指令,这样构造出来的函数就和之前函数定义完全一致,但是内容是彻底改写的函数了。
还有另外一种方式就是在自定义的MethodVisitor的visitCode回调中,插入新代码后提前返回,也就是在插入新代码的最后调用retrun指令(调用return指令的时候要注意根据原函数本身的返回值类型调用相应的return指令) 如下是一个简单示例:
public class RemoveMethodVisitor extends ClassVisitor {
public RemoveMethodVisitor(ClassWriter classWriter) {
super(Opcodes.ASM4, classWriter);
// TODO Auto-generated constructor stub
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
// TODO Auto-generated method stub
if (name.equals("<init>") || name.equals("<clinit>") || name.startsWith("access$")) {
return super.visitMethod(access, name, desc, signature, exceptions);
}
// return null;//返回null则将整个方法清空,包括定义和方法体
MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
// 保留方法的定义,清空方法体代码,插入新代码
return new ReplaceWithEmptyBody(mv);
}
static class ReplaceWithEmptyBody extends MethodVisitor {
private final MethodVisitor targetWriter;
ReplaceWithEmptyBody(MethodVisitor writer) {
super(Opcodes.ASM5);
targetWriter = writer;
}
@Override
public void visitCode() {
targetWriter.visitCode();
// 添加新的代码
addSomeCode(targetWriter);
targetWriter.visitInsn(Opcodes.RETURN);//提前返回,要根据原函数的返回值类型调用相应类型的return指令,此处示例简单调用void类型的return指令
}
@Override
public void visitEnd() {
targetWriter.visitEnd();
}
@Override
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
return targetWriter.visitAnnotation(desc, visible);
}
@Override
public void visitParameter(String name, int access) {
targetWriter.visitParameter(name, access);
}
}
public static void addSomeCode(MethodVisitor mv) {
//在此添加字节码指令
}
}
原因是visitCode函数是进入函数后执行原函数字节码前被ASM框架回调,也就是说我们在visitCode回调中增加的代码会被插入到原函数字节码的前面,这样我们提前return就相当于把后面的字节码给废弃掉了,这样用jd-gui或者luyten等反编译工具查看就看不到原函数的字节码对应的代码了。比如原函数内容如下:
然后使用前面的代码在visitCode回调中的addSomeCode中插入一段try-catch块字节码代码,然后用luyten查看,内容如下:
可以看到原函数的内容确实被废除了,只剩下try-catch块的内容,不过这样从效果上虽然达到了使得原函数字节码指令废除的效果,但是实际上这些字节码指令其实还是在函数指令代码中,用javap命令还是可以查看到。如下图是用javap命令查看的内容:
使用插件自动生成ASM字节码
通过前面的示例可以看到通过ASM框架修改类的字节码非常方便,而且ASM框架提供的API非常容易使用,不过大家可能已经意识到ASM的一个不足,就是插入代码是ASM字节码指令语句,当要插入代码的语句非常复杂时,其对应的字节码指令也是极其复杂的,但是对应我们普通开发者而言,是很难直接把java代码用字节码指令改写出来的,好在已经有这样的工具能够帮我们完成这个麻烦的问题。在ASM的官网上提供了一个eclipse plugn工具。该工具能够将我们在eclipse中写的java代码翻译为ASM字节码指令形式。
下载及基本使用可参考官网:http://asm.ow2.org/eclipse/index.html
需要注意的是该插件依赖asm框架jar包,所以除了下载了该插件外还需要下载asm库的jar包,然后将这2个jar包放到eclipse的插件目录下,然后重启eclipse,依次展开
- Window -> Show View -> Other -> Java -> Bytecode
就可以在eclipse底部的面板上看到ByteCode选项卡,如图所示:
点击上图箭头所指示的按钮就可以实现将java代码翻译为ASM字节码指令的功能了,如上图在ByteCode面板上显示的是Eclipse代码编辑区java代码对应的ASM字节码指令代码,然后我们只需要复制这段ASM字节码指令到需要插入该代码的位置即可。




文章评论