JVM-Sandbox 启动过程源码分析

JVM-Sandbox 启动有两种方式:ATTACHAGENT

AGENT 方式的入口

必须和服务一起启动,需要修改服务的启动命令,如:

java -javaagent:${HOME}/sandbox/lib/sandbox-agent.jar=server.port=8820;server.ip=0.0.0.0 
     -jar ${HOME}/.sandbox-module/repeater-bootstrap.jar

通过sandbox-agent.jar的pom文件;或者通过jar -xvf sandbox-agent.jar解压jar包,查看META-INF目录下的MANIFEST.MF文件我们可以获得程序入口。

pom 方式查看入口

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-assembly-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>attached</goal>
            </goals>
            <phase>package</phase>
            <configuration>
                <descriptorRefs>
                    <descriptorRef>jar-with-dependencies</descriptorRef>
                </descriptorRefs>
                <archive>
                    <manifestEntries>
                        <Premain-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Premain-Class>
                        <Agent-Class>com.alibaba.jvm.sandbox.agent.AgentLauncher</Agent-Class>
                        <Can-Redefine-Classes>true</Can-Redefine-Classes>
                        <Can-Retransform-Classes>true</Can-Retransform-Classes>
                    </manifestEntries>
                </archive>
            </configuration>
        </execution>
    </executions>
</plugin>

解压jar包方式查看入口

admin@wangyuhao lib % jar -xvf  sandbox-agent.jar       
  已创建: META-INF/
  已解压: META-INF/MANIFEST.MF
  已创建: com/
  已创建: com/alibaba/
  已创建: com/alibaba/jvm/
  已创建: com/alibaba/jvm/sandbox/
  已创建: com/alibaba/jvm/sandbox/agent/
  已解压: com/alibaba/jvm/sandbox/agent/SandboxClassLoader.class
  已解压: com/alibaba/jvm/sandbox/agent/AgentLauncher.class
admin@wangyuhao lib % cat META-INF/MANIFEST.MF 
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: admin
Build-Jdk: 1.8.0_281
Agent-Class: com.alibaba.jvm.sandbox.agent.AgentLauncher
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Premain-Class: com.alibaba.jvm.sandbox.agent.AgentLauncher

通过上述两种方式我们可以找到程序入口类是AgentLauncher,这种方式启动是调用的premain方法:

    /**
     * 启动加载
     *
     * @param featureString 启动参数
     *                      [namespace,prop]
     * @param inst          inst
     */
    public static void premain(String featureString, Instrumentation inst) {
        System.out.println("Sandbox 以Agent方式启动");
        LAUNCH_MODE = LAUNCH_MODE_AGENT;
        install(toFeatureMap(featureString), inst);
    }

这里的核心是install(toFeatureMap(featureString), inst);方法,主要作用是在当前JVM安装jvm-sandbox

ATTACH 方式的入口

即插即用的启动模式,可以在不重启目标JVM的情况下完成沙箱的植入。原理和GREYS、BTrace类似,利用了JVM的Attach机制实现,它是调用的agentmain方法。

这种方式加载稍微复杂一点,他的启动入口是执行sandbox命令,如:

./sandbox.sh -p `ps -ef | grep java | grep  com.alibaba.repeater.console.start.Application  | grep -v grep | awk  {print $2} `

ps -ef | grep java | grep com.alibaba.repeater.console.start.Application | grep -v grep | awk {print $2} 的作用是用来找到当前jvm程序的进程号,转换过来的命令是:./sandbox.sh -p 67672

深入sandbox.sh这个脚本可以发现其核心是attach_jvm函数。:

