字节伙伴堆栈操作:如何使用局部变量和 if 语句

Byte buddy stack manipulation: how to work with local variables and if statements

一般来说,问题是:

详情:

我正在使用 bytebuddy 生成一些 类 的 equals 方法。为此,我使用了 net.bytebuddy.implementation.Implementation 的自定义实现。理论上,我计划生成的字节码应该具有几乎以下语义:

@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (obj == null) {
        return false;
    }
    if (getClass() != obj.getClass()) {
        return false;
    }
    final T other = (T) obj;
    if (this.a != other.a) {
        return false;
    }
    if (!Objects.equals(this.b, other.b)) {
        return false;
    }
    return true;
}

上面的代码有一个局部变量和几个if。我还没有找到他们两个的官方 StackManipulation,所以:

喜欢:

interface Branching extends StackManipulation {
    @Override
    default boolean isValid() {
        return true;
    }

    class Mark implements Branching {

        private final Label label;

        public Mark(Label label) {
            this.label = label;
        }

        @Override
        public final Size apply(MethodVisitor mv, Implementation.Context ctx) {
            mv.visitLabel(label);
            return new Size(0, 0);
        }
    }

    class IfNe implements Branching {

        private final Label label;

        public IfNe(Label label) {
            this.label = label;
        }

        @Override
        public final Size apply(MethodVisitor mv, Implementation.Context ctx) {
            mv.visitJumpInsn(Opcodes.IFNE, label);
            return new Size(-2, 0);
        }
    }
}

看来我做错了,因为生成的字节码缺少局部变量 table 和堆栈映射帧。而且当然没有通过验证,抱怨"Expecting a stackmap frame at branch target X".

更新:

我认为值得在这里添加一个例子。我说的初始案例很大,所以我重新写了一个模块来演示一个问题。它很大,但我无法想象如何让它变小:

package com.xxx.proba.bytebuddy;

import java.io.File;
import java.io.PrintStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.scaffold.InstrumentedType;
import net.bytebuddy.implementation.Implementation;
import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
import net.bytebuddy.implementation.bytecode.StackManipulation;
import net.bytebuddy.implementation.bytecode.collection.ArrayAccess;
import net.bytebuddy.implementation.bytecode.constant.IntegerConstant;
import net.bytebuddy.implementation.bytecode.constant.TextConstant;
import net.bytebuddy.implementation.bytecode.member.FieldAccess;
import net.bytebuddy.implementation.bytecode.member.MethodInvocation;
import net.bytebuddy.implementation.bytecode.member.MethodReturn;
import net.bytebuddy.implementation.bytecode.member.MethodVariableAccess;
import net.bytebuddy.jar.asm.Label;
import net.bytebuddy.jar.asm.MethodVisitor;
import net.bytebuddy.jar.asm.Opcodes;


/**
 * Assuming that I want to generate the class like this one
 * 
 * @author skapral
 */
class ExampleClass {
    public void main(String[] args) {
        if (args[0].equals("")) {
            System.out.println("a");
        } else {
            System.out.println("b");
        }
    }
}

public class Main {
    private final static Method EQUALS;
    private final static Method PRINTLN;
    private final static Field SYSTEM_OUT;

