You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于ASM实现类似Fabric Mixin:如何将字节码写回原方法?

问题描述

我正在尝试制作类似Minecraft Fabric模组加载器中的Mixin工具,目前已完成的部分似乎能正常运行(未抛出错误,但暂无其他验证方式),但现在卡在了需要将合并后的字节码写回原方法的步骤。请问该如何实现?

我的代码
/**
 * @param method1 基础方法
 * @param method2 待注入的代码所属方法
 * @param descriptor 两个方法的描述符
 * @param access_level 两个方法的访问权限(私有、公开等)
 * @param pos 注入位置
 * @param method1Class 基础方法所属的类
 * @param method 基础方法对应的Method对象
 * @throws Exception 出现异常时抛出
 */
public static void mixin(MethodNode method1, MethodNode method2, String descriptor, int access_level, String pos, Class<?> method1Class, Method method) throws Exception {
    // 创建合并特征的新方法
    MethodNode mergedMethod = new MethodNode(
            access_level,
            "mergedMethod",
            descriptor,
            null,
            null
    );

    // 将两个方法的指令合并到新方法中
    switch (pos){
        case "HEAD" -> {
            mergedMethod.instructions.add(method2.instructions);
            mergedMethod.instructions.add(method1.instructions);
        }
        case "TAIL" -> {
            mergedMethod.instructions.add(method1.instructions);
            mergedMethod.instructions.add(method2.instructions);
        }
        default -> {
            throw new IllegalArgumentException("Invalid mixin point provided!");
        }
    }

    // 重新计算栈大小和最大本地变量数
    mergedMethod.maxStack = Math.max(method1.maxStack, method2.maxStack);
    mergedMethod.maxLocals = Math.max(method1.maxLocals, method2.maxLocals);

    // 生成合并方法的字节码
    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
    mergedMethod.accept(cw);

    byte[] bytecode = cw.toByteArray();

    method.setAccessible(true);
}
解决方案

要把合并后的字节码写回原方法,核心是通过底层API或字节码操作工具修改目标方法的字节码,以下是两种可行方案:

方案1:使用Unsafe类直接修改方法字节码

Unsafe是JDK内部API,可直接操作内存中的方法字节码,适合已加载类的方法修改:

// 在你的mixin方法末尾添加以下代码
// 获取Unsafe实例
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);

// 获取方法的methodData字段(HotSpot JVM结构)
Field methodDataField = Method.class.getDeclaredField("methodData");
methodDataField.setAccessible(true);
Object methodData = methodDataField.get(method);

// 获取methodData中的code字段,替换为合并后的字节码
Field codeField = methodData.getClass().getDeclaredField("code");
codeField.setAccessible(true);
unsafe.putObject(methodData, unsafe.objectFieldOffset(codeField), bytecode);

注意:Unsafe依赖JVM实现,不同厂商/版本兼容性差,生产环境需谨慎使用。

方案2:使用ASM修改类字节码并重定义类

通过ASM修改整个类的字节码,再借助Instrumentation重定义已加载类,兼容性更好:

步骤1:读取原类并创建ClassWriter

// 替换原代码中生成bytecode的部分
ClassReader cr = new ClassReader(method1Class.getName());
// 使用COMPUTE_FRAMES自动计算栈帧和局部变量表,避免手动计算的错误
ClassWriter classWriter = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES);

步骤2:自定义ClassVisitor替换目标方法

ClassVisitor cv = new ClassVisitor(Opcodes.ASM9, classWriter) {
    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        // 匹配目标方法(方法名+描述符)
        if (name.equals(method.getName()) && desc.equals(descriptor)) {
            // 将合并后的方法指令写入ClassWriter
            mergedMethod.accept(super.visitMethod(access, name, desc, signature, exceptions));
            return null; // 跳过原方法的处理
        }
        return super.visitMethod(access, name, desc, signature, exceptions);
    }
};
cr.accept(cv, ClassReader.SKIP_DEBUG);
byte[] modifiedClassBytes = classWriter.toByteArray();

步骤3:重定义类(需Instrumentation实例)

如果能获取到Instrumentation对象,直接重定义类:

instrumentation.redefineClasses(new ClassDefinition(method1Class, modifiedClassBytes));

若没有Instrumentation,可通过自定义类加载器加载修改后的字节码,但仅对该加载器创建的类实例生效。

额外注意事项

  • 直接拼接指令可能导致栈帧异常,建议使用ClassWriter.COMPUTE_FRAMES让ASM自动计算栈帧和局部变量表,替代手动取maxStack/maxLocals最大值的逻辑。
  • 注入逻辑需考虑方法的异常表、局部变量表冲突问题,避免合并后出现运行时错误。
  • 若目标方法是静态方法、构造方法,需调整访问权限和指令处理逻辑,保证兼容性。

内容的提问来源于stack exchange,提问作者Qweru

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 19:25:03