# attach sandbox to target JVM
# return : attach jvm local info
function attach_jvm() {

  # got an token
  local token
  token="$(date | head | cksum | sed  s/ //g )"

  # attach target jvm
  "${SANDBOX_JAVA_HOME}/bin/java" 
    ${SANDBOX_JVM_OPS} 
    -jar "${SANDBOX_LIB_DIR}/sandbox-core.jar" 
    "${TARGET_JVM_PID}" 
    "${SANDBOX_LIB_DIR}/sandbox-agent.jar" 
    "home=${SANDBOX_HOME_DIR};token=${token};server.ip=${TARGET_SERVER_IP};server.port=${TARGET_SERVER_PORT};namespace=${TARGET_NAMESPACE}" ||
    exit_on_err 1 "attach JVM ${TARGET_JVM_PID} fail."

  # get network from attach result
  SANDBOX_SERVER_NETWORK=$(grep "${token}" "${SANDBOX_TOKEN_FILE}" | grep "${TARGET_NAMESPACE}" | tail -1 | awk -F ";"  {print $3";"$4} )
  [[ -z ${SANDBOX_SERVER_NETWORK} ]] &&
    exit_on_err 1 "attach JVM ${TARGET_JVM_PID} fail, attach lose response."

}

attach_jvm函数第一会生成一个带时间戳信息的唯一token,然后使用java命令启动sandbox代理,最后获取attach结果的网络信息。核心是执行了java -jar sandbox-cor.jar然后传了三个参数,简化后的关键信息如下:

java -jar /home/admin/sandbox/lib/sandbox-core.jar  76092 "/home/admin/sandbox/lib/sandbox-agent.jar" "home=/home/admin/sandbox;token=2019091703032893;server.ip=0.0.0.0;server.port=12345;namespace=default" 

通过上述sandbox-agent.jar 查询入口类的方式,可以找到sandbox-cor.jar的入口类为:CoreLauncher,入口即为该类的main方法。

public static void main(String[] args) {
    try {

        // check args
        if (args.length != 3
                || StringUtils.isBlank(args[0])
                || StringUtils.isBlank(args[1])
                || StringUtils.isBlank(args[2])) {
            throw new IllegalArgumentException("illegal args");
        }

        new CoreLauncher(args[0], args[1], args[2]);
    } catch (Throwable t) {
        t.printStackTrace(System.err);
        System.err.println("sandbox load jvm failed : " + getCauseMessage(t));
        System.exit(-1);
    }
}

main方法通过解析接口传过来的三个参数,三个参数分别为:

  • targetJvmPid(PID(JVM进程ID)):76092。
  • agentJarPath(agent.jar全路径):/home/admin/sandbox/lib/sandbox-core.jar。
  • cfg(配置信息):home=/home/admin/sandbox;token=2019091703032893;server.ip=0.0.0.0;server.port=12345;namespace=default。

最后通过获取到的信息调用VirtualMachine.attach()方法,通过attach来执行agent.jar。

// 加载Agent
private void attachAgent(final String targetJvmPid,
                         final String agentJarPath,
                         final String cfg) throws Exception {
    
    VirtualMachine vmObj = null;
    try {
        vmObj = VirtualMachine.attach(targetJvmPid);
        if (vmObj != null) {
            vmObj.loadAgent(agentJarPath, cfg);
        }

    } finally {
        if (null != vmObj) {
            vmObj.detach();
        }
    }

}

Attach实现原理可以参考如下资料:

  • Java Attach机制简介: https://blog.csdn.net/u013332124/article/details/88362317
  • Java Attach源码解析:https://www.cnblogs.com/Jack-Blog/p/15026267.html

通过上述方法就调用了sandbox-agent.jar中的AgentLauncher类的agentmain方法:

/**
 * 动态加载
 *
 * @param featureString 启动参数
 *                      [namespace,token,ip,port,prop]
 * @param inst          inst
 */
public static void agentmain(String featureString, Instrumentation inst) {
    System.out.println("Sandbox 以ATTACH方式启动");
    LAUNCH_MODE = LAUNCH_MODE_ATTACH;
    final Map<String, String> featureMap = toFeatureMap(featureString);
    writeAttachResult(
            getNamespace(featureMap),
            getToken(featureMap),
            install(featureMap, inst)
    );
}

CoreLauncher类主要完成了sandbox-agent.jar的代理加载,核心方法还是 install(featureMap, inst)方法。

由此可见ATTACHAGENT两种启动方式最后都是调用的 install(featureMap, inst)子方法来完成sandbox的加载。

Agent 初始化过程(install方法)

