DRAFT
Yanyan's Wiki 操作系统 (2023)

Java 内存访问 Trace

注意事项

截止日期:2020 年 12 月 10 日 23:59:59 (以服务器时间为准); The deadline is firm.

提交方法:在命令行中 (请确保拥有 curl 命令) 执行 (将学号、姓名、路径替换为你的个人信息)

curl http://jyywiki.cn/upload -F course=ISER2020 -F module=PA2 -F stuid=学号 -F stuname=中文姓名 -F file=@待提交的文件路径

ISER2020-PA2 提交结果

1. 背景

仅仅依靠程序自身的 log (printf) 很难诊断一些运行时发生的问题,例如 GC 过于频繁等。此时,动态分析工具能在运行时收集 (或打印) 额外的信息以帮助问题的诊断。在正式进入实验之前,推荐大家了解一下 VisualVM,它是 Java 程序的动态分析框架,并且能将 JVM 的状态可视化地展示出来。

在这个实验中,我们用过程序插桩的方式 hack Java 程序,使程序在运行时,能在每次访问共享内存时,都打印一个日志。内存访问日志能用于 race detection 等动态分析。

2. 实验描述

针对一个已打包为 jar 包的 Java 程序,你希望了解程序对共享内存的读写,即为每一个共享内存访问输出一条日志信息。输出一个列表,每个条目输出一行,包含用空格分开的三部分内容:

  • R/W 表示读或写
  • 一个十进制表示 32 位整数,访问的线程号
  • 一个十六进制表示的 64 位整数,对象标识。尽量使不同对象有不同标识,虽然你很难对于 long-running 的程序绝对做到这一点 (思考为什么?)。简单起见,可以考虑直接使用 System.identityHashCode()
  • 对象/类访问的成员 (或数组的下标)

你可以将你的工具实现成命令行工具,用法同 java (你会在你的命令行工具中调用 java):

$ jmtrace -jar something.jar "hello world" # jmtrace 用法用 java
R 1032 b026324c6904b2a9 cn.edu.nju.ics.Foo.someField
W 1031 e7df7cd2ca07f4f1 java.lang.Object[0]
W 1031 e7df7cd2ca07f4f2 java.lang.Object[1]
...

你可以只输出 jar 包中类的内存访问日志,忽略库函数 (java.lang, java.util 等) 中的内存访问。

3. 实现 mtrace

不同于 Linux 的命令行工具 ltrace (通过劫持 GOT 表中的跳转地址) 和 strace (使用操作系统提供的 ptrace 机制),如果希望在更细的粒度上追踪程序的行为,通常需要对程序进行一些改写,即插桩 (program instrumentation),例如,我们可以在源代码级进行插桩:

static void foo() {
  int[] a = new int [10];
  for (int i = 0; i < a.length; i++) {
    a[i] = 0;
  }
  SomeClass.staticField = 1;
  someObj.otherField = someObj.field;
}

通过自动化的改写工具 (改写 AST 后再生成程序),我们可以得到 “插桩” 后的版本:

static void foo() {
  int[] a = new int [10];
  for (int i = 0; i < a.length; i++) {
    int $t0 = 0;
    a[i] = $t0;
    mtrace.traceArrayWrite(a, i, $t0); // instrument added
  }

  int $t1 = 1;
  SomeClass.staticField = $t1;
  mtrace.traceStaticWrite(SomeClass, "staticField", $t1); // instrument added

  SomeType $t2 = someObj.field;
  mtrace.traceFieldRead(someObj, "field", $t2); // instrument add

  someObj.otherField = $t2;
  mtrace.traceFieldWrite(someObj, "otherField", $t2); // instrument add
}

可以看到,插桩前后程序的行为是完全一样的,但变量访问进行了一定的改写,并且在每次 field 或 array 读写之后都插入了一条 log。本质上,这是一种文本的替换,只是我们一般借助 AST 能简化这样的转换。插桩后的程序功能与原先程序等价,但可以输出额外的 trace log (在 mtrace 的方法中实现)。

Java 的源代码将会被编译成字节码 (bytecode)。Bytecode 的形式比源代码简单的多,因此对字节码进行插桩是更方便的选择。例如,字节码分析工具 ASM 能非常容易地对字节码进行改写。通过在访问内存的指令之后插入相应的记录 (访问内存的指令只有 getstatic/putstatic/getfield/putfield/*aload/*astore),通常是一个向 trace 方法的调用。如果考虑插桩内存访问,在字节码插桩会简单地多。

最后,我们推荐使用 JVMTI (JVM Tool Interface) 捕获 class loading 时的回调 (callback)。相当于 JVMTI 能够劫持所有 Java 的类加载,并且在加载时获得被加载类的字节码。此时就是进行插桩的最佳时机——这样甚至不会错过任何动态生成的类的插桩!

你也可以搜索互联网,查找其他可能的解决方案。例如,Java 提供了 java.lang.Instrument,相当于是刚才过程的包装——这能省去不少事情,但也减少了 hacking 的乐趣。你可以自由选择实现的方式。

4. 提交方法与评分准则

将以下内容打成压缩包 (zip 或 tar) 上传:

  • (必要) 工具的源代码和编译说明。同 PA1,请不要上传依赖、二进制文件等能够被源码生成的文件
  • (必要) pdf 格式的实验报告,简述你实现 jmtrace 时主要使用的技术和遇到的挑战。实验报告不宜超过 2 页 A4 篇幅
  • (可选) 工具的 binary,可能包含 jar 包、可执行文件等

本实验按照实现的正确性评分。我们会运行一些较小 (数百万次内存访问以内) 的基准程序,并检查程序的输出结果。注意并发是 Java 语言/虚拟机的一部分,这些程序可能启动多个线程。

5. Artifact

为了更好地促进后续的研究工作,将研究工作的代码以开源项目的形式发布是非常必要的。因此,在这门课程中,我们要求你选一个 programming assignment (codesim 或 jmtrace),并按照开源软件的通常标准维护。对于 artifact 来说,你要尽最大的可能为这份代码负责,因为未来其他研究者可能会基于你的代码开展其他研究工作。

特别注意:Artifact 准备是二选一的。你只需要提交一份 artifact 供 peer-review。你可以任选是 PA1 (codesim) 或是 PA2 (jmtrace) 作为 artifact。关于如何准备 Artifact 请参考 Artifact 准备指南

Creative Commons License    苏 ICP 备 2020049101 号