    static {
        try {
            EQUALS = Object.class.getMethod("equals", Object.class);
            PRINTLN = PrintStream.class.getMethod("println", String.class);
            SYSTEM_OUT = System.class.getField("out");
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    public static void main(String[] args) throws Exception {
        DynamicType.Unloaded<Object> unloaded = new ByteBuddy()
                .subclass(Object.class)
                .name("com.echelon.proba.bytebuddy.ExampleClassGenerated")
                .defineMethod("main", void.class, Visibility.PUBLIC)
                .withParameter(String[].class)
                .intercept(new Implementation() {
                    @Override
                    public ByteCodeAppender appender(Implementation.Target implementationTarget) {
                        return new ByteCodeAppender() {
                            @Override
                            public ByteCodeAppender.Size apply(MethodVisitor mv, Implementation.Context ctx, MethodDescription md) {
                                Label ifLabel = new Label();
                                Label elseLabel = new Label();

                                StackManipulation.Size size = new StackManipulation.Compound(
                                        MethodVariableAccess.REFERENCE.loadFrom(1),
                                        IntegerConstant.ZERO,
                                        ArrayAccess.REFERENCE.load(),
                                        new TextConstant(""),
                                        MethodInvocation.invoke(new MethodDescription.ForLoadedMethod(EQUALS)), 
                                        new IfEq(ifLabel),
                                        FieldAccess.forField(new FieldDescription.ForLoadedField(SYSTEM_OUT)).read(),
                                        new TextConstant("a"),
                                        MethodInvocation.invoke(new MethodDescription.ForLoadedMethod(PRINTLN)),
                                        new GoTo(elseLabel),
                                        new Mark(ifLabel),
                                        FieldAccess.forField(new FieldDescription.ForLoadedField(SYSTEM_OUT)).read(),
                                        new TextConstant("b"),
                                        MethodInvocation.invoke(new MethodDescription.ForLoadedMethod(PRINTLN)),
                                        new Mark(elseLabel),
                                        MethodReturn.VOID
                                ).apply(mv, ctx);
                                return new Size(size.getMaximalSize(), md.getStackSize());
                            }
                        };
                    }

                    @Override
                    public InstrumentedType prepare(InstrumentedType instrumentedType) {
                        return instrumentedType;
                    }
                })
                .make();
        unloaded.saveIn(new File("/tmp/aaa")); /* Preserve it for future investigation by javap */
        Object obj = unloaded.load(Main.class.getClassLoader()).getLoaded().newInstance();
        obj.getClass().getMethod("main", String[].class).invoke(obj, new String[] {"aaa"}); /* Trigger class loading and verification */
    }
}


class IfEq implements StackManipulation {
    private final Label label;

    public IfEq(Label label) {
        this.label = label;
    }

    @Override
    public boolean isValid() {
        return true;
    }

    @Override
    public StackManipulation.Size apply(MethodVisitor mv, Implementation.Context ctx) {
        mv.visitJumpInsn(Opcodes.IFEQ, label);
        return new StackManipulation.Size(-1, 0);
    }
}

class GoTo implements StackManipulation {
    private final Label label;

    public GoTo(Label label) {
        this.label = label;
    }

    @Override
    public boolean isValid() {
        return true;
    }

    @Override
    public StackManipulation.Size apply(MethodVisitor mv, Implementation.Context ctx) {
        mv.visitJumpInsn(Opcodes.GOTO, label);
        return new StackManipulation.Size(0, 0);
    }
}

class Mark implements StackManipulation {
    private final Label label;

    public Mark(Label label) {
        this.label = label;
    }

    @Override
    public boolean isValid() {
        return true;
    }

    @Override
    public StackManipulation.Size apply(MethodVisitor mv, Implementation.Context ctx) {
        mv.visitLabel(label);
        return new StackManipulation.Size(0, 0);
    }
}

在那个例子中,在 Main::main 方法中,我试图通过 bytebuddy 生成简单的 ExampleClass。在尝试加载它并调用方法时,我得到了 VerifyError。

  Location:
    com/echelon/proba/bytebuddy/ExampleClassGenerated.main([Ljava/lang/String;)V @8: ifeq
  Reason:
    Expected stackmap frame at this location.
  Bytecode:
    0x0000000: 2b03 3212 08b6 000c 9900 0eb2 0012 1214
    0x0000010: b600 1aa7 000b b200 1212 1cb6 001a b1  

    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.newInstance(Class.java:412)
    at com.xxx.proba.bytebuddy.Main.main(Main.java:103)

更新:总结一下:这个简单的 AsmVisitorWrapper 帮助了我:

public class EnableFramesComputing implements AsmVisitorWrapper {
    @Override
    public final int mergeWriter(int flags) {
        return flags | ClassWriter.COMPUTE_FRAMES;
    }

    @Override
    public final int mergeReader(int flags) {
        return flags | ClassWriter.COMPUTE_FRAMES;
    }

    @Override
    public final ClassVisitor wrap(TypeDescription td, ClassVisitor cv, Implementation.Context ctx, TypePool tp, FieldList<FieldDescription.InDefinedShape> fields, MethodList<?> methods, int wflags, int rflags) {
        return cv;
    }
}

可以通过在 DynamicType.Builder 上调用 visit 来实现它,例如:

DynamicType.Unloaded<Object> unloaded = new ByteBuddy()
            .subclass(Object.class)
            .visit(new EnableFramesComputing())
            ...

Byte Buddy 旨在成为一个高级字节码操作库。如果您想创建低级字节代码,您最有可能直接使用 ASM,这是一个很好的工具。

A​​SM 提供通过设置 COMPUTE_FRAMES 标志来计算堆栈映射帧。您可以通过注册 AsmVisitorWrapper 来设置标志,它只设置该标志而不注册包装器。

如果要创建自定义字节码,是否考虑过 Advice 组件?它允许您以普通方式编写代码 Java,其中字节代码在运行时内联并映射到适当的参数。