jvm-sandbox源码笔记之目标类增强

627 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天。点击查看活动详情

一、前言

前几篇文章主要讲了jvm-sandbox启动过程和加载模块过程,这一篇了解一下jvm-sandbox增强目标类的流程。

二、增强流程

想要知道代码是如何被增强的,需要知道增强的入口和链路。我门以官方的例子作为我们的自定义模块来观察它的增强过程。
官方的例子破损的时钟

MetaInfServices(Module.class)
@Information(id = "broken-clock-tinker")
public class BrokenClockTinkerModule implements Module {
    private final Logger logger = LoggerFactory.getLogger(BrokenClockTinkerModule.class);

    @Resource
    private ModuleEventWatcher moduleEventWatcher;

    @Command("repairCheckState")
    public void repairCheckState() {

        new EventWatchBuilder(moduleEventWatcher)
                .onClass("com.taobao.demo.Clock")
                .onBehavior("checkState")
                .onWatch(new AdviceListener() {
                    /**
                     * 拦截{@code com.taobao.demo.Clock#checkState()}方法,当这个方法抛出异常时将会被
                     * AdviceListener#afterThrowing()所拦截
                     */
                    @Override
                    protected void afterThrowing(Advice advice) throws Throwable {
                        // 在此,你可以通过ProcessController来改变原有方法的执行流程
                        // 这里的代码意义是:改变原方法抛出异常的行为,变更为立即返回;void返回值用null表示
                        ProcessController.returnImmediately(null);
                    }
                });
        
    }
}

我们将其打包放入~/.sandbox-moudle目录下,按照官方的说明执行/sandbox.sh -p {pid} -d 'broken-clock-tinker/repairCheckState'。观察sandbox的调用链路。\

  1. 执行命令首先由ModuleHttpServlet的doGet方法接受请求(这是我们前面讲到的由jetty启动的服务)
  2. doMethod根据传进来的id获取moudle并通过获取Command注解获取要执行的method对象,通过反射执行
  3. 执行broken-clock-tinker模块的repairCheckState方法
  4. 调用 事件观察者类构建器EventWatchBuilder的onBehavior和onWatch(就是官方例子中调用的方法)
  5. 最终调用DefaultModuleEventWatcher的watch方法。在此方法中完成字节码增强 (两张图是一个流程,只是跳出自定义模块之后就看不到了就截了两张图)

屏幕快照 2022-04-21 上午2.16.24.png

屏幕快照 2022-04-21 上午2.18.31.png

三、增强实现

在watch方法中是在哪里对加载的类进行增强呢? 其实就是这一行inst.addTransformer(sandClassFileTransformer, true) inst就是启动代理程序时由jvm传进来的Instrumentation实例, Instrumentation的addTransformer方法接受一个ClassFileTransformer的实例,那么接下来的类的加载都会经过这个ClassFileTransformer(就是上面的sandClassFileTransformer)。如果代理程序是热启动,在代理程序启动之前已经加载的类要怎么办呢?解决这个问题就是下面这个代码reTransformClasses(watchId,waitingReTransformClasses, progress)。 在reTransformClasses方法中又调用了inst.retransformClasses方法,这个方法的作用是对于已经加载的类重新进行转换处理,即会触发重新加载类。走完这两步目标类就被增强完成了。那么下一步咱们看下sandbox具体是如何进行操作的。
可以看下创建ClassFileTransformer的代码

// 给对应的模块追加ClassFileTransformer
final SandboxClassFileTransformer sandClassFileTransformer = new SandboxClassFileTransformer(
        watchId, coreModule.getUniqueId(), matcher, listener, isEnableUnsafe, eventType, namespace);

这里面传入了几个参数watchId是在后续的监听逻辑用到,暂时不用介绍coreModule.getUniqueId()是模块的id, matcher匹配器,觉得哪些类需要增强,listener监听器用于事件通知,isEnableUnsafe是否开启unsafe,开启unsafe之后sandbox能修改被BootstrapClassLoader所加载的类。eventType需要被监听的事件,namespace租户。
SandboxClassFileTransformer是实现了ClassFileTransformer的所有,当类被加载时,会通过 transform方法,这里就是增强的具体逻辑。

transform方法

