java - 检测代码后出现 NoClassDefFoundError

标签 java instrumentation java-bytecode-asm javaagents bytecode-manipulation

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

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

AgentClass 位于代理的 .jar 中。但在检测后,进程开始执行新代码并抛出 NoClassDefFoundError,它找不到 AgentClass。 我尝试以包含 try-catch block 的方式对类进行测试,并使用 forName 加载 AgentClass,如下所示:

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

但是后来我遇到了一些与重新计算堆栈帧相关的错误,例如: 由以下原因引起:java.lang.VerifyError:分支目标 20 处的堆栈映射帧不一致 我通过使用 visitMaxs() 解决了这个问题(我正在使用 ASM 库)。然后我得到了这个:StackMapTable 错误:错误的偏移量。 这是通过使用 GOTO 而不是 RETURN 解决的,但后来我得到:ClassFormatError:方法中的非法局部变量表

有没有更简单的方法来解决我最初的 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();
                                  

最佳答案

现在我的猜测是你的类加载器层次结构类似于:

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

即应用程序类加载器和自定义 URL 类加载器是同级的,或者以某种其他方式位于类加载器层次结构的不同部分,即其中一个加载的类对于另一个而言是未知的。

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

另请解释如何附加仪器代理:

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

在澄清一些 OP 评论后进行更新:

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

警告:您需要在引导类路径上所需的任何类被其他已加载的类(包括代理类本身)导入或使用之前添加 JAR。因此,如果在您的情况下,兄弟类加载器中的另一个类调用的 AgentClass 方法恰好位于包含 premainagentmain 的同一个类中> 方法,您希望将该方法(以及可能从外部调用的所有其他方法)分解到另一个实用程序类中。另外,不要直接从代理主类引用该类,而是首先让代理将其自己的 JAR 添加到启动类路径,然后通过反射而不是直接从代理主类调用其中的任何方法。代理主类完成工作后,其他类可以直接引用引导类路径中的类,问题就解决了。

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

关于java - 检测代码后出现 NoClassDefFoundError,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63207481/

相关文章:

java - 使用 Unsafe.defineClass 在运行时定义多个类

java - 如何使用检测打印 Java 运行时调用的所有方法?

Java 字节码 asm - 如何创建仅更改类名的类的克隆?

java - 如何在使用 asm 库进行检测的方法中查找空的局部变量

java - 哪种加密算法可用于加密存储在磁盘上的文件?

java - 从 IIB 插入 Oracle DB 时出错

java - Spring MVC Hibernate 错误 java.lang.Object;无法转换为 model.Employee

java - 如何在 AppEngine 数据存储区中存储单个实例对象

Android Studio 无法正确导入 androidx 测试类

java - 使用 java asm 获取函数参数值以进行字节码检测