JMCR 字节码插桩(二)

简述

本文通过介绍 JMCR 中一些类的部分具体来描述 JMCR 中的插装流程。

提纲

  • Premain
  • Instrumentor
  • ClassAdapter
  • MethodAdaptor

系列文章:
1. JMCR 简介
2. JMCR 字节码插桩(一)
3. JMCR 字节码插桩(二)
4. JMCR 约束求解原理
5. JMCR 线程调度

JMCR 字节码插桩(一)为基础,本章来探讨一下 JMCR 具体如何对多线程程序进行插桩。

一、Premain 类

结合官网给出的运行命令:

usage: ./mcr_cmd [options] test_class [parameters]
  e.g., ./mcr_cmd [options] edu.tamu.aser.rvtest_simple_tests.Example

观察根目录下 mcr_cmd 脚本的内容:

#...
# functions
# $1 -- test class
function runTests()
{
    echo "Running tests from: ${class_name}"    

    java -ea -javaagent:libs/agent.jar \
    #....
}

找到根目录下的 libs/agent.jar 文件,解压后查看 MANIFEST.MF

Manifest-Version: 1.0
Premain-Class: edu.tamu.aser.instrumentation.Instrumentor
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Created-By: 1.8.0_101 (Oracle Corporation)

二、Instrumentor 类

找到位于 mcr-instrumentor 模块下的Instrumentor类,找到关键的 transform 函数

