检测由 bootstrap / extension class loader 加载的 classes 的正确方法是什么?

What is the proper way to instrument classes loaded by bootstrap / extension class loader?

我终于用 Byte Buddy 写了一个 Java 代理,它使用 Advice API 在进入和离开方法时打印消息。使用我当前的配置,此代理似乎仅适用于由 Application ClassLoader 加载的 classes。

但是,我希望它也适用于由任何 classloader 加载的 classes。我遇到过多种似乎不起作用的技术(参见 enableBootstrapInjection() or ignore())。事实上,enableBootstrapInjection() 已经从 ByteBuddy 中消失了,ignore() 方法让我的 JVM 恐慌,因为我相信我有循环问题,比如尝试检测 java.lang.instrument class(但是这个似乎不是唯一的问题,我找不到列出这些错误的方法)。

这是我的代理的简化版本:

AgentBuilder mybuilder = new AgentBuilder.Default()
        .ignore(nameStartsWith("net.bytebuddy."))
        .disableClassFormatChanges()
        .with(RedefinitionStrategy.RETRANSFORMATION)
        .with(InitializationStrategy.NoOp.INSTANCE)
        .with(TypeStrategy.Default.REDEFINE);
mybuilder.type(nameMatches(".*").and(not(nameMatches("^src.Agent")))) // to prevent instrumenting itself
        .transform((builder, type, classLoader, module) -> {
            try {
                return builder
                .visit(Advice.to(TraceAdvice.class).on(isMethod()));
                } catch (SecurityException e) {
                    e.printStackTrace();
                    return null;
                }
            }
        ).installOn(inst);
System.out.println("Done");

以及我的建议的简化版本 class,如有必要:

public class TraceAdvice {
    @Advice.OnMethodEnter
    static void onEnter(
        @Origin Method method,
        @AllArguments(typing = DYNAMIC) Object[] args
    ) {
        System.out.println("[+]");
    }

    @Advice.OnMethodExit
    static void onExit() {
        System.out.println("[-]");
    }
}

例如,我意识到检测 java.io.PrintStream.println 的循环依赖性,我可以忽略这些方法(例如第 7 行的 .and(not(nameMatches("^java.io.PrintStream"))))。

以下是激活日志记录并获得有用的日志输出的方法。我还向您展示了如何手动重新转换已加载的 bootstrap class。 Bootstrap class安装transformer后加载的es,会自动进行transform,在下面的日志中也可以看到。

import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;

import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.lang.reflect.Method;
import java.util.Properties;

import static net.bytebuddy.agent.builder.AgentBuilder.RedefinitionStrategy.RETRANSFORMATION;
import static net.bytebuddy.matcher.ElementMatchers.*;

class ByteBuddyInstrumentBootstrapClasses {
  public static void main(String[] args) throws UnmodifiableClassException {
    Instrumentation instrumentation = ByteBuddyAgent.install();
    installTransformer(instrumentation);

    // Use already loaded bootstrap class 'Properties'
    System.out.println("Java version: " + System.getProperties().getProperty("java.version"));
    // Retransform already loaded bootstrap class 'Properties'
    instrumentation.retransformClasses(Properties.class);
    // Use retransformed bootstrap class 'Properties' (should yield advice output)
    System.out.println("Java version: " + System.getProperties().getProperty("java.version"));
  }

  private static void installTransformer(Instrumentation instrumentation) {
    new AgentBuilder.Default()
      .disableClassFormatChanges()
      .with(RETRANSFORMATION)
      // Make sure we see helpful logs
      .with(AgentBuilder.RedefinitionStrategy.Listener.StreamWriting.toSystemError())
      .with(AgentBuilder.Listener.StreamWriting.toSystemError().withTransformationsOnly())
      .with(AgentBuilder.InstallationListener.StreamWriting.toSystemError())
      .ignore(none())
      // Ignore Byte Buddy and JDK classes we are not interested in
      .ignore(
        nameStartsWith("net.bytebuddy.")
          .or(nameStartsWith("jdk.internal.reflect."))
          .or(nameStartsWith("java.lang.invoke."))
          .or(nameStartsWith("com.sun.proxy."))
      )
      .disableClassFormatChanges()
      .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
      .with(AgentBuilder.InitializationStrategy.NoOp.INSTANCE)
      .with(AgentBuilder.TypeStrategy.Default.REDEFINE)
      .type(any())
      .transform((builder, type, classLoader, module) -> builder
        .visit(Advice.to(TraceAdvice.class).on(isMethod()))
      ).installOn(instrumentation);
  }

