2

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();
                                  
Nfff3
  • 321
  • 8
  • 24
  • You can try to do `ClassLoader.getSystemClassLoader().loadClass("AgentClass")`. – Johannes Kuhn Aug 01 '20 at 20:17
  • @JohannesKuhn, I've tried that as well, but the error messages are the same :( – Nfff3 Aug 01 '20 at 21:56
  • 1
    Please show your ClassVisitor. – Johannes Kuhn Aug 01 '20 at 23:33
  • 2
    Your question is unclear. Asking about ASM problems without showing any ASM code is not such a helpful approach. And I mean complete classes, not just snippets or pseudo code. I have at least three ideas in mind why what you are trying to do might fail. But this is SO and not a quiz show, so I do not want to guess or speculate. Instead, I would rather like to analyse your situation and specifically answer your question. I think you agree that this is what you want, too: answers, not guesses. – kriegaex Aug 02 '20 at 05:21
  • @kriegaex, added the code. – Nfff3 Aug 02 '20 at 12:07
  • 2
    Okay, thanks. Before I look at the ASM code in detail, another question. Why would you call `Class.forName()` after the call failed already? What good would that do? It does not seem to make any logical sense. If it is meant to solve the class-loading problem, it is not the right way to solve it. Another question is: Is `AgentClass` really in the default package, not something like `my.package.name.AgentClass`? – kriegaex Aug 03 '20 at 00:14

1 Answers1

3

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:

  • via -javaagent:/path/to/your/agent.jar or
  • via hot-attachment during runtime (if so, please show the code)

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.

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • `Xbootclasspath/a:` is working fine when I attach my agent during load time with `-javaagent`, but I'd like to attach my agent via hot-attachment during runtime(updated question with the code) and I will be happy if a solution exists for that as well – Nfff3 Aug 03 '20 at 17:53
  • Are you attaching to the local VM or to another one? I am asking because the solution for the local case is simpler than the generic one. – kriegaex Aug 04 '20 at 00:42
  • I am attaching to a VM on the same host but not self-attaching to the same VM that executes the attaching code. – Nfff3 Aug 04 '20 at 07:59
  • Sorry for answering slowly, I was quite busy. I updated my answer. – kriegaex Aug 05 '20 at 00:40
  • Thank you very much, hot attachment also works now. The only thing which doesn't work is when I try to instrument one of the classes in my own agent, if these classes contain references to some other classes outside of the jar and not on the boostrap classpath(like `List` or `ArrayList`), it throws `NullPointerException` in `ASM`'s `SymbolTable` when it recalculates the Stack Frames....But I think this is expected behavior, so I will somehow refactor my classes to only contain necessary logic which can be instrumented without errors. – Nfff3 Aug 05 '20 at 20:38
  • 1
    Why would you develop a self-instrumenting agent? Maybe your agent does too much. I recommend a separation of concerns. Without further explanation I cannot recommend a concrete approach, but I am sure your problem can be solved by fixing your application and/or agent design. Make sure not to run into circularity problems or race conditions and you should be fine. FYI, from this point on I will not answer any more questions without an [MCVE](https://stackoverflow.com/help/mcve). – kriegaex Aug 06 '20 at 02:21