Java 字节码检测:对 defineClass 的反射调用中的 NullPointerException

Posted

技术标签:

【中文标题】Java 字节码检测:对 defineClass 的反射调用中的 NullPointerException【英文标题】:Java Bytecode Instrumentation: NullPointerException in reflective call to defineClass 【发布时间】:2013-03-19 12:39:35 【问题描述】:

意图

我正在使用java.lang.instrument 包为Java 程序创建一些工具。这个想法是我通过这个系统使用字节码操作,以便在每个方法的开头和结尾添加方法调用。一般来说,修改后的 Java 方法如下所示:

public void whateverMethod()
    MyFancyProfiler.methodEntered("whateverMethod");
    //the rest of the method as usual...
    MyFancyProfiler.methodExited("whateverMethod");

MyFancyProfiler 是一个相对复杂系统的入口点,该系统在premain 方法(它是java.lang.instrument 的一部分)期间被初始化。

edit - MyFancyProfiler 包含一个静态 API,它将通过类似于 in the solution to this question 描述的机制获得对系统其余部分的引用。引用以Object 的形式获得,并通过反射进行适当的调用,因此即使当前的 ClassLoader 不知道底层类,它仍然可以工作。

困难

对于一个简单的 Java 程序,该方法可以正常工作。对于“真正的”应用程序(如窗口应用程序,尤其是 RCP/OSGi 应用程序),我遇到了ClassLoaders 的问题。有些ClassLoaders 不知道如何找到MyFancyProfiler 类,所以当它尝试调用MyFancyProfiler 中的静态方法时会抛出异常。

我对此(以及我真正的问题发生的地方)的解决方案目前是通过反射调用defineClassMyFancyProfiler“注入”到每个遇到的ClassLoader 中。它的要点是:

public byte[] transform(ClassLoader loader, String className, /* etc... */) 
  if(/* this is the first time I've seen loader */)
    //try to look up `MyFancyProfiler` in `loader`.
    if(/* loader can't find my class */)
      // get the bytes for the MyFancyProfiler class
      // reflective call to 
      // loader.defineClass(
      //   "com.foo.bar.MyFancyProfiler", classBytes, 0, classBytes.length);
    
  
  // actually do class transformation via ASM bytecode manipulation

编辑以获取更多信息 - 这种注入的原因是为了确保每个类,无论是哪个 ClassLoader 加载它,都能够直接调用 MyFancyProfiler.methodEntered。一旦它进行了调用,MyFancyProfiler 将需要使用反射与系统的其余部分进行交互,否则当它尝试直接引用时我会得到一个 InvocationTargetException 或 NoClassDef 异常。我目前正在使用它,以便MyFancyProfiler 的唯一“直接”依赖项是 JRE 系统类,所以它似乎没问题。

问题

这甚至有效!大多数时候!但是对于我在尝试跟踪 Eclipse(从命令行启动 IDE)时遇到的至少两个单独的 ClassLoader,我从 ClassLoader.defineClass 方法内部得到了一个 NullPointerException

java.lang.NullPointerException
    at java.lang.ClassLoader.checkPackageAccess(ClassLoader.java:500)
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:791)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:634)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:601)
    // at my code that calls the reflection

ClassLoader.java 的第 500 行是对 domains.add(pd) 的调用,其中 domains 似乎是在构造函数时初始化的 Set,pdProtectionDomain (据我所知)应该是“默认”ProtectionDomain。所以我看不出那条线引起NullPointerException的明显方式。目前我很困惑,我希望有人可以对此提供一些见解。

什么可能导致defineClass 以这种方式失败?如果没有明显的解决方案,您能否提供一种潜在的替代方法来解决我的整体问题?

【问题讨论】:

这是一个研究项目?如果没有,为什么不使用 AspectJ? 是否使用 -javaagent 标志插入了您的分析器,以及定义的 premain? 见 [***.com/questions/6750392/… @DiogoSantana 这是为了工作。我已经重命名了一些东西以保持模糊。我们从 AspectJ 开始,但发现它存在这个问题以及更多问题。可以说我们不能也不会使用 AspectJ。 @AmirAfghani 是的,除了目标跟踪应用程序使用许多不同类加载器的这种相当具体的情况外,一切正常。在我遇到问题的情况下,我正在尝试跟踪 Eclipse,它为它加载的每个“捆绑包”提供了一个新的 ClassLoader。我当前的解决方案可能适用于 90% 的捆绑包,但会产生我提到的其余 10% 的异常。 【参考方案1】:

您将获得NullPointerException,因为在这种情况下this 为空。如果你想使用引导类加载器(即null)加载一个类,你需要使用sun.misc.Unsafe.defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain); 方法绕过安全检查,null 作为 ClassLoader,null 作为 ProtectionDomain。

【讨论】:

【参考方案2】:

尝试在引导类加载器中加载包含 MyFancyProfiler 的 jar,而不是向 ClassLoaders 注入代码。最简单的方法是将以下行添加到 javaagent jar 的清单中:

Boot-Class-Path: fancy-profiler-bootstrap-stuff.jar

这将使所有类加载器都可以访问该 jar 中的所有内容,我相信包括 OSGi 和朋友。

【讨论】:

终于回去试试这个,果然奏效了!非常感谢!

以上是关于Java 字节码检测:对 defineClass 的反射调用中的 NullPointerException的主要内容,如果未能解决你的问题,请参考以下文章

[Java安全]利用TemplatesImpl执行字节码

字节码引用检测原理与实战

检测 Java 字节码中的循环 - 区分后端类型

一句话木马该怎么实现?现在就带你了解

javasec类加载机制

ASM 字节码插桩:隐私合规方法检测