BTrace系列之五:实现原理
原理简介 |
BTrace的实现原理其实早有博文阐述,详细见这里。
我在这里只所以在此进行阐述是想带着大家思考一下这些原理的背后,我们是否也能够运用这些原理去实现一些意想不到的事情,其实就是一个抛砖引玉的过程,希望大家能够多多进行讨论,搞出一些开源的好的工具也没准。
BTrace整个实现的原理是Java Agent+ASM+Java instrument+ Java Complier Api
首先来阐述一下各个原理:
Java Agent技术:
代理被部署为 JAR 文件。JAR 文件清单中的属性指定将被加载以启动代理的代理类。对于支持命令行接口的实现,可以在命令行指定一个选项来启动代理。实现也支持在 VM 启动后某一时刻启动代理的机制。例如,实现可以提供一种机制,允许某种工具连接 到运行的应用程序,并将该工具代理的加载初启到该应用程序中。如何初启加载的细节与实现有关。
对于使用命令行接口的实现,可以将以下选项添加到命令行来启动代理:
-javaagent:jarpath[=options]
jarpath 是代理 JAR 文件的路径。 options 是代理选项。此开关可以在同一代码行使用多次,从而创建多个代理。多个代理可以使用相同的 jarpath。代理 JAR 文件必须遵守 JAR 文件规范。
代理 JAR 文件的清单必须包含 Premain-Class 属性。此属性的值是代理类 的名称。代理类必须实现公共静态 premain 方法,该方法的原理与 main 应用程序入口点类似。在 Java 虚拟机 (JVM) 初始化后,每个 premain 方法将按照指定代理的顺序调用,然后将调用实际的应用程序 main 方法。每个 premain 方法必须按照依次进行的启动顺序返回。
premain 方法有两种可能的签名。JVM 首先尝试在代理类上调用以下方法:
public static void premain(String agentArgs, Instrumentation inst);
如果代理类没有实现此方法,那么 JVM 将尝试调用:
public static void premain(String agentArgs);
如果代理在 VM 启动之后启动,那么代理类还有一个要使用的 agentmain 方法。如果是使用命令行选项启动代理,那么 agentmain 方法将不会被调用。
代理类将被系统类加载器加载(参见 ClassLoader.getSystemClassLoader)。系统类加载器是通常加载包含应用程序 main 方法的类的类加载器。 premain 方法将在与应用程序 main 方法相同的安全性和类加载器规则下运行。不存在代理 premain 方法可以执行的建模限制。任何应用程序 main 可以执行的事情(包括创建线程)对于 premain 都是合法的。
每个代理通过 agentArgs 参数传递其代理选项。代理选项作为单个字符串传递,任何其他解析应由代理本身执行。
如果该代理不能被解析(例如,由于无法加载代理类,或者代理类没有恰当的 premain 方法),那么 JVM 将中止。如果 premain 方法抛出未捕获的异常,那么 JVM 将中止。
Premain-Class
如果 JVM 启动时指定了代理,那么此属性指定代理类,即包含 premain 方法的类。如果 JVM 启动时指定了代理,那么此属性是必需的。如果该属性不存在,那么 JVM 将中止。注:此属性是类名,不是文件名或路径。
Agent-Class
如果实现支持 VM 启动之后某一时刻启动代理的机制,那么此属性指定代理类。 即包含 agentmain 方法的类。 此属性是必需的,如果不存在,代理将无法启动。 注:这是类名,而不是文件名或路径。
Boot-Class-Path
由引导类加载器搜索的路径列表。路径表示目录或库(在许多平台上通常作为 JAR 或 zip 库被引用)。查找类的特定于平台的机制失败后,引导类加载器会搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分开。路径使用分层 URI 的路径组件语法。如果该路径以斜杠字符(“/”)开头,则为绝对路径,否则为相对路径。相对路径根据代理 JAR 文件的绝对路径解析。忽略格式不正确的路径和不存在的路径。如果代理是在 VM 启动之后某一时刻启动的,则忽略不表示 JAR 文件的路径。此属性是可选的。
Can-Redefine-Classes
布尔值(true 或 false,与大小写无关)。是否能重定义此代理所需的类。true 以外的值均被视为 false。此属性是可选的,默认值为 false。
Can-Retransform-Classes
布尔值(true 或 false,与大小写无关)。是否能重转换此代理所需的类。true 以外的值均被视为 false。此属性是可选的,默认值为 false。
Can-Set-Native-Method-Prefix
布尔值(true 或 false,与大小写无关)。是否能设置此代理所需的本机方法前缀。true 以外的值均被视为 false。此属性是可选的,默认值为 false。
Java instrument:
实现可以提供一种机制在 VM 启动之后某一时刻启动代理。如何初启的细节是特定于实现的,但通常应用程序已经启动并且其 main 方法已经调用。如果实现支持在 VM 启动之后启动代理,则以下内容适用:
代理 JAR 的清单必须包含属性 Agent-Class。此属性的值是代理类 的名称。代理类必须实现公共静态 agentmain 方法。系统类加载器(ClassLoader.getSystemClassLoader)必须支持将代理 JAR 文件添加到系统类路径的机制。代理 JAR 将被添加到系统类路径。系统类路径是通常加载包含应用程序 main 方法的类的类路径。代理类将被加载,JVM 尝试调用 agentmain 方法。JVM 首先尝试对代理类调用以下方法:
public static void agentmain(String agentArgs, Instrumentation inst);
如果代理类没有实现此方法,那么 JVM 将尝试调用:
public static void agentmain(String agentArgs);
如果代理是使用命令行选项启动的,那么代理类还有一个要使用的 agentmain 方法。如果是在 VM 启动之后启动代理,那么 agentmain 方法将不会被调用。
代理通过 agentArgs 参数传递其代理选项。代理选项作为单个字符串传递,任何其它解析应该由代理本身执行。
agentmain 方法应该执行任何启动代理所需的初始化。启动完成时该方法应返回。如果代理无法启动(例如,由于无法加载代理类,或者代理类没有构造 agentmain 方法),那么 JVM 将中止。如果 agentmain 方法抛出未捕获的异常,那么它将被忽略。
脚本解析引擎:
Java Complier Api:JVM允许我们动态传递.java文件,使用JVM编译java生成class。
代码实现 |
BTrace的jar包:
btrace-agent.jar
btrace-boot.jar
btrace-client.jar
三个jar对应了BTrace的工作原理中角色。
整个代码的入口处是:
Com.sun.btrace.client.Main,我们使用btrace <pid> xxx.java就是运行的这个类的main方法。
Main方法中主要做了如下四件事情:
1.创建一个客户端;
2.编译trace脚本;
3.动态attach一个agent;
4.创建客户端Socket与服务端Socket通信。
Client client = new Client(port, PROBE_DESC_PATH, DEBUG,
TRACK_RETRANSFORM, UNSAFE, DUMP_CLASSES, DUMP_DIR);
if (!new File(fileName).exists())
{
errorExit("File not found: " + fileName, 1);
}
// 进行编译,注意现在还没有使用ASM进行字节码修改,只是经过语法校验器的检查
byte[] code = client.compile(fileName, classPath, includePath);
if (code == null)
{
errorExit("BTrace compilation failed", 1);
}
// 动态的启动agent
client.attach(pid);
registerExitHook(client);
if (con != null)
{
registerSignalHandler(client);
}
if (isDebug())
debugPrint("submitting the BTrace program");
// 启动客户端Socket进行链接,将编译后的字节码和参数传输到服务端
client.submit(fileName, code, btraceArgs,
createCommandListener(client));
我们具体看下如何进行编译的
this.unsafe = unsafe;
//获取JVMTI的编译器
this.compiler = ToolProvider.getSystemJavaCompiler();
this.stdManager = compiler.getStandardFileManager(null, null, null);
其中com.sun.btrace.compiler.Compiler提供了Main方法,也可以单独进行编译BTrace脚本。
具体用法如下:
Usage: btracec <options> <btrace source files>
where possible options include:
-classpath <path> Specify where to find user class files and annotation processors
-cp <path> Specify where to find user class files and annotation processors
-I <path> Specify where to find include files
-d <directory> Specify where to place generated class files
// 使用verifier进行是BTrace脚本的语法校验器,进行例如no loop等校验
Verifier btraceVerifier = new Verifier(unsafe);
BTrace的安全性检查是通过Verifier来实现的,规范可参照JSR 269。只要将该类重写就可以突破BTrace的安全限制,不过请慎重,这样可能会影响到目标进程。
ClassReader cr = new ClassReader(classBytes.get(name));
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
cr.accept(new Postprocessor(cw), ClassReader.EXPAND_FRAMES
+ ClassReader.SKIP_DEBUG);
在经过编译之后,使用ASM进行了一次字节码的修改,我们看下这个在Client端的字节码修改是在做什么操作?
其实客户端并没有对编译好的脚本的字节码进行修改,只是针对其做了一些兼容性,例如,域访问控制器、简写。
到目前为止整个编译过程就进行完了。
接下来该介绍Agent技术了。
VirtualMachine vm = null;
vm = VirtualMachine.attach(pid);
vm.loadAgent(agentPath, agentArgs);
这里可以看到就是通过这几行代码,从BTrace的客户端,触发了目标JVM上的agent启动,这样就正式从。
BTrace Agent分析:
public static void premain(String args, Instrumentation inst) {
main(args, inst);
}
public static void agentmain(String args, Instrumentation inst) {
main(args, inst);
}
Main:方法中所做的事情就是:
1.添加boot classpath
inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(path)));
inst.appendToSystemClassLoaderSearch(new JarFile(new File(path)));
2.启动服务端Socket
ServerSocket ss;
try {
if (isDebug()) debugPrint("starting server at " + port);
System.setProperty("btrace.port", String.valueOf(port));
if (scriptOutputFile != null && scriptOutputFile.length() > 0) {
System.setProperty("btrace.output", scriptOutputFile);
}
ss = new ServerSocket(port);
} catch (IOException ioexp) {
ioexp.printStackTrace();
return;
}
3. 注册提供的class转换器。
inst.addTransformer(clInitTransformer, false);
inst.addTransformer(this, true);
4.ASM进行字节码修改,并进行类转换。
Client implements ClassFileTransformer
public byte[] transform(
ClassLoader loader,
String cname,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classfileBuffer)
throws IllegalClassFormatException {
private byte[] instrument(Class clazz, String cname, byte[] target) {
byte[] instrumentedCode;
try {
ClassWriter writer = InstrumentUtils.newClassWriter(target);
ClassReader reader = new ClassReader(target);
Instrumentor i = new Instrumentor(clazz, className, btraceCode, onMethods, writer);
InstrumentUtils.accept(reader, i);
if (Main.isDebug() && !i.hasMatch()) {
Main.debugPrint("*WARNING* No method was matched for class " + cname); // NOI18N
}
instrumentedCode = writer.toByteArray();
} catch (Throwable th) {
Main.debugPrint(th);
return null;
}
Main.dumpClass(className, cname, instrumentedCode);
return instrumentedCode;
}