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 类似,搞懂字节码指令背后的含义,在调用原字节码前后插入新的字节码执行。篇幅原因,不再详细展开。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,045评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,114评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,120评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,902评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,828评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,132评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,590评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,258评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,408评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,335评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,385评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,068评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,660评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,747评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,967评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,406评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,970评论 2 341