阿尔萨斯 原理探究 仿写arthas-redefine

前言

arthas 可以通过 redefine 命令可以在不重启的情况下对正在运行的class文件进行变更。这篇主要是探究该原理并实现一下简易版的i_redefine的功能。整体实现逻辑:编写 agentmain ,为 Instrumentation 添加自定义Transformer,对加载后的class进行转换。将编写的 agentmain 打包成 Jar ,利用Attach API 发送指令到目标虚拟机,通知它进行loadAgent 。 使用到的技术栈:(Java agent(Java SE 6 )+ Java attach API+ asm)。

期间有一个问题困扰比较长:有什么方式知道一个class文件中对应的类的包名和类名?

看了arthas的源码才知道可以通过中 asm 的ClassReader方法实现。

agentMain

主要是为inst添加Transformer,并通知重新加载目标类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class IAgentMain {

public static void agentmain(String agentArgs, Instrumentation inst)
throws Exception {
// 注意不能仅仅根据路径来确定类名,而是要通过包中的路径+类名!!!
String path = agentArgs;
String targetClassName = getTargetClass(path).replace("/",".");
inst.addTransformer(new RedefineTransformer(path,targetClassName), true);

/**
* 这段代码的意思是,重新转换目标类,也就是 Account 类。也就是说,你需要重新定义哪个类,需要指定,否则 JVM 不可能知道。
* 还有一个类似的方法 redefineClasses ,注意,这个方法是在类加载前使用的。类加载后需要使用 retransformClasses 方法 */
inst.retransformClasses(getClassByRedefinePath(targetClassName));
}

public static Class<?> getClassByRedefinePath(String targetClassName) throws Exception{
return Class.forName(targetClassName);
}
// 通过asm来获取一个class文件的包名与类名
public static String getTargetClass(String path){
byte[] bytes = RedefineTransformer.getBytesFromFile(path);
ClassReader classReader = new ClassReader(bytes);
return classReader.getClassName();
}

}

再来看看转换类的具体逻辑,其中RedefineTransformer实现了ClassFileTransformer中的transform方法。

RedefineTransformer

如果是目标类,就通过 getBytesFromFile 加载需要替换的class文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class RedefineTransformer implements ClassFileTransformer {

public String reDefineClassPath ;
public String targetClass;
public RedefineTransformer(String classPath,String tc){
reDefineClassPath = classPath;
targetClass = tc;
}


public static byte[] getBytesFromFile(String fileName) {
try {
// precondition
File file = new File(fileName);
InputStream is = new FileInputStream(file);
long length = file.length();
byte[] bytes = new byte[(int) length];

// Read in the bytes
int offset = 0;
int numRead = 0;
while (offset <bytes.length
&& (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) {
offset += numRead;
}

if (offset < bytes.length) {
throw new IOException("Could not completely read file "
+ file.getName());
}
is.close();
return bytes;
} catch (Exception e) {
System.out.println("error occurs in _ClassTransformer!"
+ e.getClass().getName());
return null;
}
}

@Override
public byte[] transform(ClassLoader l, String className, Class<?> c,
ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {
className = className.replace("/",".");
if (!className.equals(targetClass)) {
return null;
}
return getBytesFromFile(reDefineClassPath);

}
}

测试类

测试类比较简单,有两个方法的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Main {

public static void main( String[] args ) throws InterruptedException {
Main demo = new Main();
while (true) {
demo.sayHello();
demo.sayBye();
System.out.println("-------------------");
Thread.sleep(1000);
}
}

public void sayHello() {
System.out.println("Hello World!");
}

public void sayBye() {
System.out.println("Bye Bye");
}

}

修改后的测试类

修改的比较简单,就是在sayBye下面添加了一行输出,通过 javac 对它进行编译,存放到路径F:/Main.class .

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {

public static void main( String[] args ) throws InterruptedException {
Main demo = new Main();
while (true) {
demo.sayHello();
demo.sayBye();
System.out.println("-------------------");
Thread.sleep(1000);
}
}

public void sayHello() {
System.out.println("Hello World!");
}

public void sayBye() {
System.out.println("Bye Bye");
System.out.println("fix good Bye!!");
}

}

通知目标主机

利用Java attach通知目标主机加载agentmain对应的Jar包,其中path是需要替换上的新class文件,pid是你运行测试类得到的pid号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class JVMTIThread {

public static void main(String[] args) throws Exception{

String path = "F:/Main.class";
String pid = "85317" ;
JVMTIThread.attach(path,pid);
}

public static void attach(String classPath, String pid)
throws IOException, AttachNotSupportedException, AgentLoadException, AgentInitializationException {
VirtualMachine virtualMachine = VirtualMachine.attach(pid);
virtualMachine.loadAgent("F:\\agentTest\\target\\agentTest-1.0-SNAPSHOT.jar", classPath);
virtualMachine.detach();
}
}

验证

先启动Main的方法,将对应的 pid 与 修改后的class路径 填入到JVMTIThread 中并运行,结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Hello World!
Bye Bye
-------------------
Hello World!
Bye Bye
-------------------
Hello World!
Bye Bye
-------------------
Hello World!
Bye Bye
-------------------
Hello World!
Bye Bye
fix good Bye!!
-------------------
Hello World!
Bye Bye
fix good Bye!!
-------------------

可以看到运行的过程中Main的确被替换上了新的方法。