由于需求,我最近向 javassist 贡献了一个数据流分析框架。该框架允许应用程序通过推理确定每条字节码指令开始时局部变量表和栈帧的类型状态。对于那些不熟悉 Java 字节码格式的人来说,一旦 Java 程序编译,就会丢失大量信息,因为这些信息在程序执行时并非真正需要,省略它们有助于保持类文件较小。
为了说明这种丢失,看看以下简单的 Java 方法
public static class Base {} public static class A extends Base{} public static class B extends Base{} public static class C extends B{} private void foo(int x) { Base b; if (x > 4) { b = new A(); } else { b = new C(); } b.toString(); }
尽管在 Java 代码中非常清楚地看到 b
是 Base
类型,但这种信息在编译器的输出中缺失
private void foo(int); Code: Stack=2, Locals=3, Args_size=2 0: iload_1 1: iconst_4 2: if_icmple 16 5: new #68; //class example/Example$A 8: dup 9: invokespecial #70; //Method example/Example$A."<init>":()V 12: astore_2 13: goto 24 16: new #71; //class example/Example$C 19: dup 20: invokespecial #73; //Method example/Example$C."<init>":()V 23: astore_2 24: aload_2 25: invokevirtual #74; //Method java/lang/Object.toString:()Ljava/lang/String; 28: pop 29: return
由于 toString()
是 Object 声明的,所以第 25 行只能告诉我们类型是 Object,这显然并不具体。如果类是带调试信息编译的,你将能够了解到局部 #2 是 Base
类型,但你即使有了这些信息,也不一定知道 invokevirtual 调用的对象是存储在局部变量 2 中的值。确定这一点的方法是知道指令执行前栈帧的状态。
分析框架通过模拟每条指令的影响来实现这一点,直到最终可以推断出类型信息。这个过程不使用任何调试信息,因为没有保证它可用。相反,它通过跟踪所有可能的状态来外推它,因为每个分支都会被评估,直到类型信息减少到最具体的类型状态。
以下使用该框架的代码能够告诉我们第 25 行调用的类型实际上是 Base
Analyzer a = new Analyzer(); CtClass clazz = ClassPool.getDefault().get("example.Example"); Frame[] frames = a.analyze(clazz.getDeclaredMethod("foo")); System.out.println(frames[25].peek()); // Prints "example.Example$Base"
我还添加了一个名为 framedump 的小工具,它能够以人类可读的格式在每条指令处输出整个状态,是的,我知道这有些可议 :)
$ framedump example.Example private void foo(int); 0: iload_1 stack [] locals [example.Example, int, empty] ... snipped for brevity ... 24: aload_2 stack [] locals [example.Example, int, example.Example$Base] 25: invokevirtual #85 = Method java.lang.Object.toString(()Ljava/lang/String;) stack [example.Example$Base] locals [example.Example, int, example.Example$Base] 28: pop stack [java.lang.String] locals [example.Example, int, example.Example$Base] 29: return stack [] locals [example.Example, int, example.Example$Base]
你们中的一些人可能正在想
听起来不错,但究竟为什么我会需要使用这个呢?
这绝对不是每个人都需要的,但它对于某些应用来说非常有用
- 字节码增强器
- 验证器
- 优化器
- 调试/性能分析工具
- 反汇编器
为了进一步说明增强器示例,出于安全考虑,JVM实际上会自己进行数据流分析,以验证一个类在运行前是否违反了类型规则。这对于任何操作字节码的应用程序来说都是一个有趣的挑战,因为任何影响可能类型状态的变化都可能导致验证错误,JVM将抛出该类。此类框架可用于防止此类问题,因为它们揭示了JVM验证器可用的相同(在javassist分析器的情况下,实际上更详细)信息。
如果您想尝试这个新功能,请下载最近发布的3.8.0版本这里。
Java文档在这里。
注意,我还应该提到ASM项目已经有一个类似的框架有一段时间了,然而,由于我需要处理多接口和数组类型减少的能力,它在我这个案例中是不可用的。此外,我已经在使用javassist,切换是不可能的,主要是因为我依赖的其他功能。
祝您玩得开心!