Arthas 之 jad 反编译原理

jad 命令介绍

jad 即 java decompiler,把 JVM 已加载类的字节码反编译成 Java 代码。

https://alibaba.github.io/arthas/jad.html

获取到类的字节码

反编译有两部分工作:

  1. 获取到字节码
  2. 反编译为 Java 代码

那么怎么从运行的 JVM 里获取到字节码?

最常见的思路是,在classpaths下面查找,比如 ClassLoader.getResource("java/lang/String.class"),但是这样子查找到的字节码不一定对。比如可能有多个冲突的 jar,或者有 Java Agent 修改了字节码。

ClassFileTransformer 机制

从 JDK 1.5 起,有一套ClassFileTransformer的机制,Java Agent 通过Instrumentation注册ClassFileTransformer,那么在类加载或者retransform时就可以回调修改字节码。

显然,在 Arthas 里,要增强的类是已经被加载的,所以它们的字节码都是在retransform时被修改的。
通过显式调用Instrumentation.retransformClasses(Class<?>...)可以触发回调。

Arthas 里增强字节码的watch/trace/stack/tt等命令都是通过ClassFileTransformer来实现的。

java.lang.instrument.ClassFileTransformer的接口如下:

public interface ClassFileTransformer {
    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}

看到这里,读者应该猜到jad是怎么获取到字节码的了:

  1. 注册一个ClassFileTransformer
  2. 通过Instrumentation.retransformClasses触发回调
  3. 在回调的transform函数里获取到字节码
  4. 删掉注册的ClassFileTransformer

使用 cfr 来反编译

获取到字节码之后,怎样转换为 Java 代码呢?

以前大家使用比较多的反编译软件可能是jd-gui,但是它不支持 JDK8 的 lambda 语法和一些新版本 JDK 的特性。

后面比较成熟的反编译软件是cfr,它以前是不开源的。直到最近的0.145版本,作者终于开源了,可喜可贺。地址是

https://github.com/leibnitz27/cfr

在 Arthas jad命令里,通过调用cfr来完成反编译。

jad 命令的缺陷

99% 的情况下,jad命令 dump 下来的字节码是准确的,除了一些极端情况。

  1. 因为 JVM 里注册的ClassFileTransformer可能有多个,那么在 JVM 里运行的字节码里,可能是被多个ClassFileTransformer处理过的。
  2. 触发了retransformClasses之后,这些注册的ClassFileTransformer会被依次回,上一个处理的字节码传递到下一个。
    所以不能保证这些ClassFileTransformer第二次执行会返回同样的结果。
  3. 有可能一些ClassFileTransformer会被删掉,触发retransformClasses之后,之前的一些修改就会丢失掉。

所以目前在 Arthas 里,如果开两个窗口,一个窗口执行watch/tt等命令,另一个窗口对这个类执行jad,那么可以观察到watch/tt停止了输出,实际上是因为字节码在触发了retransformClasses之后,watch/tt所做的修改丢失了。

精确获取字节码

如果想精确获取到 JVM 内运行的 Java 字节码,可以使用这个dumpclass工具,它是通过sa-jdi.jar来实现的,保证 dump 下来的字节码是 JVM 内所运行的。

https://github.com/hengyunabc/dumpclass

jad 命令实现过程

JadCommand.java:

process() -> processExactMatch() -> Decompiler.decompile()

cfr 反编译示例

参考 Arthas 代码:com.taobao.arthas.core.util.Decompiler.java

引入依赖

首先,需要引入 maven 依赖:

<dependency>
	<groupId>org.benf</groupId>
	<artifactId>cfr</artifactId>
	<version>0.150</version>
</dependency>

编写方法

编写反编译类 sample.utils.Decompiler:


public static String decompile(String classFilePath, String methodName) {
	return decompile(classFilePath, methodName, false);
}

/**
 * @param classFilePath
 * @param methodName
 * @param hideUnicode
 * @return
 */
public static String decompile(String classFilePath, String methodName, boolean hideUnicode) {
	final StringBuilder result = new StringBuilder(8192);

	OutputSinkFactory mySink = new OutputSinkFactory() {
		@Override
		public List<SinkClass> getSupportedSinks(SinkType sinkType, Collection<SinkClass> collection) {
			return Arrays.asList(SinkClass.STRING, SinkClass.DECOMPILED, SinkClass.DECOMPILED_MULTIVER,
							SinkClass.EXCEPTION_MESSAGE);
		}

		@Override
		public <T> Sink<T> getSink(final SinkType sinkType, SinkClass sinkClass) {
			return new Sink<T>() {
				@Override
				public void write(T sinkable) {
					// skip message like: Analysing type demo.MathGame
					if (sinkType == SinkType.PROGRESS) {
						return;
					}
					result.append(sinkable);
				}
			};
		}
	};

	HashMap<String, String> options = new HashMap<String, String>();
	/**
	 * @see org.benf.cfr.reader.util.MiscConstants.Version.getVersion() Currently,
	 *      the cfr version is wrong. so disable show cfr version.
	 */
	options.put("showversion", "false");
	options.put("hideutf", String.valueOf(hideUnicode));
	if (!StringUtils.isBlank(methodName)) {
		options.put("methodname", methodName);
	}

	CfrDriver driver = new CfrDriver.Builder().withOptions(options).withOutputSink(mySink).build();
	List<String> toAnalyse = new ArrayList<String>();
	toAnalyse.add(classFilePath);
	driver.analyse(toAnalyse);

	return result.toString();
}

测试

String className = "sample/redefine/RedefineSuccess.class";
String classPath = Thread.currentThread().getContextClassLoader().getResource(className).getFile();
System.out.println(classPath);

String result = decompile(classPath, null);
System.out.println(result);

String result2 = decompile(classPath, "printFlag");
System.out.println(result2);

输出

/C:/Codes/JavaWorkSpace/workspace48/arthas-demo/target/classes/sample/redefine/RedefineSuccess.class
/*
 * Decompiled with CFR.
 */
package sample.redefine;

import java.util.concurrent.TimeUnit;

public class RedefineSuccess {
    public static void main(String[] args) throws InterruptedException {
        while (true) {
            TimeUnit.SECONDS.sleep(3L);
            RedefineSuccess.printFlag();
        }
    }

    private static void printFlag() {
        boolean flag = false;
        if (flag) {
            System.out.println("flag is true.");
        } else {
            System.out.println("flag is false.");
        }
    }
}

private static void printFlag() {
    boolean flag = false;
    if (flag) {
        System.out.println("flag is true.");
    } else {
        System.out.println("flag is false.");
    }
}

参考文档

http://hengyunabc.github.io/arthas-jad/