【Arthas】Arthas 类查找和反编译原理

x33g5p2x  于2022-04-06 转载在 其他  
字(9.3k)|赞(0)|评价(0)|浏览(389)

1.概述

转载:Arthas 类查找和反编译原理

2.开篇

Arthas支持通过类相关的操作命令,包括sc、sm、jad等。

sc(Search-Class)命令搜索出所有已经加载到 JVM 中的 Class 信息。

sm(Search-Method)命令搜索出所有已经加载了 Class 信息的方法信息。

jad命令将 JVM 中实际运行的 class 的 byte code 反编译成 java 代码。

这篇文章是分析上述类相关操作的底层实现原理包含Instrumentation和CFR。

3.SC实现原理

public class SearchClassCommand extends AnnotatedCommand {
    public void process(final CommandProcess process) {
        // 1、核心是获取Instrumentation对象
        Instrumentation inst = process.session().getInstrumentation();

        // 2、SearchUtils负责查找对应的class
        List<Class<?>> matchedClasses = new ArrayList<Class<?>>(SearchUtils.searchClass(inst, classPattern, isRegEx, hashCode));

        // 3、对查询结果进行下排序
        Collections.sort(matchedClasses, new Comparator<Class<?>>() {
            @Override
            public int compare(Class<?> c1, Class<?> c2) {
                return StringUtils.classname(c1).compareTo(StringUtils.classname(c2));
            }
        });

        affect.rCnt(matchedClasses.size());
        process.appendResult(new RowAffectModel(affect));
        process.end();
    }
}

public class SearchUtils {

    public static Set<Class<?>> searchClass(Instrumentation inst, Matcher<String> classNameMatcher, int limit) {
        if (classNameMatcher == null) {
            return Collections.emptySet();
        }
        final Set<Class<?>> matches = new HashSet<Class<?>>();
        // 通过inst.getAllLoadedClasses获取所有加载的类
        for (Class<?> clazz : inst.getAllLoadedClasses()) {
            if (classNameMatcher.matching(clazz.getName())) {
                matches.add(clazz);
            }
            if (matches.size() >= limit) {
                break;
            }
        }
        return matches;
    }

java Instrumentation指的是可以用独立于应用程序之外的代理(agent)程序来监测和协助运行在JVM上的应用程序。这种监测和协助包括但不限于获取JVM运行时状态,替换和修改类定义等。

Arthas的SC命令就是通过Instrumentation的getAllLoadedClasses来实现类的查找。

4. SM实现原理

public class SearchMethodCommand extends AnnotatedCommand {

    public void process(CommandProcess process) {
        RowAffect affect = new RowAffect();

        Instrumentation inst = process.session().getInstrumentation();
        Matcher<String> methodNameMatcher = methodNameMatcher();
        // 1、通过Instrumentation查找对应的类
        Set<Class<?>> matchedClasses = SearchUtils.searchClass(inst, classPattern, isRegEx, hashCode);

        for (Class<?> clazz : matchedClasses) {
            try {
                // 2、遍历类的构造函数
                for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
                    if (!methodNameMatcher.matching("<init>")) {
                        continue;
                    }

                    MethodVO methodInfo = ClassUtils.createMethodInfo(constructor, clazz, isDetail);
                    process.appendResult(new SearchMethodModel(methodInfo, isDetail));
                    affect.rCnt(1);
                }
                // 3、遍历所有的方法
                for (Method method : clazz.getDeclaredMethods()) {
                    if (!methodNameMatcher.matching(method.getName())) {
                        continue;
                    }
                    MethodVO methodInfo = ClassUtils.createMethodInfo(method, clazz, isDetail);
                    process.appendResult(new SearchMethodModel(methodInfo, isDetail));
                    affect.rCnt(1);
                }
            } catch (Error e) {
            }
        }

        process.appendResult(new RowAffectModel(affect));
        process.end();
    }
}

