检测代码后出现 NoClassDefFoundError

NoClassDefFoundError after instrumenting code

我正在将我的 Java 代理动态附加到检测代码的 java 进程。基本上它为方法的每个开始添加了一个静态调用:

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

AgentClass在于代理的.jar。但是在检测之后,进程开始执行新代码并抛出 NoClassDefFoundError,找不到 AgentClass。 我尝试以一种包含 try-catch 块的方式对 类 进行测试,并像这样用 forName 加载 AgentClass

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

但后来我遇到了几个与重新计算堆栈帧相关的错误,例如: Caused by: java.lang.VerifyError: Inconsistent stackmap frames at branch target 20 我通过使用 visitMaxs() 解决了这个问题(我正在使用 ASM 库)。然后我得到了这个:StackMapTable error: bad offset。 这是通过使用 GOTO 而不是 RETURN 解决的,但后来我得到了:ClassFormatError: Illegal local variable table in method.

有没有更简单的方法来解决我最初的 NoClassDefFoundError 错误?

更新:我的代理 类 加载了应用程序类加载器(sun.misc.Launcher$AppClassLoader),并且我想要检测的进程加载 类 使用自定义 URL 类加载器。

更新2: 这就是我想转换成字节码的内容:

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

我的MethodVisitor(我不太擅长字节码,所以字节码是由ASM自动生成的,使用了一个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();
        }
        ...
        }
        }

更新 3 这就是我在运行时附加代理的方式:

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

目前我的猜测是您的 class 加载程序层次结构类似于:

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

或者也许:

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

即应用程序 class 加载器和自定义 URL class 加载器是同级或以其他方式位于 class 加载器层次结构的不同部分,即 classes其中一个加载的是另一个不知道的。

解决这个问题的方法是找到一个共同的祖先,并确保在那里加载了您的检测方案所需的 classes。我通常使用 bootstrap class 加载程序。在我向您解释如何以编程方式将 classes 添加到 bootstrap class 加载程序之前,请尝试将您的代理 JAR 手动添加到 bootstrap class 路径Java 命令行通过 -Xbootclasspath/a:/path/to/your/agent.jar 并查看自定义 URL class 加载器是否找到 class。如果那行不通,我会感到非常惊讶。然后请反馈,我们可以继续。

还请说明您是如何附加检测代理的:

  • 通过-javaagent:/path/to/your/agent.jar
  • 在运行时通过热连接(如果是,请显示代码)

在澄清 OP 评论后更新:

可以通过调用方法 Instrumentation.appendToBootstrapClassLoaderSearch(JarFile) 将 JAR(不是单个 classes)添加到 bootstrap class 路径。在您代理的 premain 或(对于热连接)agentmain 方法中,JVM 会向您传递一个 Instrumentation 实例,您可以将其用于该目的。

警告:您需要在 bootstrap class 路径上需要的任何 classes 被导入或被其他人使用之前添加 JAR,已经加载 classes(包括代理 class 本身)。因此,如果在您的情况下,同级 class 加载程序中的另一个 class 调用的 AgentClass 方法恰好位于容纳 premainagentmain 方法,您想将该方法(以及所有其他可能从外部调用的方法)分解为另一个实用程序 class。另外,不要直接从代理 main class 引用那个 class,而是先让代理将自己的 JAR 添加到引导 class 路径,然后通过反射调用其中的任何方法而不是直接来自代理主 class。 agent mainclass完成后,其他classes可以直接引用bootstrapclass路径上的classes,问题解决了。

但仍然存在一个问题:代理如何找到 JAR 路径以添加到 bootstrap class 路径?那取决于你。您可以在命令行上设置系统 属性,从文件中读取路径,硬编码,将其作为代理配置字符串通过 attachedVm.loadAgent(agentPath, configString) 传递给 premain/agentmain(在此case configString 再次包含代理路径)或其他。或者,创建一个内部 JAR 作为主代理 JAR 内的资源,其中包含要放在 bootstrap class 加载程序上的 classes。代理可以加载资源,将其保存到临时文件中,然后将临时文件路径添加到 bootstrap class 路径中。这有点复杂,但很干净,因此在代理开发人员中很受欢迎。有时这种方案被称为“蹦床代理”方法。