本文提供如何获取运行时Java程序加载的数据段内容方法和原理分析,通过dumpclass工具,结合jdk底层sa-jdi工具。可以实现对指定类的class字节码获取的功能。通过获取到的字节码,通过jad等反编译工具可以得到Java源代码,为分析代码提供帮助。

1 获取原理分析

  1. 使用dumpclass工具获取运行时类字节码
# 1.在该jar程序中,主要实现了类过滤器,以及对类字符串模式匹配的功能
# 2.实际上,通过dumpclass工具调用的为openjdk底层的sa-jdi工具,Java Debug Interface,类似于GDB的debug调试工具。其中的类sun.jvm.hotspot.tools.jcore.ClassDump 实现了对于运行时JVM内存中数据段字节码的dump处理。
java -jar dumpclass.jar -p 5826 -o ./dumpCode *HelloWorld
  1. 通过将自定义的filter注入到环境中,使得可以通过本类完成数据段中类的过滤
// 向系统中注入自定义 filter,实现类为 DumpWrapperFilter
System.setProperty("sun.jvm.hotspot.tools.jcore.filter", "io.github.hengyunabc.dumpclass.DumpWrapperFilter");
  1. 底层反射调用 sa-jdi 的 ClassDump 对象的 main 方法
// 反射得到 sa-jdi 包中的 ClassDump 对象,并调用 main 方法,传入dump的pid进程号
Method mainMethod = classLoader.loadClass("sun.jvm.hotspot.tools.jcore.ClassDump").getMethod("main",String[].class);
// sun.jvm.hotspot.tools.jcore.ClassDump.main(new String[] { pid });
mainMethod.invoke(null, new Object[] { new String[] { "" + pid } });
  1. 通过阅读 openjdk sa-jdi 源码,了解 sa-jdi 的 ClassDump 源码位置,它首先获取环境中的两个key对应的包value
//openjdk/hotspot/agent/src/share/classes/sun/jvm/hotspot/tools/jcore/ClassDump.java
// 获取 filter 和 dump 的输出目录
filterClassName = System.getProperty("sun.jvm.hotspot.tools.jcore.filter");

outputDirectory = System.getProperty("sun.jvm.hotspot.tools.jcore.outputDir");
  1. walk through the system dictionary,遍历pid进程的JVM系统数据字段,通过filter过滤是否需要dump,最后通过 ClassWriter 进行数据字段的dump,生成class字节码文件
// walk through the system dictionary
// 获取数据字段内容字典
SystemDictionary dict = VM.getVM().getSystemDictionary();
// 遍历字典中的全部类
dict.classesDo(new SystemDictionary.ClassVisitor() {
  public void visit(Klass k) {
    if (k instanceof InstanceKlass) {
      try {
        // 需要dump文件的目标class筛选
        dumpKlass((InstanceKlass) k);
      } catch (Exception e) {
        System.out.println(k.getName().asString());
        e.printStackTrace();
      }
    }
  }
});

private void dumpKlass(InstanceKlass kls) {
  // 判定是否满足filter过滤条件
  if (classFilter != null && ! classFilter.canInclude(kls) ) {
    return;
  }
  // ...
  
  // 向指定文件中dump class 字节码
  try {
    f.createNewFile();
    OutputStream os = new BufferedOutputStream(new FileOutputStream(f));
    try {
      // ClassWriter 具体dump
      ClassWriter cw = new ClassWriter(kls, os);
      cw.write();
    } finally {
      os.close();
    }
  } catch(IOException exp) {
    exp.printStackTrace();
  }
}
  1. ClassWrite的写入操作,遵循Java字节码规范完成的解析工作
public void write() throws IOException {
  if (DEBUG) debugMessage("class name = " + klass.getName().asString());

  // write magic
  dos.writeInt(0xCAFEBABE);

  writeVersion();
  writeConstantPool();
  writeClassAccessFlags();
  writeThisClass();
  writeSuperClass();
  writeInterfaces();
  writeFields();
  writeMethods();
  writeClassAttributes();

  // flush output
  dos.flush();
}

至此,完成了对指定运行时类字节码的获取。

  • 实际上 dumpclass 是对 sa-jdi 的封装,可以使用 sa-jdi 直接通过参数指定的方式,完成对某个运行时类的dump操作
$ java -classpath "$JAVA_HOME/lib/sa-jdi.jar" -Dsun.jvm.hotspot.tools.jcore.filter=sun.jvm.hotspot.tools.jcore.PackageNameFilter -Dsun.jvm.hotspot.tools.jcore.PackageNameFilter.pkgList=com.ustb.geth.testdemo.HelloWorld  -Dsun.jvm.hotspot.tools.jcore.outputDir=./dumpCode sun.jvm.hotspot.tools.jcore.ClassDump 11708

Attaching to process ID 11708, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.301-b09

2 运行时字节码获取具体操作步骤-实例demo

  1. 编写程序HelloWorld.java 用于测试
package com.ustb.geth.testdemo;

public class HelloWorld {
    public static void main(String[] args) {
        try {
            System.out.println("hello world Java");
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  1. 编译并启动运行
# 编译为字节码
javac com/ustb/geth/testdemo/HelloWorld.java
# 启动运行
java com.ustb.geth.testdemo.HelloWorld &
  1. 下载用于获取运行时类字节码的工具jar包
# github 托管地址
https://github.com/hengyunabc/dumpclass
# 直接下载jar包
wget http://search.maven.org/remotecontent?filepath=io/github/hengyunabc/dumpclass/0.0.2/dumpclass-0.0.2.jar -O dumpclass.jar
  1. 获取运行的java程序的pid,此时得到HelloWorld程序的pid为 5826
jps -lv

5826 com.ustb.geth.testdemo.HelloWorld
21846 springboot-service-0.0.7-SNAPSHOT.jar
5287 AUR.jar
  1. 执行程序获取HelloWorld运行时数据段的类class字节码
# -jar 指定dumpclass的jar包
# -p 指定需要dump字节码的java程序的进程号
# -o 指定获取的class文件存放路径
# pattern 指定需要dump的class类的模式匹配规则
java -jar dumpclass.jar -p 5826 -o ./dumpCode *HelloWorld

can not find sa-jdi.jar from classpath, try to load it from java.home.
Attaching to process ID 5826, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.301-b09
Dumped classes counter: 1
Output directory: /root/testDumpClass/./dumpCode
Over write files size: 1
Over write files:
/root/testDumpClass/./dumpCode/com/ustb/geth/testdemo/HelloWorld.class

3.待解决问题

1.通过javac HelloWorld.java 得到 class 文件 HelloWorld.class,使用md5的方式得到该class文件Hash值为「A」;通过 dumpclass 的方式从进程中恢复的 HelloWorld.class 文件,使用md5的方式得到该文件Hash值为「B」。
现象:A != B ,为何恢复后程序的class文件不同?
2.如果 java 直接运行通过 dumpclass 得到的class文件,会报错 “Expected stackmap frame at this location”,类似于没有通过JVM验证,但是加上参数 -noverify 就又可以正常运行了。这个实验似乎与上面Hash值不同的现象呼应。类似于JVM运行时保护?

206117380-e8932582-c6cf-4767-8aa0-6d2ae42391a0.png

版权声明:本文为yorickjun原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/yorickjun/article/details/128479388