在java世界里,字节码改写 + 反射可以让你变成"上帝",你可以完成任何你想做的事情,而字节码改写中asm是当之无愧的老大哥,对字节码认识不深的小伙伴可以看看我这篇文章
本文的目的是现有互联网上asm的资料不够体系和细致,其和传统java编程也非常的不一样,使用时有很多需要注意的地方,比如栈的平衡以及如何分析栈内操作数的状态。
接下来以我最近对随机数方法的实际字节码改写需求为例带大家熟悉整个字节码改写过程.
先描述下我的技术需求
拦截所有生成随机数的方法,记录入参、调用类名、调用方法名、当前行号、方法内行号和出参
-
桥接(代理)方法
在字节码改写中非常重要的一个部分就是桥接(代理)方法,原来调用A方法,当你想做一些事情那么你可能需要将原有的调用修改为调用桥接方法,然后在桥接方法中实现你的处理逻辑
javapublic Object method() { // 业务处理逻辑 invokeMethod(params); // 业务处理逻辑 }
javapublic Object method() { // 业务处理逻辑 invokeBrigeMethod(params...); // 业务处理逻辑 }
桥接方法是否是必须的呢?一般来说如果你的处理逻辑比较简单也是可以不需要桥接方法的,直接将相应的调用指令替换成处理指令,但一般来说如果处理没那么简单使用桥接方法可以使编程更简单,而且很多时候还有一些隐含的好处,比如可以维持原有业务代码的行号避免干扰业务排查问题,对业务类的字节码变更较小,可复用性更高等等。
以上面的需求为例随机数的桥接方法被设计成这样
java// 一般来说桥接方法会设计成静态方法,这样在字节码修改时调用更加方便。 // 第1个参数是Random实例,保证随机数结果依旧从原有的Random实例生成 // 第2个参数是一个数组,因为随机数方法入参的个数是不一致,这里统一使用数组接收,这样就不用每个方法都对应一个桥接方法了 // 第3个参数标识当前是哪个随机数方法,用来生成对应的结果 // 第3个参数又是一个数组,里面是当前随机数方法的一些身份信息,这里主要是考虑到后续可能会新增新的参数,为了避免签名的修改这里使用数组 public static Object invoke(Random random, Object[] request, RandomMethod randomMethod, Object[] randomIdentity);
-
字节码改写
标准使用姿势
java// classFileBuffer为原始字节码byte数组 ClassReader classReader = new ClassReader(classFileBuffer); ClassWriter writer = new EasyClassWriter(COMPUTE_MAXS | COMPUTE_FRAMES, loader); ClassVisitor visitor = new InvokeRandomTransformClassVisitor(); classReader.accept(visitor, EXPAND_FRAMES);
核心逻辑在
InvokeRandomTransformClassVisitor
中javapublic class InvokeRandomTransformClassVisitor { @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { // 跳过特殊方法、Object的内部方法、抽象方法、native方法 return new InvokeRandomTransformMethodVisitor(api, visitor, access, name, desc); } }
在做一系列的过滤之后引出
InvokeRandomTransformMethodVisitor
javaprivate class InvokeRandomTransformMethodVisitor extends AdviceAdapter { private final String methodName; private int methodStartLineNumber = -1; private int currentLineNumber = -1; // 记录方法开始行数,用来计算方法内行数 @Override public void visitLineNumber(int line, Label start) { if (this.methodStartLineNumber == -1) { this.methodStartLineNumber = line; } this.currentLineNumber = line; super.visitLineNumber(line, start); } @Override public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) { if (INVOKEVIRTUAL == opcode) { if (根据owner、name和descriptor判断是否为随机数方法) { // 将Random方法的入参合并成一个数组压入操作数栈 // 压入随机方法名 // 创建一个大小为4的数组填入身份标识 // 调用桥接方法 invokeStatic(TYPE, INVOKE_METHOD); // 提取响应类型描述符做相应拆箱操作保证留在栈上的结果类型一致 AsmClassUtil.unbox(this, Type.getReturnType(descriptor)); return; } } super.visitMethodInsn(opcode, owner, name, descriptor, isInterface); } } }
-
首先判断是否是随机数调用,通过指令和调用类、方法及其描述符来做判断,这里先判断是否是
*INVOKEVIRTUAL
,这会过滤掉大多数无关指令,然后再根据*调用类、方法和描述符判断是否是随机数方法,一般来说先判断指令可以筛选掉很多无关的信息性能会更好。 -
接下来将Random方法的入参合并成一个数组压入操作数栈,这个是整个改写中最难的部分
javapublic static void visitArgumentsAsArray(MethodVisitor methodVisitor, String descriptor) { // 解析方法描述符以获得参数类型 // Parse the method descriptor to get the argument type Type[] argumentTypes = Type.getArgumentTypes(descriptor); int argumentCount = argumentTypes.length; // 创建一个指定大小的数组并压入栈 // Create an array of a specified size and push it onto the stack if (argumentCount <= Byte.MAX_VALUE) { methodVisitor.visitIntInsn(BIPUSH, argumentCount); } else { methodVisitor.visitIntInsn(SIPUSH, argumentCount); } methodVisitor.visitTypeInsn(ANEWARRAY, OBJECT_JVM_CLASS_NAME); // 倒序遍历参数类型,将每个参数装箱并加入数组 // Traverse the argument type in reverse order, box each argument and add it to the array for (int i = argumentCount - 1; i >= 0; i--) { Type type = argumentTypes[i]; if (LONG_TYPE.equals(type) || DOUBLE_TYPE.equals(type)) { methodVisitor.visitInsn(DUP_X2); } else { methodVisitor.visitInsn(DUP_X1); } swap(methodVisitor, type); // 指定数组索引 // Specify the array index if (i <= Byte.MAX_VALUE) { methodVisitor.visitIntInsn(BIPUSH, i); } else { methodVisitor.visitIntInsn(SIPUSH, i); } swap(methodVisitor, type); box(methodVisitor, type); methodVisitor.visitInsn(AASTORE); } } public static void swap(MethodVisitor methodVisitor, Type type) { if (LONG_TYPE.equals(type) || DOUBLE_TYPE.equals(type)) { methodVisitor.visitInsn(DUP_X2); methodVisitor.visitInsn(POP); } else { methodVisitor.visitInsn(SWAP); } }
首先根据入参个数创建一个Object数组,数组的长度应该是原方法的参数个数,这里还有一个细节就是
*BIPUSH*
用于将一个字节(8位)的常量推送到操作数栈上,它的常量范围是从 -128 到 127,但理论上一个方法的入参最多可以可以有256个(随机数参数最多是3个),超过127时应该使用*SIPUSH
,对于当前场景BIPUSH
就够用了但为了后续可能会用于其它方法所以这里做了这个判断。*然后是 倒序遍历参数类型,将每个参数装箱并加入数组,和传统编程不一样的是大家一定要理解栈的特质------先入先出,所以此时留在栈顶的是原方法的最后一个参数,所以为了将参数按照原有顺序打包成数组应该是倒序地将参数存入数组,上面的字节码改写可能会让人眼花缭乱,我将会描述每个指令执行之后栈的状态大家会有更深刻的体会
以最常用的
public int nextInt(int bound)
为例当执行到nextInt的
*INVOKEVIRTUAL
指令时栈的状态是这样的(可以看到栈顶是一个int
值 )*在执行
*ANEWARRAY
后会新增一个数组(在栈顶)变成了这样*对于
int
*由于它只占用一个Slot执行DUP_X1
让栈变成下面这样ps: 这里解释一下为什么这么做,因为后续将元素放入数组是要消耗三个操作数包括array本身的,所以这里使用
DUP_X1
对数组做一个备份使用swap*因为
AASTORE
对操作数的顺序要求是数组,下标,元素使用
*BIPUSH
或SIPUSH
设置下标(从后向前),这个因为只有一个参数所以是0*使用swap调整操作数顺序
使用
*AASTORE
设置数组,这会消耗最近的三个操作数*这样就将桥接方法的前两个参数准备好了,这里面还有一些细节:
- 第一个是
*box*(methodVisitor, type);
,因为在java中数组只能存放引用类型无法存放基本类型,所以对于int
这种需要装箱成包装类型,比如对于int
可以调用Integer.valueOf()
javamethodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf", "(I)Ljava/lang/Integer;", false);
- 第二个细节是对于
int
和*reference
类型要交换它们只需要使用SWAP
指令即可,但如果入参是*long
/double
这种2个Slot的类型则需要使用DUP_X2
+POP
来完成 - 第三个细节是 多个参数第一次循环结束时栈的状态为
*[..., random, 操作数, array]
,按照上述的处理继续把它们放入数组即可,保证最后留在栈顶的是array
*
- 第一个是
-
然后是压入
RandomMethod
,这是一个枚举值 所以使用*GETSTATIC
即可*javavisitFieldInsn(GETSTATIC, "RandomMethod的JVM描述符比如xxx/xxx/RandomMethod", randomMethod.name(), "RandomMethod的JVM返回描述符比如Lxxx/xxx/RandomMethod;");
-
接下来是创建一个大小为4的数组填入身份标识,这个部分要简单一点
javavisitLdcInsn(4); visitTypeInsn(ANEWARRAY, "java/lang/Object"); // 填入类名 visitInsn(DUP); visitLdcInsn(0); visitLdcInsn(className); visitInsn(AASTORE); // 填入方法名 visitInsn(DUP); visitLdcInsn(1); visitLdcInsn(methodName); visitInsn(AASTORE); // 填入行号 visitInsn(DUP); visitLdcInsn(2); visitLdcInsn(currentLineNumber); // 基本类型int转换为包装类型Integer,因为数组只能存放包装类型 AsmClassUtil.box(this, INT_TYPE); visitInsn(AASTORE); // 填入方法内行号 (方法的第几行) visitInsn(DUP); visitLdcInsn(3); visitLdcInsn(currentLineNumber - methodStartLineNumber); // 基本类型int转换为包装类型Integer,因为数组只能存放包装类型 AsmClassUtil.box(this, INT_TYPE); visitInsn(AASTORE);
值得注意的还是基本类型要转换为包装类型,同时除了传统的行号以外,这里面额外记录了方法内行号(方法的第几行),作为一个相对坐标它的鲁棒性相比于文件行号要好很多。
-
最后调用桥接方法并做相应拆箱操作保证留在栈上的结果类型一致,基本类型和包装类型的拆装箱也是我们作字节码改写时要时刻注意的地方
-
-
代码验证
由于字节码改写的复杂性以及无法debug的特殊性,验证测试变成了重中之重,毕竟很难有人能一次性就完成考虑各种场景的代码还没有问题,下面就是一个典型的验证测试
javapublic class InvokeRandomTransformClassVisitorTest extends ClassLoader { public Class<?> loadClassFromBytes(String className, byte[] classBytes) { return defineClass(className, classBytes, 0, classBytes.length); } public static void main(String[] args) throws Exception { String classPath = "原始class文件路径"; String outputClassPath = "改写后class文件保存路径"; executeByteCodeRewrite(classPath, outputClassPath); } private static void executeByteCodeRewrite(String classpath, String outputClassPath) throws Exception { // 从class文件中读取字节数组 byte[] classFileBuffer = Files.readAllBytes(Paths.get(classpath)); byte[] modifiedClassBytes = 调用InvokeRandomTransformClassVisitor做字节码改写; // 将修改后的字节数组写入到指定路径的文件中 try (FileOutputStream fos = new FileOutputStream(outputClassPath)) { fos.write(modifiedClassBytes); } System.out.println("Class transformed and written to file: " + outputClassPath); // 加载修改后的类并使用反射调用方法验证结果 Class<?> clazz = new InvokeRandomTransformClassVisitorTest().loadClassFromBytes("类名", modifiedClassBytes); Object instance = clazz.getDeclaredConstructor().newInstance(); Method method = clazz.getMethod("testRandom", String.class); System.out.println(method.invoke(instance, "")); } }
其实无非就是将改写后的字节码输出到新的文件这样可以再通过IDE反编译获取改写后的java文件
再将修改后的字节码加载到JVM中通过反射调用验证其逻辑
一个是看一个是验证执行结果,看通常可以让你直接明白哪里的操作有问题,操作数的位置不对,但即使看上去没问题执行也不一定就ok就比如上面提到的拆装箱问题仅仅从看是很难发现的
分析报错也是非常重要的一环
javaException in thread "main" java.lang.VerifyError: Bad type on operand stack in aastore Exception Details: Location: xxx/xxx/InvokeRandomTransformClassVisitorEntity.testRandom(Ljava/lang/String;)Ljava/util/List; @54: aastore Reason: Type 'xxx/xxx/RandomMethod' (current frame, stack[3]) is not assignable to reference type Current Frame: bci: @54 flags: { } locals: { 'xxx/xxx/InvokeRandomTransformClassVisitorEntity', 'java/lang/String', 'java/util/Random', 'java/util/ArrayList' } stack: { 'java/util/Random', '[Ljava/lang/Object;', 'xxx/xxx/RandomMethod', 'xxx/xxx/RandomMethod', integer, 'java/lang/String' } Bytecode: 0x0000000: bb00 1559 b700 164d bb00 1859 b700 194e 0x0000010: 2c14 001a 1001 bd00 045b 5b57 1000 5b57 0x0000020: b800 2153 b200 2712 28bd 0004 1229 122b 0x0000030: 5359 122c 122d 5359 122e 122f b800 3453 0x0000040: 5912 3512 2eb8 0034 53b8 003b c000 3d2d 0x0000050: 59b6 0041 57ba 0055 0000 b900 5902 002d 0x0000060: 2c11 03e8 1001 bd00 045a 5f10 005f b800 0x0000070: 3453 b200 5c12 28bd 0004 1229 122b 5359 0x0000080: 122c 122d 5359 122e 125d b800 3453 5912 0x0000090: 3512 35b8 0034 53b8 003b c000 31b6 0061 0x00000a0: b800 34b9 0049 0200 572d b800 6711 03e8 0x00000b0: 1001 bd00 045a 5f10 005f b800 3453 b200 0x00000c0: 6a12 28bd 0004 1229 122b 5359 122c 122d 0x00000d0: 5359 122e 126b b800 3453 5912 3512 28b8 0x00000e0: 0034 53b8 003b c000 31b6 0061 b800 34b9 0x00000f0: 0049 0200 572d b800 6710 00bd 0004 b200 0x0000100: 6e12 28bd 0004 1229 122b 5359 122c 122d 0x0000110: 5359 122e 126f b800 3453 5912 3512 70b8 0x0000120: 0034 53b8 003b c000 31b6 0061 b800 34b9 0x0000130: 0049 0200 572d b800 6704 100a 1002 bd00 0x0000140: 045a 5f10 015f b800 3453 5a5f 1000 5fb8 0x0000150: 0034 53b2 0073 1228 bd00 0412 2912 2b53 0x0000160: 5912 2c12 2d53 5912 2e12 74b8 0034 5359 0x0000170: 1235 1275 b800 3453 b800 3bc0 0031 b600 0x0000180: 61b8 0034 b900 4902 0057 2db8 0067 1000 0x0000190: bd00 04b2 0078 1228 bd00 0412 2912 2b53 0x00001a0: 5912 2c12 2d53 5912 2e12 79b8 0034 5359 0x00001b0: 1235 127a b800 3453 b800 3bc0 007c b600 0x00001c0: 80b8 0083 b900 4902 0057 2db8 0067 1000 0x00001d0: bd00 04b2 0086 1228 bd00 0412 2912 2b53 0x00001e0: 5912 2c12 2d53 5912 2e12 87b8 0034 5359 0x00001f0: 1235 1288 b800 3453 b800 3bc0 001d b600 0x0000200: 8cb8 0021 b900 4902 0057 1010 bc08 3a04 0x0000210: bb00 8e59 b700 8f19 0410 01bd 0004 5a5f 0x0000220: 1000 5f53 b200 9212 28bd 0004 1229 122b 0x0000230: 5359 122c 122d 5359 122e 1293 b800 3453 0x0000240: 5912 3512 94b8 0034 53b8 003b 2d19 04b9 0x0000250: 0049 0200 572d b0 at java.lang.Class.getDeclaredConstructors0(Native Method) at java.lang.Class.privateGetDeclaredConstructors(Class.java:2671) at java.lang.Class.getConstructor0(Class.java:3075) at java.lang.Class.getDeclaredConstructor(Class.java:2178) at xxx.xxx.InvokeRandomTransformClassVisitorTest.executeByteCodeRewrite(InvokeRandomTransformClassVisitorTest.java:46) at xxx.xxx.InvokeRandomTransformClassVisitorTest.main(InvokeRandomTransformClassVisitorTest.java:27)
这个就是非常典型的报错,它的含义是在执行aastore时操作数的类型不对,Reason里面是更详细的信息:栈中下标为3的元素不是一个引用类型,这个时候你就需要检查栈的操作数,确保aastore指令执行时栈上前3个操作数是数组、下标、元素。
在排查问题时像我上面教的那样分析栈的状态,确定操作数的位置,这可以让你少走很多弯路。
好了,到这里如果你真的有在细心阅读且跟着我的例子实践了一遍,那么恭喜你你在asm使用上已经入门了,很多字节码改写对你而言将没有秘密,从此你将自由穿梭在java世界之中而没有什么可以阻拦你。
但修行在个人,jvm的庞大世界才刚刚向你展开,享受且继续努力吧。 原文地址:pebble-skateboard-d46.notion.site/Java-asm-19...
附件(完整代码)
EasyClassWriter
java
public class EasyClassWriter extends ClassWriter {
private final ClassLoader classLoader;
public EasyClassWriter(int flags, ClassLoader classLoader) {
super(flags);
this.classLoader = classLoader;
}
@Override
public String getCommonSuperClass(String type1, String type2) {
try {
String className1 = type1.replace("/", ".");
String className2 = type2.replace("/", ".");
if (classLoader != null) {
// 从字节流中读读取公共superClass
String[] type1SupperArr = AsmClassUtil.readSuperClass(type1, classLoader);
String[] type2SupperArr = AsmClassUtil.readSuperClass(type2, classLoader);
for (int i = type1SupperArr.length; i >= 0; i--) {
for (int j = type2SupperArr.length; j >= 0; j--) {
String type1Idx = i == type1SupperArr.length ? className1 : type1SupperArr[i];
String type2Idx = j == type2SupperArr.length ? className2 : type2SupperArr[j];
if (type1Idx.equals(type2Idx)) {
return type1Idx.replace('.', '/');
}
}
}
}
} catch (Throwable ignored) {
}
return OBJECT_JVM_CLASS_NAME;
}
}
InvokeRandomTransformClassVisitor
java
public class InvokeRandomTransformClassVisitor extends TransformClassVisitor {
private static final Type TYPE = Type.getType(RandomMethodBridge.class);
private static final String RANDOM_METHOD_NAME = RandomMethod.class.getName();
private static final Method INVOKE_METHOD = Method.getMethod(
String.format("Object invoke(java.util.Random,Object[],%s,Object[])", RANDOM_METHOD_NAME));
private static final String RANDOM_METHOD_JVM_NAME = RANDOM_METHOD_NAME.replace(".", "/");
private static final String RANDOM_METHOD_JVM_RETURN_NAME = "L" + RANDOM_METHOD_JVM_NAME + ";";
public InvokeRandomTransformClassVisitor(ClassVisitor classVisitor) {
super(AsmCompatible.apiVersion(), classVisitor);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature,
String[] exceptions) {
MethodVisitor visitor = super.visitMethod(access, name, desc, signature, exceptions);
// 跳过特殊方法、Object的内部方法、抽象方法、native方法
if (ExtensionUtil.ignoreMethod(name) || AsmClassUtil.isObjectMethod(name, desc)
|| Modifier.isAbstract(access) || Modifier.isNative(access)) {
return visitor;
}
// 当前方法需要增强
if (RandomUtil.shouldTransformMethod(className, name)) {
return new InvokeRandomTransformMethodVisitor(api, visitor, access, name, desc);
}
return visitor;
}
private class InvokeRandomTransformMethodVisitor extends AdviceAdapter {
private final String methodName;
private int methodStartLineNumber = -1;
private int currentLineNumber = -1;
protected InvokeRandomTransformMethodVisitor(int api, MethodVisitor methodVisitor, int access,
String name, String descriptor) {
super(api, methodVisitor, access, name, descriptor);
this.methodName = name;
}
@Override
public void visitLineNumber(int line, Label start) {
if (this.methodStartLineNumber == -1) {
this.methodStartLineNumber = line;
}
this.currentLineNumber = line;
super.visitLineNumber(line, start);
}
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor,
boolean isInterface) {
if (INVOKEVIRTUAL == opcode) {
RandomMethod randomMethod = RandomMethod.getByMethodIdentity(
MethodSignatureParser.toNormalClass(owner), name, descriptor);
if (null != randomMethod) {
result.setEnhanced(true);
// 将Random方法的入参合并成一个数组压入操作数栈
AsmClassUtil.visitArgumentsAsArray(this, descriptor);
// 压入随机方法名
visitFieldInsn(GETSTATIC,
RANDOM_METHOD_JVM_NAME,
randomMethod.name(),
RANDOM_METHOD_JVM_RETURN_NAME);
// 创建一个大小为4的数组
visitLdcInsn(4);
visitTypeInsn(ANEWARRAY, "java/lang/Object");
// 填入类名
visitInsn(DUP);
visitLdcInsn(0);
visitLdcInsn(className);
visitInsn(AASTORE);
// 填入方法名
visitInsn(DUP);
visitLdcInsn(1);
visitLdcInsn(methodName);
visitInsn(AASTORE);
// 填入行号
visitInsn(DUP);
visitLdcInsn(2);
visitLdcInsn(currentLineNumber);
// 基本类型int转换为包装类型Integer,因为数组只能存放包装类型
AsmClassUtil.box(this, INT_TYPE);
visitInsn(AASTORE);
// 填入方法内行号 (方法的第几行)
visitInsn(DUP);
visitLdcInsn(3);
visitLdcInsn(currentLineNumber - methodStartLineNumber);
// 基本类型int转换为包装类型Integer,因为数组只能存放包装类型
AsmClassUtil.box(this, INT_TYPE);
visitInsn(AASTORE);
invokeStatic(TYPE, INVOKE_METHOD);
// 提取响应类型描述符
AsmClassUtil.unbox(this, Type.getReturnType(descriptor));
return;
}
}
super.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
}
}
TransformClassVisitor
java
public class TransformClassVisitor extends ClassVisitor {
protected final int api;
protected final TransformResult result;
protected String className;
public TransformClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
this.api = api;
this.result = TransformResult.notEnhanced();
}
@Override
public void visit(int apiVersion, int access, String name, String signature, String superName,
String[] interfaces) {
this.className = name.replace("/", ".");
super.visit(apiVersion, access, name, signature, superName, interfaces);
}
public TransformResult getTransformResult() {
return result;
}
}
TransformResult
java
public class TransformResult {
private boolean enhanced = false;
private byte[] classBuffer = null;
public static TransformResult notEnhanced() {
TransformResult transformResult = new TransformResult();
transformResult.enhanced = false;
return transformResult;
}
public static TransformResult enhanced(byte[] classBuffer) {
TransformResult transformResult = new TransformResult();
transformResult.enhanced = true;
transformResult.classBuffer = classBuffer;
return transformResult;
}
public boolean isEnhanced() {
return enhanced;
}
public byte[] getClassBuffer() {
return classBuffer;
}
public void setEnhanced(boolean enhanced) {
this.enhanced = enhanced;
}
public void setClassBuffer(byte[] classBuffer) {
this.classBuffer = classBuffer;
}
}
AsmClassUtil
java
public class AsmClassUtil {
private static final Map<Object, Map<String, String[]>> LOADER_SUPER_CLASS_CACHE_MAP = new IdentityHashMap<>();
private static final Lock SUPPER_CLASS_CACHE_INIT_LOCK = new ReentrantLock();
private static final Lock SUPER_CLASS_ARR_INIT_LOCK = new ReentrantLock();
public static final String OBJECT_JVM_CLASS_NAME = "java/lang/Object";
/**
* @author: Ares
* @description: 包装类型拆箱
* @description: box type unboxing
* @time: 2025-02-11 17:38:30
* @params: [methodVisitor, descriptor] 方法访问者,描述符
*/
public static void unboxing(MethodVisitor methodVisitor, String descriptor) {
unboxing(methodVisitor, Type.getType(descriptor));
}
/**
* @author: Ares
* @description: 包装类型拆箱
* @description: box type unboxing
* @time: 2025-01-23 22:10:15
* @params: [methodVisitor, descriptor, isMethod] 方法访问者,描述符, 是否是方法
*/
public static void unboxing(MethodVisitor methodVisitor, Type type) {
switch (type.getSort()) {
case Type.BOOLEAN:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Boolean");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Boolean", "booleanValue", "()Z",
false);
break;
case Type.BYTE:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Byte");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Byte", "byteValue", "()B", false);
break;
case Type.CHAR:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Character");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Character", "charValue", "()C",
false);
break;
case Type.SHORT:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Short");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Short", "shortValue", "()S", false);
break;
case Type.INT:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Integer");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Integer", "intValue", "()I", false);
break;
case Type.FLOAT:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Float");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Float", "floatValue", "()F", false);
break;
case Type.LONG:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Long");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Long", "longValue", "()J", false);
break;
case Type.DOUBLE:
methodVisitor.visitTypeInsn(CHECKCAST, "java/lang/Double");
methodVisitor.visitMethodInsn(INVOKEVIRTUAL, "java/lang/Double", "doubleValue", "()D",
false);
break;
case Type.VOID:
break;
default:
String descriptor = type.getDescriptor();
char c = descriptor.charAt(0);
if (c == '[') {
methodVisitor.visitTypeInsn(CHECKCAST, descriptor);
} else {
methodVisitor.visitTypeInsn(CHECKCAST, descriptor.substring(1, descriptor.length() - 1));
}
}
}
/**
* @author: Ares
* @description: 基本类型装箱
* @description: base type boxing
* @time: 2025-01-23 22:10:15
* @params: [methodVisitor, descriptor] 方法访问者,描述符
*/
public static void boxing(MethodVisitor methodVisitor, String descriptor) {
Type type = Type.getType(descriptor);
boxing(methodVisitor, type);
}
public static void boxing(MethodVisitor methodVisitor, Type type) {
// 根据参数类型进行加载和装箱
// load and box according to the parameter type
switch (type.getSort()) {
case Type.INT:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Integer", "valueOf",
"(I)Ljava/lang/Integer;", false);
break;
case Type.BOOLEAN:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Boolean", "valueOf",
"(Z)Ljava/lang/Boolean;", false);
break;
case Type.CHAR:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Character", "valueOf",
"(C)Ljava/lang/Character;", false);
break;
case Type.BYTE:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Byte", "valueOf",
"(B)Ljava/lang/Byte;", false);
break;
case Type.SHORT:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Short", "valueOf",
"(S)Ljava/lang/Short;", false);
break;
case Type.LONG:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Long", "valueOf",
"(J)Ljava/lang/Long;", false);
break;
case Type.FLOAT:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Float", "valueOf",
"(F)Ljava/lang/Float;", false);
break;
case Type.DOUBLE:
methodVisitor.visitMethodInsn(INVOKESTATIC, "java/lang/Double", "valueOf",
"(D)Ljava/lang/Double;", false);
break;
case Type.OBJECT:
case Type.ARRAY:
case Type.VOID:
break;
default:
throw new IllegalArgumentException("Unsupported argument type: " + type);
}
}
/**
* @author: Ares
* @description: 将原方法的入参合并成一个数组传给桥接方法
* @description: Merge the original method's arguments into an array and pass them to the bridge
* method
* @time: 2025-02-11 17:55:36
* @params: [methodVisitor, descriptor] 方法访问者,描述符
*/
public static void visitArgumentsAsArray(MethodVisitor methodVisitor, String descriptor) {
/**
[..., random, int]
ANEWARRAY [..., random, int, array]
DUP_X1 [..., random, array, int, array]
SWAP [..., random, array, array, int]
*PUSH [..., random, array, array, int, 0]
SWAP [..., random, array, array, 0, int]
AASTORE [..., random, array]
[..., random, int, int]
ANEWARRAY [..., random, int, int, array]
DUP_X1 [..., random, int, array, int, array]
SWAP [..., random, int, array, array, int]
*PUSH [..., random, int, array, array, int, 1]
SWAP [..., random, int, array, array, 1, int]
AASTORE [..., random, int, array]
DUP_X1 [..., random, array, int, array]
SWAP [..., random, array, array, int]
*PUSH [..., random, array, array, int, 0]
SWAP [..., random, array, array, 0, int]
AASTORE [..., random, array]
[..., random, long]
ANEWARRAY [..., random, long, array]
DUP_X2 [..., random, array, long, array]
DUP_X2 [..., random, array, array, long, array]
POP [..., random, array, array, long]
*PUSH [..., random, array, array, long, 0]
DUP_X2 [..., random, array, array, 0, long, 0]
POP [..., random, array, array, 0, long]
AASTORE [..., random, array]
*/
// 解析方法描述符以获得参数类型
// Parse the method descriptor to get the argument type
Type[] argumentTypes = Type.getArgumentTypes(descriptor);
int argumentCount = argumentTypes.length;
// 创建一个指定大小的数组并压入栈
// Create an array of a specified size and push it onto the stack
if (argumentCount <= Byte.MAX_VALUE) {
methodVisitor.visitIntInsn(BIPUSH, argumentCount);
} else {
methodVisitor.visitIntInsn(SIPUSH, argumentCount);
}
methodVisitor.visitTypeInsn(ANEWARRAY, OBJECT_JVM_CLASS_NAME);
// 倒序遍历参数类型,将每个参数装箱并加入数组
// Traverse the argument type in reverse order, box each argument and add it to the array
for (int i = argumentCount - 1; i >= 0; i--) {
Type type = argumentTypes[i];
if (LONG_TYPE.equals(type) || DOUBLE_TYPE.equals(type)) {
methodVisitor.visitInsn(DUP_X2);
} else {
methodVisitor.visitInsn(DUP_X1);
}
swap(methodVisitor, type);
// 指定数组索引
// Specify the array index
if (i <= Byte.MAX_VALUE) {
methodVisitor.visitIntInsn(BIPUSH, i);
} else {
methodVisitor.visitIntInsn(SIPUSH, i);
}
swap(methodVisitor, type);
box(methodVisitor, type);
methodVisitor.visitInsn(AASTORE);
}
}
/**
* @author: Ares
* @description: 根据类型交换栈顶元素
* @description: Exchange the top element of the stack according to the type
* @time: 2025-02-12 18:46:54
* @params: [methodVisitor, type] 方法访问者,类型
*/
public static void swap(MethodVisitor methodVisitor, Type type) {
if (LONG_TYPE.equals(type) || DOUBLE_TYPE.equals(type)) {
methodVisitor.visitInsn(DUP_X2);
methodVisitor.visitInsn(POP);
} else {
methodVisitor.visitInsn(SWAP);
}
}
/**
* @author: Ares
* @description: 读取父类数组
* @description: read super class array
* @time: 2025-02-11 17:36:58
* @params: [className, loader] 类名,类加载器
* @return: java.lang.String[] 父类数组
*/
public static String[] readSuperClass(String className, ClassLoader loader) throws IOException {
return readSuperClass(className, loader, null);
}
/**
* @author: Ares
* @description: 读取父类数组
* @description: read super class array
* @time: 2025-02-11 17:36:58
* @params: [className, loader, classBytes] 类名,类加载器,类字节码
* @return: java.lang.String[] 父类数组
*/
public static String[] readSuperClass(String className, ClassLoader loader, byte[] classBytes)
throws IOException {
if (loader == null) {
return null;
}
Map<String, String[]> supperClassCacheMap = LOADER_SUPER_CLASS_CACHE_MAP.get(loader);
if (supperClassCacheMap == null) {
SUPPER_CLASS_CACHE_INIT_LOCK.lock();
try {
// 类加载器的父类加载器一般不会超过4层(根加载器是BootStrap加载器)
// The parent class loader of the class loader generally does not exceed 4 layers (the root loader is the BootStrap loader)
supperClassCacheMap = MapUtil.newConcurrentMap(4);
LOADER_SUPER_CLASS_CACHE_MAP.put(loader, supperClassCacheMap);
} finally {
SUPPER_CLASS_CACHE_INIT_LOCK.unlock();
}
}
String[] superClassArr = supperClassCacheMap.get(className);
if (superClassArr == null) {
SUPER_CLASS_ARR_INIT_LOCK.lock();
try {
superClassArr = supperClassCacheMap.get(className);
if (superClassArr == null) {
superClassArr = innerReadSuperClass(className, loader, classBytes);
if (superClassArr == null) {
superClassArr = new String[0];
}
supperClassCacheMap.put(className, superClassArr);
}
} finally {
SUPER_CLASS_ARR_INIT_LOCK.unlock();
}
}
return superClassArr;
}
private static String[] innerReadSuperClass(String className, ClassLoader loader,
byte[] classBytes) throws IOException {
InputStream inputStream;
if (null != classBytes) {
inputStream = new ByteArrayInputStream(classBytes);
} else {
inputStream = loader.getResourceAsStream(className.replace(".", "/") + ".class");
}
if (null == inputStream) {
return null;
}
ClassReader classReader = new ClassReader(inputStream);
String superClassName = classReader.getSuperName();
if (null != superClassName && !OBJECT_JVM_CLASS_NAME.equals(superClassName)) {
String superClassNameReal = superClassName.replace("/", ".");
String[] superClasses = readSuperClass(superClassName, loader);
if (null != superClasses && superClasses.length > 0) {
String[] mergedSuperClasses = new String[superClasses.length + 1];
System.arraycopy(superClasses, 0, mergedSuperClasses, 0, superClasses.length);
mergedSuperClasses[superClasses.length] = superClassNameReal;
return mergedSuperClasses;
} else {
return new String[]{superClassNameReal};
}
}
return null;
}
public static String forPath(String className) {
return className.replaceAll("\\.", "/").concat(".class");
}
public static boolean isObjectMethod(String methodName, String methodDesc) {
if ("hashCode".equals(methodName) && "()I".equals(methodDesc)) {
return true;
}
if ("equals".equals(methodName) && "(Ljava/lang/Object;)Z".equals(methodDesc)) {
return true;
}
if ("clone".equals(methodName) && "()Ljava/lang/Object;".equals(methodDesc)) {
return true;
}
if ("finalize".equals(methodName) && "()V".equals(methodDesc)) {
return true;
}
if ("toString".equals(methodName) && "()Ljava/lang/String;".equals(methodDesc)) {
return true;
}
if ("getClass".equals(methodName) && "()Ljava/lang/Class;".equals(methodDesc)) {
return true;
}
if ("notify".equals(methodName) && "()V".equals(methodDesc)) {
return true;
}
if ("notifyAll".equals(methodName) && "()V".equals(methodDesc)) {
return true;
}
if ("wait".equals(methodName) && "(J)V".equals(methodDesc)) {
return true;
}
return false;
}
}
ExtensionUtil
java
public class ExtensionUtil {
private static final Set<String> IGNORE_METHOD_SET = CollectionUtil.asSet(JACOCO_INIT_METHOD_NAME,
CLASS_INIT_METHOD_NAME, CONSTRUCTOR_METHOD_NAME);
public static boolean ignoreMethod(String methodName) {
return IGNORE_METHOD_SET.contains(methodName);
}
}
RandomMethod
java
public enum RandomMethod {
/**
* Random
*/
RANDOM_NEXT_BOOLEAN(0, Random.class.getName() + "/nextBoolean~()Z"),
RANDOM_NEXT_DOUBLE(1, Random.class.getName() + "/nextDouble~()D"),
RANDOM_NEXT_FLOAT(2, Random.class.getName() + "/nextFloat~()F"),
RANDOM_NEXT_GAUSSIAN(3, Random.class.getName() + "/nextGaussian~()D"),
RANDOM_NEXT_INT(4, Random.class.getName() + "/nextInt~()I"),
Random_NEXT_INT_WITH_INT(5, Random.class.getName() + "/nextInt~(I)I", true),
Random_NEXT_LONG(6, Random.class.getName() + "/nextLong~()J"),
RANDOM_NEXT_BYTES(7, Random.class.getName() + "/nextBytes~([B)V", true, true),
RANDOM_DOUBLES(8, Random.class.getName() + "/doubles~()Ljava/util/stream/DoubleStream;"),
RANDOM_DOUBLES_WITH_TWO_DOUBLES(9, Random.class.getName() + "/doubles~(DD)Ljava/util/stream/DoubleStream;", true),
RANDOM_DOUBLES_WITH_LONG(10, Random.class.getName() + "/doubles~(J)Ljava/util/stream/DoubleStream;", true),
RANDOM_DOUBLES_WITH_LONG_AND_TWO_DOUBLES(11, Random.class.getName() + "/doubles~(JDD)Ljava/util/stream/DoubleStream;"),
RANDOM_INTS(12, Random.class.getName() + "/ints~()Ljava/util/stream/IntStream;"),
RANDOM_INTS_WITH_TWO_INT(13, Random.class.getName() + "/ints~(II)Ljava/util/stream/IntStream;", true),
RANDOM_INTS_WITH_LONG(14, Random.class.getName() + "/ints~(J)Ljava/util/stream/IntStream;", true),
RANDOM_INTS_WITH_LONG_AND_TWO_INTS(15, Random.class.getName() + "/ints~(JII)Ljava/util/stream/IntStream;", true),
RANDOM_LONGS(16, Random.class.getName() + "/longs~()Ljava/util/stream/LongStream;"),
RANDOM_LONGS_WITH_LONG(17, Random.class.getName() + "/longs~()Ljava/util/stream/LongStream;", true),
RANDOM_LONGS_WITH_TWO_LONGS(18, Random.class.getName() + "/longs~(JJ)Ljava/util/stream/LongStream;", true),
RANDOM_LONGS_WITH_THIRD_LONGS(19, Random.class.getName() + "/longs~(JJJ)Ljava/util/stream/LongStream;", true),
/**
* ThreadLocalRandom
*/
THREAD_LOCAL_RANDOM_BOOLEAN(20, ThreadLocalRandom.class.getName() + "/nextBoolean~()Z"),
THREAD_LOCAL_RANDOM_NEXT_DOUBLE(21, ThreadLocalRandom.class.getName() + "/nextDouble~()D"),
THREAD_LOCAL_RANDOM_NEXT_DOUBLE_WITH_DOUBLE(22, ThreadLocalRandom.class.getName() + "/nextDouble~(D)D", true),
THREAD_LOCAL_RANDOM_NEXT_DOUBLE_WITH_TWO_DOUBLE(23, ThreadLocalRandom.class.getName() + "/nextDouble~(DD)D", true),
THREAD_LOCAL_RANDOM_NEXT_FLOAT(24, ThreadLocalRandom.class.getName() + "/nextFloat~()F"),
THREAD_LOCAL_RANDOM_NEXT_GAUSSIAN(25, ThreadLocalRandom.class.getName() + "/nextGaussian~()D"),
THREAD_LOCAL_RANDOM_NEXT_INT(26, ThreadLocalRandom.class.getName() + "/nextInt~()I"),
THREAD_LOCAL_RANDOM_NEXT_INT_WITH_INT(27, ThreadLocalRandom.class.getName() + "/nextInt~(I)I", true),
THREAD_LOCAL_RANDOM_NEXT_INT_WITH_TWO_INT(28, ThreadLocalRandom.class.getName() + "/nextInt~(II)I", true),
THREAD_LOCAL_RANDOM_NEXT_LONG(29, ThreadLocalRandom.class.getName() + "/nextLong~()J"),
THREAD_LOCAL_RANDOM_NEXT_LONG_WITH_LONG(30, ThreadLocalRandom.class.getName() + "/nextLong~(J)J", true),
THREAD_LOCAL_RANDOM_NEXT_LONG_WITH_TWO_LONG(31, ThreadLocalRandom.class.getName() + "/nextLong~(JJ)J", true),
THREAD_LOCAL_RANDOM_DOUBLES(32, ThreadLocalRandom.class.getName() + "/doubles~()Ljava/util/stream/DoubleStream;"),
THREAD_LOCAL_RANDOM_DOUBLES_WITH_TWO_DOUBLES(33, ThreadLocalRandom.class.getName() + "/doubles~(DD)Ljava/util/stream/DoubleStream;", true),
THREAD_LOCAL_RANDOM_DOUBLES_WITH_LONG(34, ThreadLocalRandom.class.getName() + "/doubles~(J)Ljava/util/stream/DoubleStream;", true),
THREAD_LOCAL_RANDOM_DOUBLES_WITH_LONG_AND_TWO_DOUBLES(35, ThreadLocalRandom.class.getName() + "/doubles~(JDD)Ljava/util/stream/DoubleStream;"),
THREAD_LOCAL_RANDOM_INTS(36, ThreadLocalRandom.class.getName() + "/ints~()Ljava/util/stream/IntStream;"),
THREAD_LOCAL_RANDOM_INTS_WITH_TWO_INT(37, ThreadLocalRandom.class.getName() + "/ints~(II)Ljava/util/stream/IntStream;", true),
THREAD_LOCAL_RANDOM_INTS_WITH_LONG(38, ThreadLocalRandom.class.getName() + "/ints~(J)Ljava/util/stream/IntStream;", true),
THREAD_LOCAL_RANDOM_INTS_WITH_LONG_AND_TWO_INTS(39, ThreadLocalRandom.class.getName() + "/ints~(JII)Ljava/util/stream/IntStream;", true),
THREAD_LOCAL_RANDOM_LONGS(40, ThreadLocalRandom.class.getName() + "/longs~()Ljava/util/stream/LongStream;"),
THREAD_LOCAL_RANDOM_LONGS_WITH_LONG(41, ThreadLocalRandom.class.getName() + "/longs~()Ljava/util/stream/LongStream;", true),
THREAD_LOCAL_RANDOM_LONGS_WITH_TWO_LONGS(42, ThreadLocalRandom.class.getName() + "/longs~(JJ)Ljava/util/stream/LongStream;", true),
THREAD_LOCAL_RANDOM_LONGS_WITH_THIRD_LONGS(43, ThreadLocalRandom.class.getName() + "/longs~(JJJ)Ljava/util/stream/LongStream;", true),
/**
* SecureRandom
*/
SECURE_RANDOM_NEXT_BYTES(44, SecureRandom.class.getName() + "/nextBytes~([B)V", true, true),
;
private static final Map<Integer, RandomMethod> CODE_CACHED = new HashMap<>(values().length);
private static final Map<String, RandomMethod> METHOD_IDENTITY_CACHED = new HashMap<>(values().length);
static {
for (RandomMethod randomMethod : values()) {
if (null != randomMethod) {
METHOD_IDENTITY_CACHED.put(randomMethod.getMethodDesc(), randomMethod);
CODE_CACHED.put(randomMethod.getCode(), randomMethod);
}
}
}
public static RandomMethod getByMethodIdentity(String className, String methodName,
String methodDesc) {
return METHOD_IDENTITY_CACHED.get(String.format("%s/%s~%s", className, methodName, methodDesc));
}
public static RandomMethod getByCode(Integer code) {
return CODE_CACHED.get(code);
}
private final Integer code;
private final String methodDesc;
private final boolean hasParam;
private final boolean nextBytes;
RandomMethod(Integer code, String methodDesc) {
this(code, methodDesc, false, false);
}
RandomMethod(boolean nextBytes, Integer code, String methodDesc) {
this(code, methodDesc, false, nextBytes);
}
RandomMethod(Integer code, String methodDesc, boolean hasParam) {
this(code, methodDesc, hasParam, false);
}
RandomMethod(Integer code, String methodDesc, boolean hasParam, boolean nextBytes) {
this.code = code;
this.methodDesc = methodDesc;
this.hasParam = hasParam;
this.nextBytes = nextBytes;
}
public Integer getCode() {
return code;
}
public String getMethodDesc() {
return methodDesc;
}
public boolean isHasParam() {
return hasParam;
}
public boolean isNextBytes() {
return nextBytes;
}
}
InvokeRandomTransformClassVisitorTest
java
public class InvokeRandomTransformClassVisitorTest extends ClassLoader {
public Class<?> loadClassFromBytes(String className, byte[] classBytes) {
// Define class from byte array
return defineClass(className, classBytes, 0, classBytes.length);
}
public static void main(String[] args) throws Exception {
String classpath = "原始class文件";
String outputClassPath = "改写后class文件保存路径";
executeByteCodeRewrite(classPath, outputClassPath);
}
private static void executeByteCodeRewrite(String classPath, String outputClassPath)
throws Exception {
// 从class文件中读取字节数组
byte[] classFileBuffer = Files.readAllBytes(Paths.get(classPath));
TransformResult transformResult = ExtensionUtil.transformMethod(null, classFileBuffer,
InvokeRandomTransformClassVisitor::new);
byte[] modifiedClassBytes = transformResult.getClassBuffer();
// 将修改后的字节数组写入到指定路径的文件中
try (FileOutputStream fos = new FileOutputStream(outputClassPath)) {
fos.write(modifiedClassBytes);
}
System.out.println("Class transformed and written to file: " + outputClassPath);
// 加载修改后的类并使用反射调用方法验证结果
Class<?> clazz = new InvokeRandomTransformClassVisitorTest().loadClassFromBytes(
InvokeRandomTransformClassVisitorEntity.class.getName(), modifiedClassBytes);
Object instance = clazz.getDeclaredConstructor().newInstance();
Method method = clazz.getMethod("testRandom", String.class);
System.out.println(method.invoke(instance, ""));
}
}
InvokeRandomTransformClassVisitorEntity
java
public class InvokeRandomTransformClassVisitorEntity {
public List<Object> testRandom(String noUse) {
Random random = new Random();
List<Object> list = new ArrayList<>();
random.ints(4).forEach(list::add);
list.add(random.nextInt(1_000));
list.add(ThreadLocalRandom.current().nextInt(1_000));
list.add(ThreadLocalRandom.current().nextInt());
list.add(ThreadLocalRandom.current().nextInt(1, 10));
list.add(ThreadLocalRandom.current().nextDouble());
list.add(ThreadLocalRandom.current().nextLong());
// list.add(ThreadLocalRandom.current().nextDouble(1.0));
// list.add(ThreadLocalRandom.current().nextDouble(1.0, 100.0));
byte[] bytes = new byte[16];
new SecureRandom().nextBytes(bytes);
list.add(bytes);
return list;
}
}