在 install 方法中完成对 agent 的初始化,在初始化的过程中使用到了自定义的 SandboxClassLoader 对沙箱类进行加载,ModuleJarClassLoader 对./modele~/.sanbox-modele目录中module。jar进行加载,实现沙箱内部类与业务类隔离。

sandbox-agent.jar中的AgentLauncher类的install方法源码如下:

/**
 * 在当前JVM安装jvm-sandbox
 *
 * @param featureMap 启动参数配置
 * @param inst       inst
 * @return 服务器IP:PORT
 */
private static synchronized InetSocketAddress install(final Map<String, String> featureMap,
                                                      final Instrumentation inst) {

    final String namespace = getNamespace(featureMap);
    final String propertiesFilePath = getPropertiesFilePath(featureMap);
    final String coreFeatureString = toFeatureString(featureMap);

    try {
        final String home = getSandboxHome(featureMap);
        // 将Spy注入到BootstrapClassLoader
        inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(
                getSandboxSpyJarPath(home)
                // SANDBOX_SPY_JAR_PATH
        )));

        // 构造自定义的类加载器,尽量减少Sandbox对现有工程的侵蚀
        final ClassLoader sandboxClassLoader = loadOrDefineClassLoader(
                namespace,
                getSandboxCoreJarPath(home)
                // SANDBOX_CORE_JAR_PATH
        );

        // CoreConfigure类定义
        final Class<?> classOfConfigure = sandboxClassLoader.loadClass(CLASS_OF_CORE_CONFIGURE);

        // 反序列化成CoreConfigure类实例
        final Object objectOfCoreConfigure = classOfConfigure.getMethod("toConfigure", String.class, String.class)
                .invoke(null, coreFeatureString, propertiesFilePath);

        // CoreServer类定义
        final Class<?> classOfProxyServer = sandboxClassLoader.loadClass(CLASS_OF_PROXY_CORE_SERVER);

        // 获取CoreServer单例
        final Object objectOfProxyServer = classOfProxyServer
                .getMethod("getInstance")
                .invoke(null);

        // CoreServer.isBind()
        final boolean isBind = (Boolean) classOfProxyServer.getMethod("isBind").invoke(objectOfProxyServer);


        // 如果未绑定,则需要绑定一个地址
        if (!isBind) {
            try {
                classOfProxyServer
                        .getMethod("bind", classOfConfigure, Instrumentation.class)
                        .invoke(objectOfProxyServer, objectOfCoreConfigure, inst);
            } catch (Throwable t) {
                classOfProxyServer.getMethod("destroy").invoke(objectOfProxyServer);
                throw t;
            }

        }

        // 返回服务器绑定的地址
        return (InetSocketAddress) classOfProxyServer
                .getMethod("getLocal")
                .invoke(objectOfProxyServer);


    } catch (Throwable cause) {
        throw new RuntimeException("sandbox attach failed.", cause);
    }

}

核心流程:

  1. 通过Instrumentation调用BootstrapClassLoader去加载sandbox-spy.jarsandbox-spy.jar的主要作用是完成目标JVM和sandbox的通讯。
  2. 创建SandboxClassLoader,并通过该ClassLoader去加载sandbox-core.jar
  3. 通过SandboxClassLoader去加载CoreConfigure类,然后将所有沙箱配置映射赋值到该类的实例。
  4. 通过SandboxClassLoader去加载ProxyCoreServer类,并获得一个JettyCoreServer实例。
  5. 然后调用JettyCoreServerbind方法完成Spy的初始化(SpyUtils.init(cfg.getNamespace());)、HTTP 服务的初始化和启动、通过ModuleJarClassLoader加载所有mudule(jvmSandbox.getCoreModuleManager().reset();)。
  6. 最后,返回代理核心服务器JettyCoreServer的服务器绑定的地址。

JVM-Sandbox 启动过程源码分析

Spy 间谍类

install方法第一会通过Instrumentation 实例将 sandbox-spy.jar 添加到 BootstrapClassLoader 的搜索范围内。

// 将Spy注入到BootstrapClassLoader
inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(
        getSandboxSpyJarPath(home)
        // SANDBOX_SPY_JAR_PATH
)));

