本文提供如何获取运行时Java程序加载的数据段内容方法和原理分析,通过dumpclass工具,结合jdk底层sa-jdi工具。可以实现对指定类的class字节码获取的功能。通过获取到的字节码,通过jad等反编译工具可以得到Java源代码,为分析代码提供帮助。
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
- 通过将自定义的filter注入到环境中,使得可以通过本类完成数据段中类的过滤
// 向系统中注入自定义 filter,实现类为 DumpWrapperFilter
System.setProperty("sun.jvm.hotspot.tools.jcore.filter", "io.github.hengyunabc.dumpclass.DumpWrapperFilter");
- 底层反射调用 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 } });
- 通过阅读 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");
- 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();
}
}
- 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
- 编写程序
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();
}
}
}
- 编译并启动运行
# 编译为字节码
javac com/ustb/geth/testdemo/HelloWorld.java
# 启动运行
java com.ustb.geth.testdemo.HelloWorld &
- 下载用于获取运行时类字节码的工具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
- 获取运行的java程序的pid,此时得到HelloWorld程序的pid为 5826
jps -lv
5826 com.ustb.geth.testdemo.HelloWorld
21846 springboot-service-0.0.7-SNAPSHOT.jar
5287 AUR.jar
- 执行程序获取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 版权协议,转载请附上原文出处链接和本声明。