inst.addTransformer(new ClassFileTransformer() {
public byte[] transform(ClassLoader loader,
                        String className,
                        Class<?> classBeingRedefined,
                        ProtectionDomain protectionDomain,
                        byte[] classfileBuffer) throws IllegalClassFormatException {
    //...
    /*
    * filter out the library classes
    */
    if (shouldInstrumentClass(className)) { // 1
        if (debug)
        {
            System.out.println("Instrumenting " + className);
        }

        ClassReader classReader = new ClassReader(classfileBuffer);
        ClassWriter classWriter = new ExtendedClassWriter(classReader, ClassWriter.COMPUTE_FRAMES);

        if (MCR_STRATEGY.equals(strategy)) { // 2
            ClassAdapter classVisitor = new ClassAdapter(classWriter);// 3

            //in the accept method, it calls visitor.visit()
            classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
            classfileBuffer = classWriter.toByteArray();
        } else { 
            //...
        }
    }
}
  1. 函数 shouldInstrumentClass 通过判断当前要加载到虚拟机的类全名(包名+类名)来判断这个类是否需要插桩。
  2. 默认使用的调度算法就是 MCR_STRATEGY 我们只看这个算法下的插桩情况。
  3. 自定义 ClassAdapter 随后讲解

我们不妨将修改后的字节码在 transform 函数返回之前用文件保存下来,便于我们查看:

  File dir = new File("out");
  if (!dir.exists()) {
      dir.mkdir();
  }

  File classFile = new File(dir, className.replace("/", ".") + ".class");
  System.out.println("class file path: " + classFile.getAbsolutePath());
  classFile.createNewFile();
  System.out.println("Writing " + className);
  byte[] code = classWriter.toByteArray();
  FileOutputStream fos = new FileOutputStream(classFile);
  fos.write(code);
  fos.close();

三、ClassAdapter 类

  1. visit 方法
    记录了一些类的元信息
public void visit(int version,
                  int access, String name,
                  String signature,
                  String superName,
                  String[] interfaces) {
    super.visit(version, access, name, signature, superName, interfaces);
    // 记录当前插桩类名
    this.classname = name;
    // 记录是否是线程或者线程的子类
    isThreadClass = RVGlobalStateForInstrumentation.instance.isThreadClass(classname);
    // 记录这个类或者其父类是否实现了 Runnable 接口
    isRunnableClass = RVGlobalStateForInstrumentation.instance.isRunnableClass(classname);
}
  1. visitField 方法
    对于类里面每一个变量(静态、非静态),都会调用一边visitField。
public FieldVisitor visitField(int access, String name, String desc,
        String signature, Object value) 
{
    String sig_var = (classname+"."+name).replace("/", ".");
    // RVGlobalStateForInstrumentation 是一个记录运行时全局状态的一个类
    // 在 RVGlobalStateForInstrumentation.variableIdMap<String, Integer> 中以(classname+variableName,id)对儿记录了变量(id 从0开始自增)。
    RVGlobalStateForInstrumentation.instance.getVariableId(sig_var);
    if((access & Opcodes.ACC_VOLATILE)!=0)
    { 
        // 同理记录所有 volatile 变量
        RVGlobalStateForInstrumentation.instance.addVolatileVariable(sig_var);
    }
        
    if (cv != null) {
        return cv.visitField(access, name, desc, signature, value);
    }
    return null;
}
  1. visitMethod 方法
    对于类中的所有方法都会调用一次 visitMethod,这里访问到的还是方法的元信息,方法中的字节码规则还需通过返回自定义的 MethodAdapter。
public MethodVisitor visitMethod(int access, String name, String desc,
        String signature, String[] exceptions) {       

    // 这一步很重要,不然这个方法就会消失
    MethodVisitor mv = cv.visitMethod(access&(~Opcodes.ACC_SYNCHRONIZED),
            name, desc, signature, exceptions);

    if (mv != null) {
        boolean isSynchronized = false;
        boolean isStatic = false;

        // 记录一些方法元信息,例如是否为 synchronized
        if((access & Opcodes.ACC_SYNCHRONIZED)!=0)
            isSynchronized = true;
        if((access & Opcodes.ACC_STATIC)!=0)
            isStatic = true;

        // 记录这个方法签名是不是 void run()
        boolean possibleRunMethod = name.equals(RUN_METHOD_NAME) && !isStatic
                && (Type.getArgumentsAndReturnSizes(desc) >> 2) == 1 &&
                Type.getReturnType(desc).equals(Type.VOID_TYPE) &&
                (isThreadClass||isRunnableClass);
 
       //自定义 MethodAdapter 对于方法中的字节码进行修改
        mv = new MethodAdapter(mv,
                source, 
                access, 
                desc, 
                classname,
                name,name+desc,name.equals("<init>")||name.equals("<clinit>"), 
                isSynchronized,
                isStatic,
                possibleRunMethod);
    }
    return mv;
}

四、MethodAdaptor

  1. visitMethodInsn 方法
    当我们在函数中调用了另一个函数的时候,像这样:
void a(){
    b() // 这里就是一个 INVOKE 字节码指令
}
void b(){
    
}

INVOKE 指令分为五类:

invokestatic:用于调用静态方法
invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法
invokevirtual:用于调用非私有实例方法
invokeinterface:用于调用接口方法
invokedynamic:用于调用动态方法

代码中对于其中的四类进行了处理(没有InvokeDynamic 个人猜测是因为jdk版本不够)
这个函数处理的部分主要是用来记录程序中线程相关函数的调用,例如 Threadstart()join() 方法和 Objectwait()notify()

    public void visitMethodInsn(int opcode, String owner, String name, String desc) {

        // 记录了当前函数的唯一标识符
        String sig_loc = source + "|" + (className+"|"+methodSignature+"|"+line_cur).replace("/", ".");
        // 根据标识符获取 ID
        int ID  = RVGlobalStateForInstrumentation.instance.getLocationId(sig_loc);

        switch (opcode) {
        case INVOKEVIRTUAL:
            // 如果当前方法所处的类是 Thread 或其子类
            if(RVGlobalStateForInstrumentation.instance.isThreadClass(owner))
                if(name.equals("start") &&desc.equals("()V")) {
                    
                    //Some Optimizations...

  
                    //下面几行在将参数压进栈中
                    //局部变量表的最大索引
                    maxIndexCur++;
                    int index = maxIndexCur;
                    //复制栈顶(thread对象)
                    mv.visitInsn(DUP);
                    //将栈顶的值存入局部变量表下标为 index 的地方
                    mv.visitVarInsn(ASTORE, index);
                    //把当前函数对应的全局编号压入栈顶
                    addBipushInsn(mv,ID);
                    //把局部变量下标为 index 的对象压入栈顶
                    mv.visitVarInsn(ALOAD, index);
                    
                   
                    //调用RVRunTime.logBeforeStart(Integer i, Object o)方法,无返回值
                    mv.visitMethodInsn(INVOKESTATIC, logClass /*edu/tamu/aser/runtime/RVRunTime*/,
                            RVConfig.instance.LOG_THREAD_BEFORE_START/*logBeforeStart*/,
                            RVConfig.instance.DESC_LOG_THREAD_START/*(ILjava/lang/Object;)V  表示这是一个参数列表为(Integer ,Object) 返回类型为Void的函数*/);

                    mv.visitMethodInsn(opcode, owner, name, desc); // 调用原函数 
                } else {
                    // 插桩 join() 方法,或者其他函数直接调用  mv.visitMethodInsn(opcode, owner, name, desc)
                }
        } else { // 当前方法所处的类不是 Thread 或其子类
                // 插桩 wait、notify、notifyAll方法和 ReentrantLock 的 lock、unlock、lockInterruptibly 方法
        }

        case INVOKESTATIC: // 插桩Thread.sleep() 方法
        case INVOKESPECIAL:
        case INVOKEINTERFACE: // 插桩 Lock.lock、unlock方法     
        }

    }
  1. visitFieldInsn、VisitInsn
    其代码与 visitMethodInsn 类似,搞懂字节码指令背后的含义,在调用原字节码前后插入新的字节码执行。篇幅原因,不再详细展开。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。