javainstrumentationjava-bytecode-asmjavaagentsbytecode-manipulation

NoClassDefFoundError after instrumenting code


I'm attaching my Java agent dynamically to a java process which instruments the code. Basically it adds a static call to every start of method:

//method start   
AgentClass.staticMethod();  
//method body  

AgentClass lies in the agent's .jar. But after instrumentation, the process starts executing the new code and it throws a NoClassDefFoundError, it cannot find AgentClass. I tried to istrument the classes in a way to include a try-catch block and load AgentClass with forName like this:

try {
    AgentClass.staticMethod();
} catch(NoClassDefFoundError e) {
    Class.forName("AgentClass");
}

But then I got several errors related to the recalculation of stack frames like: Caused by: java.lang.VerifyError: Inconsistent stackmap frames at branch target 20 I solved this by using visitMaxs() (I am using ASM library).Then I got this: StackMapTable error: bad offset. This was solved by using GOTO instead of RETURN but then I got: ClassFormatError: Illegal local variable table in method.

Is there any easier way to solve my initial NoClassDefFoundError error?

UPDATE: My agent classes are loaded with the Application Classloader(sun.misc.Launcher$AppClassLoader), and the process which I wanted to instrument loads classes with a custom URL classloader.

UPDATE2: This is what I wanted to transform into bytecode:

 try {
        AgentClass agent = AgentClass.staticMethod();
     } catch (Throwable e) {
        try {
           Class.forName("AgentClass");
        } catch (ClassNotFoundException ex) {
     }
   }

My MethodVisitor(I am not very good at bytecode, so the bytecode was generated automatically by ASM, using a TraceClassVisitor.):

protected MethodVisitor createVisitor(MethodVisitor mv,final String name,final String desc,int access,String signature,String[]exceptions){
        int variablesCount = (8 & access) != 0 ? 0 : 1;
        Type[]args=Type.getArgumentTypes(desc);
       
        for(int i=0;i<args.length; ++i){
        Type arg=args[i];
        variablesCount+=arg.getSize();
        }

        final int varCount=variablesCount;


        return new MethodVisitor(458752,mv){
public void visitCode(){
        Label label0=new Label();
        Label label1=new Label();
        Label label2=new Label();
        this.mv.visitTryCatchBlock(label0,label1,label2,"java/lang/Throwable");
        Label label3=new Label();
        Label label4=new Label();
        Label label5=new Label();
        this.mv.visitTryCatchBlock(label3,label4,label5,"java/lang/ClassNotFoundException");
        this.mv.visitLabel(label0);
        this.mv.visitLineNumber(42,label0);
        this.mv.visitMethodInsn(Opcodes.INVOKESTATIC,"AgentClass","staticMethod","()LAgentClass;",false);
        this.mv.visitVarInsn(Opcodes.ASTORE,varCount);
        this.mv.visitLabel(label1);
        this.mv.visitLineNumber(48,label1);
        Label label6=new Label();
        this.mv.visitJumpInsn(Opcodes.GOTO,label6);
        this.mv.visitLabel(label2);
        this.mv.visitLineNumber(43,label2);
        this.mv.visitFrame(Opcodes.F_SAME1,0,null,1,new Object[]{"java/lang/Throwable"});
        this.mv.visitVarInsn(Opcodes.ASTORE,0);
        this.mv.visitLabel(label3);
        this.mv.visitLineNumber(45,label3);
        this.mv.visitLdcInsn("AgentClass");
        this.mv.visitMethodInsn(Opcodes.INVOKESTATIC,"java/lang/Class","forName","(Ljava/lang/String;)Ljava/lang/Class;",false);
        this.mv.visitInsn(Opcodes.POP);
        this.mv.visitLabel(label4);
        this.mv.visitLineNumber(47,label4);
        this.mv.visitJumpInsn(Opcodes.GOTO,label6);
        this.mv.visitLabel(label5);
        this.mv.visitLineNumber(46,label5);
        this.mv.visitFrame(Opcodes.F_FULL,1,new Object[]{"java/lang/Throwable"},1,new Object[]{"java/lang/ClassNotFoundException"});
        this.mv.visitVarInsn(Opcodes.ASTORE,1);
        this.mv.visitLabel(label6);
        this.mv.visitLineNumber(49,label6);
        this.mv.visitFrame(Opcodes.F_CHOP,1,null,0,null);
        this.mv.visitInsn(Opcodes.RETURN);
        this.mv.visitLocalVariable("e","Ljava/lang/Throwable;",null,label3,label6,0);
        this.mv.visitMaxs(1, 2);
        
        super.visitCode();
        }
        ...
        }
        }

UPDATE 3 This is how I attach my agent during runtime:

final VirtualMachine attachedVm = VirtualMachine.attach(String.valueOf(processID));
attachedVm.loadAgent(pathOfAgent, argStr);
attachedVm.detach();
                                  

Solution

  • For now my guess is that your class loader hierarchy is something like:

    boot class loader
      platform class loader
        system/application class loader
        custom URL class loader
    

    Or maybe:

    boot class loader
      platform class loader
        system/application class loader
      custom URL class loader
    

    I.e. the application class loader and the custom URL class loader are siblings or in some other way in different parts of the class loader hierarchy, i.e. the classes loaded in one of them are unknown to the other one.

    The way to solve this would be to find a common ancestor and make sure the classes needed for your instrumentation scheme are loaded there. I usually use the bootstrap class loader. Before I explain to you how to programmatically add classes to the bootstrap class loader, please try adding your agent JAR to the bootstrap class path manually on the Java command line via -Xbootclasspath/a:/path/to/your/agent.jar and see if the custom URL class loader then finds the class. I would be very surprised if that would not work. Then please report back and we can continue.

    Please also explain how you attach the instrumentation agent:


    Update after some clarifying OP comments:

    It is possible to add a JAR (not single classes) to the bootstrap class path by calling method Instrumentation.appendToBootstrapClassLoaderSearch(JarFile). In your agent's premain or (for hot-attachment) agentmain methods the JVM passes you an Instrumentation instance you can use for that purpose.

    Caveat: You need to add the JAR before any of the classes you need on the bootstrap classpath have been imported or used by other, already loaded classes (including the agent class itself). So if in your case the AgentClass method called by the other class in the sibling class loader happens to reside inside the same class housing the premain and agentmain methods, you want to factor that method (and all others that might be called from outside) into another utility class. Also, do not directly refer to that class from the agent main class, rather first make the agent add its own JAR to the boot class path and then call any methods in there via reflection rather than directly from the agent main class. After the agent main class has done its job, other classes can refer to the classes that are now on the bootstrap class path directly, the problem is solved.

    One problem remains, though: How does the agent find out the JAR path to add to the bootstrap class path? That is up to you. You can set a system property on the command line, read the path from a file, hard-code, hand it over as an agent configuration string passed to premain/agentmain via attachedVm.loadAgent(agentPath, configString) (in this case configString containing the agent path again) or whatever. Alternatively, create an inner JAR as a resource inside the main agent JAR, containing the classes to be put on the bootstrap class loader. The agent can load the resource, save it into a temporary file and then add the temp-file path to the bootstrap class path. This is a bit complicated, but clean and thus quite popular among agent developers. Sometimes this scheme is referred to as a "trampoline agent" approach.