java的asm若干实践
一、前置知识、工具、代码库等
- jdk里的javap,用来反汇编class文件查看生成的字节码
- org.objectweb.asm 用来解析、修改、保存字节码的代码库
- fernflower.jar 用来批量将class文件反编译成java代码(当然idea也可以手动单个反编译)
- java的Instrumentation,可以编写代码使用javaagent代理执行其它java文件,在代码里使用上面的asm库动态修改代码。也可以直接将所有字节保存,并加载其它class文件。
二、Instrumentation
在代理jar包中MANIFEST.MF使用Premain-Class指定代理类,代理类需要有premain方法,此方法会在被代理的jar包的main执行前执行:
1 | public static void premain(String var0, Instrumentation inst) { |
JVM在加载每一个类时,都会调用MyTransformer的transform方法对字节码进行处理:
1 | public final byte[] transform(ClassLoader l, String className, Class<?> c,ProtectionDomain pd, byte[] b) throws IllegalClassFormatException { |
- 在transform函数也可保存修改前或修改后的字节为class文件,只需要直接将b保存成class文件即可。
- 如果transform里抛出异常,程序也能正常执行,只不过类没有发生任何变化(因为没有返回修改过的字节码)。
- 如果修改过的字节码中,有错误,比如在生成方法的局部变量时,生成了如下格式
1 | LocalVariableTable: |
其中address对应的类型为desc,显然是非法的,则启动程序时会报异常:
1 | java.lang.NoClassDefFoundError: net/minecraft/client/network/NetHandlerLoginClient |
三、asm实践
使用asm修改字节码的示例有很多,这里就不再详细列举,这里仅说几个我填过的坑。在破解forge版本minecraft游戏的过程中,有些asm代码在老版本执行正常,但是在v1.12版本就导致游戏崩溃。
在transform里将修改过的代码保存成class文件并且通过idea反汇编成java文件,对比1.11和1.12两个版本生成的java代码是一样的,在各种调整jvm参数、java版本、asm库版本之后,最终找到原因和解决办法。
错误1:Caused by: java.lang.VerifyError: Expecting a stackmap frame at branch target xxx
此bug是因为原有代码在插入了一些if(xxx)等分支流程,假设代码是:
1 | MethodNode method = xxx;// 从原字节码里使用ClassReader得到方法对象。 |
以上代码在很多mc版本上都正常运行,只有v1.12版本才会报标题中的错误。是因为v1.12的代码在执行时对生成字节码的StackMapTable区域做了检测。在未修改之前的原方法中可能StackMapTable是这样的:
1 | StackMapTable: number_of_entries = 6 |
以上方法修改后,生成的字节码是这样的:
1 | StackMapTable: number_of_entries = 6 |
其实这个 frame_type 的顺序应该和代码块的顺序一致(至于StackMapTable对应代码结构的关系可以见(http://hllvm.group.iteye.com/group/topic/26545中R大的解释及其它资料)。
知道这个特性后,尝试了给java添加了某博客提到的 -noverify 和 -XX:-UseSplitVerifier 这两个jvm参数来禁用字节码验证,然而不知道为什么并没有卵用。
最后使用asm在代码合适的位置补上了一个frame解决问题。
错误2:java.lang.VerifyError: Bad local variable type
hook代码增加了局部变量,需要手动设置LocalVariableTable,通过 methodnode.visitLocalVariable
来增加变量,注意第三个参数signature如果不是generic types,则不需要设置。例如:
1 | /** |