使用BootstrapClassLoader去加载spy的最要目的应该是保证Spy能增强所有的类,包括JDK自带的一些类,Spy的主要作用是完成目标JVM和sandbox间的通讯,sandbox会将方法的执行分为三个阶段BEFORE(方法执行前)、RETURN (方法返回)和 THROWS(方法异常) 三个环节。

// BEFORE
try {

   /*
    * do something...
    */

    // RETURN
    return;

} catch (Throwable cause) {
    // THROWS
}

基于BEFORERETURNTHROWS三个环节事件分离,沙箱的模块可以完成许多类AOP的操作。

  1. 可以感知和改变方法调用的入参
  2. 可以感知和改变方法调用返回值和抛出的异常
  3. 可以改变方法执行的流程
    • 在方法体执行之前直接返回自定义结果对象,原有方法代码将不会被执行
    • 在方法体返回之前重新构造新的结果对象,甚至可以改变为抛出异常
    • 在方法体抛出异常之后重新抛出新的异常,甚至可以改变为正常返回

要完成这些动作都是依赖Spy暴露出来的xxxOnBefore()xxxOnReturn()xxxOnThrows()等钩子函数来完成通讯,下图是官网提供的图:

JVM-Sandbox 启动过程源码分析

为了更加直观的看到代码增强后的效果,我在我的服务里面新写了下面一个测试类,并通过自带的debug-trace模块来查看spyEnhance方法的执行耗时,然后反编译JVM中内存中的class文件查看效果。

原始TestService类:

@Service("testService")
public class TestService {

    /**
     * 未增强方法
     *
     * @param name
     * @return
     */
    public String notEnhance(String name) {
        System.out.println("notEnhance");
        return "1";
    }
    
    /**
     * 增强方法
     *
     * @return
     */
    public String spyEnhance() {
        System.out.println("spyEnhance");
        return "1";
    }

}

通过命令监听spyEnhance方法:

./sandbox.sh -p `ps -ef | grep java | grep  com.alibaba.repeater.console.start.Application  | grep -v grep | awk  {print $2} `  -d  debug-trace/trace?class=com.alibaba.repeater.console.service.impl.TestService&method=spyEnhance 

然后反编译JVM 内存中TestClass类:

ClassLoader:                                                                                                                                                                                                                                                                                                       
+-sun.misc.Launcher$AppClassLoader@18b4aac2                                                                                                                                                                                                                                                                        
  +-sun.misc.Launcher$ExtClassLoader@3ec300f1                                                                                                                                                                                                                                                                      

Location:                                                                                                                                                                                                                                                                                                          
/Users/admin/Documents/workspace/jvm-sandbox-repeater/repeater-console/repeater-console-service/target/classes/                                                                                                                                                                                                    

       /*
        * Decompiled with CFR.
        * 
        * Could not load the following classes:
        *  java.com.alibaba.jvm.sandbox.spy.Spy
        *  java.com.alibaba.jvm.sandbox.spy.Spy$Ret
        */
       package com.alibaba.repeater.console.service.impl;
       
       import java.com.alibaba.jvm.sandbox.spy.Spy;
       import org.springframework.stereotype.Service;
       
       @Service(value="testService")
       public class TestService {
           public String notEnhance(String name) {
/*21*/         System.out.println("notEnhance");
/*22*/         return "1";
           }
       
           /*
            * Enabled aggressive block sorting
            * Enabled unnecessary exception pruning
            * Enabled aggressive exception aggregation
            */
           public String spyEnhance() {
               try {
                   Spy.Ret ret = Spy.spyMethodOnBefore((Object[])new Object[0], (String)"default", (int)1006, (int)1004, (String)"com.alibaba.repeater.console.service.impl.TestService", (String)"spyEnhance", (String)"()Ljava/lang/String;", (Object)this);
                   int n = ret.state;
                   if (n == 1) return (String)ret.respond;
                   if (n == 2) {
                       Spy.Ret ret2;
                       throw (Throwable)ret2.respond;
                   }
                   Spy.spyMethodOnCallBefore((int)31, (String)"java.io.PrintStream", (String)"println", (String)"(Ljava/lang/String;)V", (String)"default", (int)1006);
                   try {
                       System.out.println("spyEnhance");
                   }
                   catch (Throwable throwable) {
                       Spy.spyMethodOnCallThrows((String)throwable.getClass().getName(), (String)"default", (int)1006);
                       throw throwable;
                   }
                   Spy.spyMethodOnCallReturn((String)"default", (int)1006);
/*32*/             Spy.Ret ret3 = Spy.spyMethodOnReturn((Object)"1", (String)"default", (int)1006);
                   int n2 = ret3.state;
                   if (n2 == 1) return (String)ret3.respond;
                   if (n2 == 2) Spy.Ret ret4;
                   throw (Throwable)ret4.respond;
                   return "1";
               }
               catch (Throwable throwable) {
                   Throwable throwable2 = throwable;
                   Spy.Ret ret = Spy.spyMethodOnThrows((Throwable)throwable2, (String)"default", (int)1006);
                   int n = ret.state;
                   if (n == 1) return (String)ret.respond;
                   if (n == 2) throw (Throwable)ret.respond;
                   throw throwable2;
               }
           }
       }