  public static class TraceAdvice {
    @Advice.OnMethodEnter
    static void onEnter(@Advice.Origin Method method) {
      // Avoid '+' string concatenation because of https://github.com/raphw/byte-buddy/issues/740
      System.out.println("[+] ".concat(method.toString()));
    }

    @Advice.OnMethodExit
    static void onExit(@Advice.Origin Method method) {
      // Avoid '+' string concatenation because of https://github.com/raphw/byte-buddy/issues/740
      System.out.println("[-] ".concat(method.toString()));
    }
  }
}

控制台日志:

[Byte Buddy] BEFORE_INSTALL net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer$ByteBuddy$ModuleSupport@2a54a73f on sun.instrument.InstrumentationImpl@16a0ee18
[Byte Buddy] TRANSFORM com.sun.tools.attach.VirtualMachine [jdk.internal.loader.ClassLoaders$AppClassLoader@2626b418, module jdk.attach, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM ByteBuddyInstrumentBootstrapClasses [jdk.internal.loader.ClassLoaders$AppClassLoader@2626b418, unnamed module @4e07b95f, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM com.intellij.rt.execution.application.AppMainV2 [jdk.internal.loader.ClassLoaders$AppClassLoader@2626b418, unnamed module @4e07b95f, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM com.intellij.rt.execution.application.AppMainV2 [jdk.internal.loader.ClassLoaders$AppClassLoader@2626b418, unnamed module @4e07b95f, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM com.intellij.rt.execution.application.AppMainV2$Agent [jdk.internal.loader.ClassLoaders$AppClassLoader@2626b418, unnamed module @4e07b95f, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM sun.text.resources.cldr.ext.FormatData_de [jdk.internal.loader.ClassLoaders$PlatformClassLoader@7203c7ff, module jdk.localedata, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM sun.util.resources.provider.LocaleDataProvider [jdk.internal.loader.ClassLoaders$PlatformClassLoader@7203c7ff, module jdk.localedata, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM sun.util.resources.provider.NonBaseLocaleDataMetaInfo [jdk.internal.loader.ClassLoaders$PlatformClassLoader@7203c7ff, module jdk.localedata, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM sun.util.resources.cldr.provider.CLDRLocaleDataMetaInfo [jdk.internal.loader.ClassLoaders$PlatformClassLoader@7203c7ff, module jdk.localedata, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM java.util.Formattable [null, module java.base, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM java.util.Formatter$Conversion [null, module java.base, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM java.util.Formatter$Flags [null, module java.base, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM java.util.Formatter$FormatSpecifier [null, module java.base, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM java.util.Formatter$FixedString [null, module java.base, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM java.util.Formatter$FormatString [null, module java.base, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM java.util.regex.ASCII [null, module java.base, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM java.util.regex.IntHashSet [null, module java.base, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM java.util.regex.Matcher [null, module java.base, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM java.util.regex.MatchResult [null, module java.base, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM java.lang.CharacterData00 [null, module java.base, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM java.lang.StringUTF16$CharsSpliterator [null, module java.base, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM java.util.stream.IntPipeline [null, module java.base, Thread[main,5,main], loaded=true]
[Byte Buddy] TRANSFORM java.util.stream.Sink$ChainedInt [null, module java.base, Thread[main,5,main], loaded=true]
[Byte Buddy] INSTALL net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer$ByteBuddy$ModuleSupport@2a54a73f on sun.instrument.InstrumentationImpl@16a0ee18
Java version: 14.0.2
[Byte Buddy] TRANSFORM java.util.Properties [null, module java.base, Thread[main,5,main], loaded=true]
[+] public java.lang.String java.util.Properties.getProperty(java.lang.String)
[-] public java.lang.String java.util.Properties.getProperty(java.lang.String)
Java version: 14.0.2
[Byte Buddy] TRANSFORM java.util.IdentityHashMap$IdentityHashMapIterator [null, module java.base, Thread[main,5,main], loaded=false]
[Byte Buddy] TRANSFORM java.util.IdentityHashMap$KeyIterator [null, module java.base, Thread[main,5,main], loaded=false]
[+] public boolean java.util.IdentityHashMap$IdentityHashMapIterator.hasNext()
[-] public boolean java.util.IdentityHashMap$IdentityHashMapIterator.hasNext()
[+] public java.lang.Object java.util.IdentityHashMap$KeyIterator.next()
[+] protected int java.util.IdentityHashMap$IdentityHashMapIterator.nextIndex()
[-] protected int java.util.IdentityHashMap$IdentityHashMapIterator.nextIndex()
[-] public java.lang.Object java.util.IdentityHashMap$KeyIterator.next()
[+] public boolean java.util.IdentityHashMap$IdentityHashMapIterator.hasNext()
[-] public boolean java.util.IdentityHashMap$IdentityHashMapIterator.hasNext()
[Byte Buddy] TRANSFORM java.lang.Shutdown [null, module java.base, Thread[DestroyJavaVM,5,main], loaded=false]
[Byte Buddy] TRANSFORM java.lang.Shutdown$Lock [null, module java.base, Thread[DestroyJavaVM,5,main], loaded=false]
[+] static void java.lang.Shutdown.shutdown()
[+] private static void java.lang.Shutdown.runHooks()
[-] private static void java.lang.Shutdown.runHooks()
[-] static void java.lang.Shutdown.shutdown()

请注意 System.getProperties().getProperty("java.version") 的第一次调用如何不产生建议日志记录,但重新转换后的第二次调用会产生建议日志记录。


在查看您的 GitHub 存储库后更新

我理解正确吗?模块 launcher 尝试将模块 agent 动态附加到另一个已经 运行 的 JVM。这看起来很复杂。您是否尝试使用 -javaagent:/path/to/agent.jar 参数启动另一个 JVM?稍后您仍然可以尝试其他策略。但无论哪种方式,请注意您的代理 classes AgentCompleteSTE 不会像这样在引导 class 路径上。

考虑到建议代码将被内联到目标 classes(还有 bootstrap classes),这意味着 bootstrap classes 需要能够在引导 classpath 上找到建议代码引用的所有 classes。有两种方法可以实现:

  1. 除了-javaagent:/path/to/agent.jar之外,将-Xbootclasspath/a:/path/to/agent.jar添加到目标JVM命令行。这当然只有在您对目标 JVM 的命令行有影响的情况下才有效。动态连接到任何 运行 JVM 都无法以这种方式工作,因为您来不及指定引导 class 路径选项。

  2. 将实际代理划分为“跳板代理”和另一个包含您的建议代码引用的 classes 的 JAR。额外的 JAR 可以打包在代理 JAR 中作为资源或驻留在文件系统的某个位置,具体取决于您的解决方案的通用性。跳板代理会

  • 可选择将额外的 JAR 解压到一个临时位置(如果嵌套在 springboard 代理中),
  • 通过调用方法 Instrumentation::appendToBootstrapClassLoaderSearch(JarFile)
  • 将附加 JAR 动态添加到引导 class 路径
  • 确保不要直接引用附加 JAR 中的任何 classes,但如果有的话,只能在 JAR 已经在引导 classpath 上后通过反射。想想Class.forName(..).getMethod(..).invoke(..).

顺便说一句,如果 Byte Buddy (BB) 建议引用的 classes 使用 BB API 本身,您还需要将 BB 本身放在引导 class 路径上.所有这些都不是微不足道的,因此您想尝试避免这种情况。在试图弄清楚如何最好地实现我的专用模拟工具时,我经历了所有这些 Sarek


更新 2: 我在 this GitHub fork.

中优化并大规模重组了 OP 的原始存储库

enableBootstrapInjection 方法已替换为允许多种注入策略的通用 API。之前的策略仍然可以通过 InjectionStrategy uses instrumentation 获得。这主要是对当前 Unsafe 风格的反应,因为 JVM 将在内部 APIs 关闭。

如您所说,您需要优化您的忽略匹配器以允许来自引导加载程序的某些 classes。您按名称忽略的 class 越多越好。建议是此类 class 的正确方法,因为您只能添加代码而不能更改任何 class.

的形状

如前所述,不需要将 Byte Buddy 放在引导路径上。事实上,通知方法只是模板,它们的代码将被复制粘贴到目标方法中。因此,您无权访问这些建议中的任何字段或其他方法 classes.