我将我的 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
方法恰好位于包含 premain
和 agentmain
的同一个类中> 方法,您希望将该方法(以及可能从外部调用的所有其他方法)分解到另一个实用程序类中。另外,不要直接从代理主类引用该类,而是首先让代理将其自己的 JAR 添加到启动类路径,然后通过反射而不是直接从代理主类调用其中的任何方法。代理主类完成工作后,其他类可以直接引用引导类路径中的类,问题就解决了。
但是仍然存在一个问题:代理如何找到要添加到引导类路径的 JAR 路径?那取决于你。您可以在命令行上设置系统属性,从文件中读取路径,进行硬编码,然后将其作为代理配置字符串通过 attachedVm.loadAgent( 传递给
(在本例中,premain/agentmain
) agentPath, configString)configString
再次包含代理路径)或其他内容。或者,创建一个内部 JAR 作为主代理 JAR 内的资源,其中包含要放在引导类加载器上的类。代理可以加载资源,将其保存到临时文件中,然后将临时文件路径添加到引导类路径。这有点复杂,但很干净,因此在代理开发人员中很受欢迎。有时这种方案被称为“蹦床代理”方法。
关于java - 检测代码后出现 NoClassDefFoundError,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63207481/