Arthas的SM命令首先通过Instrumentation的getAllLoadedClasses来实现类的查找。

Arthas的SM命令其次通过反射查找对应的方法。

5.JAD实现原理

public class JadCommand extends AnnotatedCommand {

    public void process(CommandProcess process) {
        RowAffect affect = new RowAffect();
        Instrumentation inst = process.session().getInstrumentation();
        // 1、通过Instrumentation查找对应的类
        Set<Class<?>> matchedClasses = SearchUtils.searchClassOnly(inst, classPattern, isRegEx, code);

        try {
            ExitStatus status = null;
            if (matchedClasses == null || matchedClasses.isEmpty()) {
                status = processNoMatch(process);
            } else if (matchedClasses.size() > 1) {
                status = processMatches(process, matchedClasses);
            } else { // matchedClasses size is 1
                // find inner classes.
                Set<Class<?>> withInnerClasses = SearchUtils.searchClassOnly(inst,  matchedClasses.iterator().next().getName() + "$*", false, code);
                if(withInnerClasses.isEmpty()) {
                    withInnerClasses = matchedClasses;
                }
                // 2、执行类的反编译核心操作
                status = processExactMatch(process, affect, inst, matchedClasses, withInnerClasses);
            }
            if (!this.sourceOnly) {
                process.appendResult(new RowAffectModel(affect));
            }
            CommandUtils.end(process, status);
        } catch (Throwable e){
        }
    }

    private ExitStatus processExactMatch(CommandProcess process, RowAffect affect, Instrumentation inst, Set<Class<?>> matchedClasses, Set<Class<?>> withInnerClasses) {
        Class<?> c = matchedClasses.iterator().next();
        Set<Class<?>> allClasses = new HashSet<Class<?>>(withInnerClasses);
        allClasses.add(c);

        try {
            // 1、创建ClassDumpTransformer对象
            ClassDumpTransformer transformer = new ClassDumpTransformer(allClasses);
            // 2、执行retransformClasses收集待反编译的类文件
            InstrumentationUtils.retransformClasses(inst, transformer, allClasses);

            Map<Class<?>, File> classFiles = transformer.getDumpResult();
            File classFile = classFiles.get(c);
            // 3、执行反编译的动作
            Pair<String,NavigableMap<Integer,Integer>> decompileResult = Decompiler.decompileWithMappings(classFile.getAbsolutePath(), methodName, hideUnicode, lineNumber);

            // 省略无关代码
            return ExitStatus.success();
        } catch (Throwable t) {
        }
    }
}

通过Instrumentation的getAllLoadedClasses来实现类的查找。
创建ClassDumpTransformer并通过retransformClasses保存原始字节码。

通过decompileWithMappings实现字节码的反编译。

class ClassDumpTransformer implements ClassFileTransformer {

    private Set<Class<?>> classesToEnhance;
    private Map<Class<?>, File> dumpResult;
    private File arthasLogHome;

    private File directory;

    public ClassDumpTransformer(Set<Class<?>> classesToEnhance) {
        this(classesToEnhance, null);
    }