阅读transform方法得知,transform做了两件事,第一过滤掉sandbox相关的类,第二 调用_transform
_transform方法

private byte[] _transform(final ClassLoader loader,
                          final String internalClassName,
                          final Class<?> classBeingRedefined,
                          final byte[] srcByteCodeArray) {
    // 如果未开启unsafe开关,是不允许增强来自BootStrapClassLoader的类
    if (!isEnableUnsafe
            && null == loader) {
        logger.debug("transform ignore {}, class from bootstrap but unsafe.enable=false.", internalClassName);
        return null;
    }

    final ClassStructure classStructure = getClassStructure(loader, classBeingRedefined, srcByteCodeArray);
    final MatchingResult matchingResult = new UnsupportedMatcher(loader, isEnableUnsafe).and(matcher).matching(classStructure);
    final Set<String> behaviorSignCodes = matchingResult.getBehaviorSignCodes();

    // 如果一个行为都没匹配上也不用继续了
    if (!matchingResult.isMatched()) {
        logger.debug("transform ignore {}, no behaviors matched in loader={}", internalClassName, loader);
        return null;
    }

    // 开始进行类匹配
    try {
        final byte[] toByteCodeArray = new EventEnhancer().toByteCodeArray(
                loader,
                srcByteCodeArray,
                behaviorSignCodes,
                namespace,
                listenerId,
                eventTypeArray
        );
        if (srcByteCodeArray == toByteCodeArray) {
            logger.debug("transform ignore {}, nothing changed in loader={}", internalClassName, loader);
            return null;
        }

        // statistic affect
        affectStatistic.statisticAffect(loader, internalClassName, behaviorSignCodes);

        logger.info("transform {} finished, by module={} in loader={}", internalClassName, uniqueId, loader);
        return toByteCodeArray;
    } catch (Throwable cause) {
        logger.warn("transform {} failed, by module={} in loader={}", internalClassName, uniqueId, loader, cause);
        return null;
    }
}

final MatchingResult matchingResult = new UnsupportedMatcher(loader, isEnableUnsafe).and(matcher).matching(classStructure)负责过滤出需要增强的类。这里使用了组合的matcher类对类进行matching配置。里面使用了各种判断来决定这个类是否需要被增强(是否是是sandbox的类/是否被启动类加载器加载/是否被Stealth注解修饰或者被Stealth修饰的类加载器加载等)
然后再调用代码增强器方法toByteCodeArray执行增强。

toByteCodeArray

@Override
public byte[] toByteCodeArray(final ClassLoader targetClassLoader,
                              final byte[] byteCodeArray,
                              final Set<String> signCodes,
                              final String namespace,
                              final int listenerId,
                              final Event.Type[] eventTypeArray) {
    // 返回增强后字节码
    final ClassReader cr = new ClassReader(byteCodeArray);
    final ClassWriter cw = createClassWriter(targetClassLoader, cr);
    final int targetClassLoaderObjectID = ObjectIDs.instance.identity(targetClassLoader);
    cr.accept(
            new EventWeaver(
                    ASM7, cw, namespace, listenerId,
                    targetClassLoaderObjectID,
                    cr.getClassName(),
                    signCodes,
                    eventTypeArray
            ),
            EXPAND_FRAMES
    );
    return dumpClassIfNecessary(cr.getClassName(), cw.toByteArray());
}

在toByteCodeArray方法中使用到了asm框架对字节码进行增强。首先构建ClassReader和ClassWriter。对对象分配一个唯一的ID。
在下面cr.accept是ClassReader接受了一个ClassVisitor的实现类,并按照顺序调用 ClassVisitor 中的方法。方法调用顺序如下

visit [visitSource] [visitModule] [visitNestHost][visitOuterClass](visitAnnotation | visitTypeAnnotation|visitAttribute)*\
(visitNestMember|visitInnerClass|visitField| visitMethod)* visitEnd;\
说明:visit,visitEnd必须调用一次,[]表示最多调用一次;\
()*表示()里面的访问可以按照排列顺序调用多次;

