4.2.7 HSDIS:JIT生成代码反汇编

在Java虚拟机规范中,详细描述了虚拟机指令集中每条指令的执行过程、执行前后对操作数栈、局部变量表的影响等细节。这些细节描述与Sun的早期虚拟机(Sun Classic VM)高度吻合,但随着技术的发展,高性能虚拟机真正的细节实现方式已经渐渐与虚拟机规范所描述的内容产生了越来越大的差距,虚拟机规范中的描述逐渐成了虚拟机实现的“概念模型”——即实现只能保证规范描述等效。基于这个原因,我们分析程序的执行语义问题(虚拟机做了什么)时,在字节码层面上分析完全可行,但分析程序的执行行为问题(虚拟机是怎样做的、性能如何)时,在字节码层面上分析就没有什么意义了,需要通过其他方式解决。

分析程序如何执行,通过软件调试工具(GDB、Windbg等)来断点调试是最常见的手段,但是这样的调试方式在Java虚拟机中会遇到很大困难,因为大量执行代码是通过JIT编译器动态生成到CodeBuffer中的,没有很简单的手段来处理这种混合模式的调试(不过相信虚拟机开发团队内部肯定是有内部工具的)。因此,不得不通过一些特别的手段来解决问题,基于这种背景,本节的主角——HSDIS插件就正式登场了。

HSDIS是一个Sun官方推荐的HotSpot虚拟机JIT编译代码的反汇编插件,它包含在HotSpot虚拟机的源码之中,但没有提供编译后的程序。在Project Kenai的网站[1]也可以下载到单独的源码。它的作用是让HotSpot的-XX:+PrintAssembly指令调用它来把动态生成的本地代码还原为汇编代码输出,同时还生成了大量非常有价值的注释,这样我们就可以通过输出的代码来分析问题。读者可以根据自己的操作系统和CPU类型从Project Kenai的网站上下载编译好的插件,直接放到JDK_HOME/jre/bin/client和JDK_HOME/jre/bin/server目录中即可。如果没有找到所需操作系统(譬如Windows的就没有)的成品,那就得自己使用源码编译一下[2]

还需要注意的是,如果读者使用的是Debug或者FastDebug版的HotSpot,那可以直接通过-XX:+PrintAssembly指令使用插件;如果使用的是Product版的HotSpot,那还要额外加入一个-XX:+UnlockDiagnosticVMOptions参数。笔者以代码清单4-6中的简单测试代码为例演示一下这个插件的使用。

代码清单4-6 测试代码


public class Bar{

int a=1;

static int b=2;

public int sum(int c){

return a+b+c;

}

public static void main(String[]args){

new Bar().sum(3);

}

}


编译这段代码,并使用以下命令执行。


java-XX:+PrintAssembly-Xcomp-XX:CompileCommand=dontinline,Bar.sum-XX:Compi leCommand=compileonly,Bar.sum test.Bar


其中,参数-Xcomp是让虚拟机以编译模式执行代码,这样代码可以“偷懒”,不需要执行足够次数来预热就能触发JIT编译[3]。两个-XX:CompileCommand意思是让编译器不要内联sum()并且只编译sum(),-XX:+PrintAssembly就是输出反汇编内容。如果一切顺利的话,那么屏幕上会出现类似下面代码清单4-7所示的内容。

代码清单4-7 测试代码


[Disassembling for mach='i386']

[Entry Point]

[Constants]

{method}'sum''(I)I'in'test/Bar'

this:ecx='test/Bar'

parm0:edx=int

[sp+0x20](sp of caller)

……

0x01cac407:cmp 0x4(%ecx),%eax

0x01cac40a:jne 0x01c6b050;{runtime_call}

[Verified Entry Point]

0x01cac410:mov%eax,-0x8000(%esp)

0x01cac417:push%ebp

0x01cac418:sub$0x18,%esp;*aload_0

;-test.Bar:sum@0(line 8)

;block B0[0,10]

0x01cac41b:mov 0x8(%ecx),%eax;*getfield a

;-test.Bar:sum@1(line 8)

0x01cac41e:mov$0x3d2fad8,%esi;{oop(a

'java/lang/Class'='test/Bar')}

0x01cac423:mov 0x68(%esi),%esi;*getstatic b

;-test.Bar:sum@4(line 8)

0x01cac426:add%esi,%eax

0x01cac428:add%edx,%eax

0x01cac42a:add$0x18,%esp

0x01cac42d:pop%ebp

0x01cac42e:test%eax,0x2b0100;{poll_return}

0x01cac434:ret


上段代码并不多,下面一句句进行说明。

1)mov%eax,-0x8000(%esp):检查栈溢。

2)push%ebp:保存上一栈帧基址。

3)sub$0x18,%esp:给新帧分配空间。

4)mov 0x8(%ecx),%eax:取实例变量a,这里0x8(%ecx)就是ecx+0x8的意思,前面“[Constants]”节中提示了“this:ecx='test/Bar'”,即ecx寄存器中放的就是this对象的地址。偏移0x8是越过this对象的对象头,之后就是实例变量a的内存位置。这次是访问“Java堆”中的数据。

5)mov$0x3d2fad8,%esi:取test.Bar在方法区的指针。

6)mov 0x68(%esi),%esi:取类变量b,这次是访问“方法区”中的数据。

7)add%esi,%eax和add%edx,%eax:做两次加法,求a+b+c的值,前面的代码把a放在eax中,把b放在esi中,而c在[Constants]中提示了,“parm0:edx=int”,说明c在edx中。

8)add$0x18,%esp:撤销栈帧。

9)pop%ebp:恢复上一栈帧。

10)test%eax,0x2b0100:轮询方法返回处的SafePoint。

11)ret:方法返回。

[1]Project Kenai:http://kenai.com/projects/base-hsdis。

[2]HLLVM圈子中有已编译好的:http://hllvm.group.iteye.com/。

[3]-Xcomp在较新的HotSpot中被移除了,如果读者的虚拟机无法使用这个参数,请加个循环预热代码,触发JIT编译。