Why doesn't ASM call my ``visitCode``?

混江龙づ霸主 提交于 2019-12-24 00:58:54

问题


I'll add my code to the end of this post.

I'm using byteBuddy 1.7.9 and whatever ASM version comes with that.

In a nutshell

I have

byte[] rawClass = ...;
ClassReader cr = new ClassReader(rawClass);
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
MethodAdder ma = new MethodAdder(Opcodes.ASM5,cw);
cr.accept(ma,ClassReader.EXPAND_FRAMES);

Where in MethodAdder, I want to add a static initialiser:

@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
    MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
    if(mv != null){
        if(!name.equals(CLINIT_NAME)) return mv;
        else{
            hasStaticInitialiser = true;
            return new ClinitReplacer(api,mv,classname);
        }
    }else return null;
}

hasStaticInitialiser = true is reached, but ClinitReplacer.visitCode is never executed. Why?

the whole story

Let's say I want to generate class B from this example using byteBuddy.

Why bytebuddy? Well, for one it's supposedly convenient, and for another, I need its class reloading functionality.

But as you can see in the tutorial, there are some inconveniences with using "pure" byte buddy code. Most importantly,

if you really need to create byte code with jump instructions, make sure to add the correct stack map frames using ASM since Byte Buddy will not automatically include them for you.

I don't want to do that.

And even if I wanted to, I tried

builder = builder
        .defineMethod("<clinit>",void.class, Modifier.STATIC)
        .withParameters(new LinkedList<>())
        .withoutCode()
        ;

and all it got me was an

Exception in thread "main" java.lang.IllegalStateException: Illegal explicit declaration of a type initializer by class B
    at net.bytebuddy.dynamic.scaffold.InstrumentedType$Default.validated(InstrumentedType.java:901)
    at net.bytebuddy.dynamic.scaffold.MethodRegistry$Default.prepare(MethodRegistry.java:465)
    at net.bytebuddy.dynamic.scaffold.subclass.SubclassDynamicTypeBuilder.make(SubclassDynamicTypeBuilder.java:162)
    at net.bytebuddy.dynamic.scaffold.subclass.SubclassDynamicTypeBuilder.make(SubclassDynamicTypeBuilder.java:155)
    at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase.make(DynamicType.java:2639)
    at net.bytebuddy.dynamic.DynamicType$Builder$AbstractBase$Delegator.make(DynamicType.java:2741)
    at Main.main(Main.java)

So what I do instead is, I stop after I've added all my fields, get the bytecode for that and load the class.

Then I have ASM add the methods for me. ( In the actual application, I also need to run the bytecode through some other ASM visitors anyway.)

And then reload the re-instrumented bytecode using ByteBuddy.

The reloading fails with

Exception in thread "main" java.lang.ClassFormatError
    at sun.instrument.InstrumentationImpl.redefineClasses0(Native Method)
    at sun.instrument.InstrumentationImpl.redefineClasses(InstrumentationImpl.java:170)
    at net.bytebuddy.dynamic.loading.ClassReloadingStrategy$Strategy$1.apply(ClassReloadingStrategy.java:261)
    at net.bytebuddy.dynamic.loading.ClassReloadingStrategy.load(ClassReloadingStrategy.java:171)
    at Main.main(Main.java)

And the reason for that seems to be that B looks like this when disassembled:

super public class B
    extends A
    version 51:0
{

public static final Field foo:"Ljava/util/Set;";

public Method "<init>":"()V"
    stack 1 locals 1
{
        aload_0;
        invokespecial   Method A."<init>":"()V";
        return;
}

static Method "<clinit>":"()V";

} // end Class B

Comparing that to the rawClass bytecode, we notice that

static Method "<clinit>":"()V";

didn't exist and was indeed added by the MethodAdder.

However, the Visitor returned in

return new ClinitReplacer(api,mv,classname);

is never used. And therefore the static initialiser body remains empty resulting in the wrongful classification as native.