Spy.spyMethodOnBefore源码如下:

public static Ret spyMethodOnBefore(final Object[] argumentArray,
                                    final String namespace,
                                    final int listenerId,
                                    final int targetClassLoaderObjectID,
                                    final String javaClassName,
                                    final String javaMethodName,
                                    final String javaMethodDesc,
                                    final Object target) throws Throwable {
    final Thread thread = Thread.currentThread();
    if (selfCallBarrier.isEnter(thread)) {
        return Ret.RET_NONE;
    }
    final SelfCallBarrier.Node node = selfCallBarrier.enter(thread);
    try {
        final SpyHandler spyHandler = namespaceSpyHandlerMap.get(namespace);
        if (null == spyHandler) {
            return Ret.RET_NONE;
        }
        return spyHandler.handleOnBefore(
                listenerId, targetClassLoaderObjectID, argumentArray,
                javaClassName,
                javaMethodName,
                javaMethodDesc,
                target
        );
    } catch (Throwable cause) {
        handleException(cause);
        return Ret.RET_NONE;
    } finally {
        selfCallBarrier.exit(thread, node);
    }
}

通过反编译的代码我们可以看出,通过注入的Spy.spyMethodOnBefore()方法作为sandbox的入口,然后调用了sandbox的事件分发处理器EventListener.onEvent()方法,sandbox通过Spy完成了目标JVM和sandbox的通讯,打开了链接两个世界的大门。

sandbox增强器EventEnhancer内部提供了输出增强类的方法,是否输出增强代码的开关isDumpClass=true需要手动开启,开关打开后,在自己项目的sandbox-class-dump目录可以查看到被增强的所有类,源码如下:

private static final boolean isDumpClass = true;

/*
 * dump class to file
 * 用于代码调试
 */
private static byte[] dumpClassIfNecessary(String className, byte[] data) {
    if (!isDumpClass) {
        return data;
    }
    final File dumpClassFile = new File("./sandbox-class-dump/" + className + ".class");
    final File classPath = new File(dumpClassFile.getParent());

    // 创建类所在的包路径
    if (!classPath.mkdirs()
            && !classPath.exists()) {
        logger.warn("create dump classpath={} failed.", classPath);
        return data;
    }

    // 将类字节码写入文件
    try {
        writeByteArrayToFile(dumpClassFile, data);
        logger.info("dump {} to {} success.", className, dumpClassFile);
    } catch (IOException e) {
        logger.warn("dump {} to {} failed.", className, dumpClassFile, e);
    }

    return data;
}

核心类的加载

命令执行原理

sandbox完成启动后,后续的所有命令的执行实则是直接访问的HTTP服务器,列如 ./sandbox.sh -p 7640 -l命令,最后执行的是 curl 命令,翻译过来是:curl -N -s "http://10.242.232.9:8820/sandbox/default/module/http/sandbox-module-mgr/list"

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
田园山水的头像 - 鹿快
评论 抢沙发

请登录后发表评论

    暂无评论内容