在EventWeaver中重写了visitMethod方法,增强逻辑会走到该方法中。在该方法的返回中,匿名继承了ReWriteMethod并重写了它的方法,然后返回了这个子类(ReWriteMethod是AdviceAdapter抽象类的具体实现类,AdviceAdapter继承了MethodVisitor)。最终返回的是MethodVisitor类型的对象。返回这个对象在ClassReader中调用了AdviceAdapter中的visitCode方法,visitCode方法又调用了onMethodEnter进行方法逻辑。最终走到上面说的返回的匿名子类中实现的AdviceAdapter的方法。
调用visitCode

// Visit the Code attribute.
if (codeOffset != 0) {
  methodVisitor.visitCode();
  readCode(methodVisitor, context, codeOffset);
}

visitCode方法

@Override
public void visitCode() {
  super.visitCode();
  if (isConstructor) {
    stackFrame = new ArrayList<>();
    forwardJumpStackFrames = new HashMap<>();
  } else {
    onMethodEnter();
  }
}

onMethodEnter方法

@Override
protected void onMethodEnter() {
    codeLockForTracing.lock(new CodeLock.Block() {
        @Override
        public void code() {
            mark(beginLabel);
            loadArgArray();
            dup();
            push(namespace);
            push(listenerId);
            loadClassLoader();
            push(targetJavaClassName);
            push(name);
            push(desc);
            loadThisOrPushNullIfIsStatic();
            invokeStatic(ASM_TYPE_SPY, ASM_METHOD_Spy$spyMethodOnBefore);
            swap();
            storeArgArray();
            pop();
            processControl();
            isMethodEnter = true;
        }
    });
}

可以看到在onMethodEnter里执行了一些方法,最终执行到了invokeStatic(ASM_TYPE_SPY, ASM_METHOD_Spy$spyMethodOnBefore),这里生成一个静态方法spyMethodOnBefore就是一个间谍方法。把代码植入到目标类中。最终由cw.toByteArray()生产修改后的字节码数组。
那么修改后的字节码长什么样子呢。

输出class文件

我们可以在以srcByteCodeArray == toByteCodeArray位置把字节码数组通过evaluate Erpression输出一下

屏幕快照 2022-04-25 上午12.37.00.png

byte[] toByteCodeArray = new byte[]{};
try {
    File file = new File("Users/myl/Desktop/1.class");
    file.createNewFile();
    FileOutputStream out = new FileOutputStream(file, true);
    out.write(toByteCodeArray);
    out.close();
} catch (IOException e) {
    e.printStackTrace();
}

得到class文件

final void checkState() {
    boolean var10000 = true;

    Object var5;
    int var6;
    try {
        Ret var10002 = Spy.spyMethodOnBefore(new Object[0], "default", 1051, 1052, "com.taobao.demo.Clock", "checkState", "()V", this);
        var6 = var10002.state;
        if (var6 != 1) {
            if (var6 != 2) {
                var10000 = true;
                throw new IllegalStateException("STATE ERROR!");
            } else {
                throw (Throwable)var10002.respond;
            }
        } else {
            var5 = var10002.respond;
        }
    } catch (Throwable var3) {
        boolean var10001 = true;
        Ret var4 = Spy.spyMethodOnThrows(var3, "default", 1051);
        var6 = var4.state;
        if (var6 != 1) {
            if (var6 != 2) {
                var10001 = true;
                throw var3;
            } else {
                throw (Throwable)var4.respond;
            }
        } else {
            var5 = var4.respond;
        }
    }
}

可以看到sandbox对破坏的时钟流程作出了改变植入了Ret var10002 = Spy.spyMethodOnBefore(xxx);Ret var4 = Spy.spyMethodOnThrows(var3, "default", 1051); 调用这个方法后就走到了sandbox内部Spy类的的处理流程,后面就是sandbox的监听处理逻辑。获得返回后,根据结果判断接下来的处理过程是抛异常还是什么都不做或者是返回 (例子是返回类型是void这里就是var5 = var10002.respond的逻辑,否则应该是 return var10002.respond;)

四、后记

sanbox增强过程是通过一些列调用最终走到asm处理增强的过程。在处理过程中有许多细节这里没有提到,比如在获取需要增强的类是如何通过组合matcher来过滤目标类,在过滤的过程中判断类是否来自于sandbox的类不被增强&是否是被启动类加载器加载的类根据配置判断是否可以被增强等等。另外对于asm增强逻辑这里没有提到因为我也没看太明白,只是介绍了一个整体的流程。