Code

A.java

import java.util.HashSet;
import java.util.Set;

public class A{
    public static final Set foo;
    static{
        foo = new HashSet<String>();
        foo.add("A");
    }
}

Main.java

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.dynamic.loading.ClassReloadingStrategy;
import net.bytebuddy.jar.asm.*;
import net.bytebuddy.jar.asm.commons.InstructionAdapter;

import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class Main {
    public static void main(String[] args) {
        ByteBuddyAgent.install();

        String targetClassname = "B";
        Class superclass = A.class;


        ByteBuddy byteBuddy = new ByteBuddy();

        DynamicType.Builder builder = byteBuddy
                .subclass(superclass)
                .name(targetClassname)
                ;

        for(Field f : superclass.getFields()){
            builder = builder.defineField(f.getName(),f.getType(),f.getModifiers());
        }

        DynamicType.Unloaded<?> loadable = builder.make();
        byte[] rawClass = loadable.getBytes();
        loadable.load(A.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION);

        ClassReader cr = new ClassReader(rawClass);
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        MethodAdder ma = new MethodAdder(Opcodes.ASM5,cw);
        cr.accept(ma,ClassReader.EXPAND_FRAMES);

        byte[] finishedClass = cw.toByteArray();

        Class unfinishedClass;
        try {
            unfinishedClass = Class.forName(targetClassname);
        }catch(ClassNotFoundException e){
            throw new RuntimeException(e);
        }

        ClassReloadingStrategy.fromInstalledAgent()
                .load(
                        A.class.getClassLoader(),
                        Collections.singletonMap((TypeDescription)new TypeDescription.ForLoadedType(unfinishedClass), finishedClass)
                );

        Set<String> result;
        try {
            result = (Set<String>)Class.forName("B").getField("foo").get(null);
        }catch(ClassNotFoundException | NoSuchFieldException | IllegalAccessException e){
            throw new RuntimeException(e);
        }
        System.out.println(result);
    }

    private static void store(String name, byte[] finishedClass) {
        Path path = Paths.get(name + ".class");
        try {
            FileChannel fc = null;
            try {
                Files.deleteIfExists(path);
                fc = new FileOutputStream(path.toFile()).getChannel();
                fc.write(ByteBuffer.wrap(finishedClass));
            } finally {
                if (fc != null) {
                    fc.close();
                }
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    static class MethodAdder extends ClassVisitor implements Opcodes{

        private static final String INIT_NAME       = "<init>";
        private static final String INIT_DESC       = "()V";

        private static final int CLINIT_ACCESS      = ACC_STATIC;
        private static final String CLINIT_NAME     = "<clinit>";
        private static final String CLINIT_DESC     = "()V";
        private static final String CLINIT_SIG      = null;
        private static final String[] CLINIT_EXCEPT = null;


        public MethodAdder(int api, ClassVisitor cv) {
            super(api, cv);
        }

        private String classname = null;
        private boolean hasStaticInitialiser = false;

        @Override
        public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
            classname = name;
            hasStaticInitialiser = false;
            cv.visit(version, access, name, signature, superName, interfaces);
        }

        @Override
        public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
            MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
            if(mv != null){
                if(!name.equals(CLINIT_NAME)) return mv;
                else{
                    hasStaticInitialiser = true;
                    return new ClinitReplacer(api,mv,classname);
                }
            }else return null;
        }

        @Override
        public void visitEnd() {
            if(!hasStaticInitialiser) visitMethod(CLINIT_ACCESS,CLINIT_NAME,CLINIT_DESC,CLINIT_SIG,CLINIT_EXCEPT);
            if(!hasStaticInitialiser) throw new IllegalStateException("ClinitReplacer not created");
            super.visitEnd();
        }

        private static class ClinitReplacer extends InstructionAdapter implements Opcodes{
            private final String classname;

            public ClinitReplacer(int api, MethodVisitor mv, String classname) {
                super(api, mv);
                this.classname = classname;
            }

            @Override
            public void visitCode() {
            mv.visitCode();
            InstructionAdapter mv = new InstructionAdapter(this.mv);

            mv.anew(Type.getType(HashSet.class));
            mv.dup();
            mv.dup();
            mv.invokespecial(Type.getInternalName(HashSet.class),INIT_NAME,INIT_DESC,false);
            mv.putstatic(classname,"foo",Type.getDescriptor(Set.class));
            mv.visitLdcInsn(classname);
            mv.invokevirtual(Type.getInternalName(HashSet.class),"add","(Ljava/lang/Object;)Z",false);
            mv.visitInsn(RETURN);
            }
        }
    }
}

回答1:


The problem is that your source class file doesn’t have a <clinit> method, hence, ASM doesn’t invoke visitMethod at all; it is you who does in

@Override
public void visitEnd() {
    if(!hasStaticInitialiser) visitMethod(CLINIT_ACCESS,CLINIT_NAME,CLINIT_DESC,CLINIT_SIG,CLINIT_EXCEPT);
    if(!hasStaticInitialiser) throw new IllegalStateException("ClinitReplacer not created");
    super.visitEnd();
}

Here, you are invoking visitMethod for <clinit> if you didn’t encounter it before, but you’re not doing anything with the returned MethodVisitor, hence, no-one is doing anything with it.

If you want to treat an absent <clinit> like visiting an empty initializer, to be transformed, you have to perform the appropriate method calls yourself, i.e.

@Override
public void visitEnd() {
    if(!hasStaticInitialiser) {
        MethodVisitor mv = visitMethod(CLINIT_ACCESS,CLINIT_NAME,CLINIT_DESC,CLINIT_SIG,CLINIT_EXCEPT);
        mv.visitCode();
        mv.visitInsn(RETURN);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }
    if(!hasStaticInitialiser) throw new IllegalStateException("ClinitReplacer not created");
    super.visitEnd();
}

But note that then, you can’t do hot code replacement, as it doesn’t support adding any methods, including <clinit>. Further, hot code replacement won’t (re-)execute class initializers anyway.

But in your code, there is no need to load the type before performing your ASM transformation. You may remove the line

loadable.load(A.class.getClassLoader(), ClassLoadingStrategy.Default.INJECTION);

and then just use the resulting finishedClass bytecode, e.g.

ClassLoadingStrategy.Default.INJECTION.load(A.class.getClassLoader(),
    Collections.singletonMap(loadable.getTypeDescription(), finishedClass));

Note that you won’t see much effect, as you are injecting code creating a HashMap, but not doing anything useful with it. You likely want to assign it to a field…

And, by the way, your code for writing a byte array is unnecessarily complicated:

private static void store(String name, byte[] finishedClass) {
    Path path = Paths.get(name + ".class");
    try {
        FileChannel fc = null;
        try {
            Files.deleteIfExists(path);
            fc = new FileOutputStream(path.toFile()).getChannel();
            fc.write(ByteBuffer.wrap(finishedClass));
        } finally {
            if (fc != null) {
                fc.close();
            }
        }
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

Just use

private static void store(String name, byte[] finishedClass) {
    Path path = Paths.get(name + ".class");
    try {
        Files.write(path, finishedClass);
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

Both, “create if it doesn’t exist” and “overwrite/truncate if it exists” are the default behavior.




回答2:


To answer the part about defining a type initializer in Byte Buddy, this can be done using:

builder = builder.invokable(isTypeInitializer()).intercept(...);

You cannot explicitly define a type initializer as those initializers are for example never exposed by the reflection API and this helps to keep Byte Buddy's type description model coherent. Instead, you match the type initializer and Byte Buddy makes sure that an intializer is added as it seems appropriate.



来源:https://stackoverflow.com/questions/48929791/why-doesnt-asm-call-my-visitcode

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!