简述
本文通过介绍 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 {
//...
}
}
}
- 函数 shouldInstrumentClass 通过判断当前要加载到虚拟机的类全名(包名+类名)来判断这个类是否需要插桩。
- 默认使用的调度算法就是
MCR_STRATEGY
我们只看这个算法下的插桩情况。 - 自定义 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 类
- 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);
}
- 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;
}
- 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
- visitMethodInsn 方法
当我们在函数中调用了另一个函数的时候,像这样:
void a(){
b() // 这里就是一个 INVOKE 字节码指令
}
void b(){
}
INVOKE 指令分为五类:
invokestatic:用于调用静态方法
invokespecial:用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法
invokevirtual:用于调用非私有实例方法
invokeinterface:用于调用接口方法
invokedynamic:用于调用动态方法
代码中对于其中的四类进行了处理(没有InvokeDynamic 个人猜测是因为jdk版本不够)
这个函数处理的部分主要是用来记录程序中线程相关函数的调用,例如 Thread
的 start()
、join()
方法和 Object
的 wait()
和 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方法
}
}
- visitFieldInsn、VisitInsn
其代码与 visitMethodInsn 类似,搞懂字节码指令背后的含义,在调用原字节码前后插入新的字节码执行。篇幅原因,不再详细展开。