    public ClassDumpTransformer(Set<Class<?>> classesToEnhance, File directory) {
        this.classesToEnhance = classesToEnhance;
        this.dumpResult = new HashMap<Class<?>, File>();
        this.arthasLogHome = new File(LogUtil.loggingDir());
        this.directory = directory;
    }

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain, byte[] classfileBuffer)
            throws IllegalClassFormatException {
        if (classesToEnhance.contains(classBeingRedefined)) {
            dumpClassIfNecessary(classBeingRedefined, classfileBuffer);
        }
        return null;
    }

    public Map<Class<?>, File> getDumpResult() {
        return dumpResult;
    }

    private void dumpClassIfNecessary(Class<?> clazz, byte[] data) {
        String className = clazz.getName();
        ClassLoader classLoader = clazz.getClassLoader();
        String classDumpDir = "classdump";

        // 创建类所在的包路径
        File dumpDir = null;
        if (directory != null) {
            dumpDir = directory;
        } else {
            dumpDir = new File(arthasLogHome, classDumpDir);
        }
        if (!dumpDir.mkdirs() && !dumpDir.exists()) {
            logger.warn("create dump directory:{} failed.", dumpDir.getAbsolutePath());
            return;
        }

        String fileName;
        if (classLoader != null) {
            fileName = classLoader.getClass().getName() + "-" + Integer.toHexString(classLoader.hashCode()) +
                    File.separator + className.replace(".", File.separator) + ".class";
        } else {
            fileName = className.replace(".", File.separator) + ".class";
        }

        File dumpClassFile = new File(dumpDir, fileName);

        // 将类字节码写入文件
        try {
            FileUtils.writeByteArrayToFile(dumpClassFile, data);
            dumpResult.put(clazz, dumpClassFile);
        } catch (IOException e) {
        }
    }
}

ClassDumpTransformer是自定义的Transformer对象,retransformClasses会进行调用。
ClassDumpTransformer的dumpClassIfNecessary负责保存原始的字节码。

public class InstrumentationUtils {

    public static void retransformClasses(Instrumentation inst, ClassFileTransformer transformer,
            Set<Class<?>> classes) {
        try {
            inst.addTransformer(transformer, true);

            for (Class<?> clazz : classes) {
                try {
                    inst.retransformClasses(clazz);
                } catch (Throwable e) {
                    String errorMsg = "retransformClasses class error, name: " + clazz.getName();
                    logger.error(errorMsg, e);
                }
            }
        } finally {
            inst.removeTransformer(transformer);
        }
    }
}

InstrumentationUtils负责执行原始字节码的收集,通过retransformClasses来实现。

public class Decompiler {

    public static Pair<String, NavigableMap<Integer, Integer>> decompileWithMappings(String classFilePath,
            String methodName, boolean hideUnicode, boolean printLineNumber) {
        final StringBuilder sb = new StringBuilder(8192);

        final NavigableMap<Integer, Integer> lineMapping = new TreeMap<Integer, Integer>();

        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, SinkClass.LINE_NUMBER_MAPPING);
            }

            @Override
            public <T> Sink<T> getSink(final SinkType sinkType, final SinkClass sinkClass) {
                return new Sink<T>() {
                    @Override
                    public void write(T sinkable) {
                        // skip message like: Analysing type demo.MathGame
                        if (sinkType == SinkType.PROGRESS) {
                            return;
                        }
                        if (sinkType == SinkType.LINENUMBER) {
                            LineNumberMapping mapping = (LineNumberMapping) sinkable;
                            NavigableMap<Integer, Integer> classFileMappings = mapping.getClassFileMappings();
                            NavigableMap<Integer, Integer> mappings = mapping.getMappings();
                            if (classFileMappings != null && mappings != null) {
                                for (Entry<Integer, Integer> entry : mappings.entrySet()) {
                                    Integer srcLineNumber = classFileMappings.get(entry.getKey());
                                    lineMapping.put(entry.getValue(), srcLineNumber);
                                }
                            }
                            return;
                        }
                        sb.append(sinkable);
                    }
                };
            }
        };

        HashMap<String, String> options = new HashMap<String, String>();
        options.put("showversion", "false");
        options.put("hideutf", String.valueOf(hideUnicode));
        options.put("trackbytecodeloc", "true");
        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);

        String resultCode = sb.toString();
        if (printLineNumber && !lineMapping.isEmpty()) {
            resultCode = addLineNumber(resultCode, lineMapping);
        }

        return Pair.make(resultCode, lineMapping);
    }
}

通过CFR-api实现字节码的反编译。

6.总结

类的查询或方法的查找基于Instrumentation来实现的。
类反编译是基于CFR来实现的,提供CFR-api实现。
认识 JavaAgent --获取目标进程已加载的所有类

相关文章