Java代码的汇编细节

一世执手 提交于 2020-02-25 17:06:18

尽管在Java语言中,存在一个“Java虚拟机规范”,规范了Java中每一条指令所能执行的动作以及堆栈的分布做了规范,但是随着技术的发展,高性能虚拟机真正的细节实现方式已经渐渐与虚拟机规范所描述产生越来越大的差距,虚拟机规范中的描述逐渐成了虚拟机实现的“概念模型”——即实现只能保证规范描述等效。 
基于上面的原因,我们分析程序的执行语义问题(虚拟机做了什么)时,在字节码层面上分析完全可行,但分析程序的执行行为问题(虚拟机是怎样做的、性能如何)时,在字节码层面上分析就没有什么意义了,需要通过其他方式解决。
    分析程序如何执行,通过软件调试工具(GDB、Windbg等)来断点调试是最常见的手段,但是这样的调试方式在JVM中会遇到很大困难,因为大量执行代码是通过JIT编译器动态生成到CodeBuffer中的,没有很简单的手段来处理这种混合模式的调试(不过相信虚拟机开发团队内部肯定是有内部工具的)。因此我们要通过一些曲线手段来解决问题,基于这种背景下,本文的主角——HSDIS插件就正式登场了。 

1.准备工作

  HSDIS是一份Sun官方推荐的HotSpot VM JIT编译代码的反汇编插件,它包含在HotSpot VM的源码之中,在Project Kenai(http://kenai.com/projects/base-hsdis)也可以下载到单独的源码。它的作用是让HotSpot的-XX:+PrintAssembly指令调用它来把动态生成的本地代码还原为汇编代码输出,同时还生成了大量非常有价值的注释,这样我们就可以通过输出的代码来分析问题。
    读者可以根据自己的操作系统和CPU类型从Kenai的网站上下载编译好的插件,直接放到JDK_HOME/jre/bin/client和JDK_HOME/jre/bin/server目录中即可。如果没有找到所需操作系统 (譬如Windows的就没有)的成品,那就得自己拿源码编译一下,或者去HLLVM圈子(http://hllvm.group.iteye.com/)中下载也可以。 
  如果你使用的是Debug或者FastDebug版的HotSpot,那可以直接通过-XX:+PrintAssembly指令使用的插件; 如果你使用的是Product版的HotSpot,那还要额外加入一个-XX:+UnlockDiagnosticVMOptions参数。
    我使用的Java版本是:

E:\data\bak>java -version

java version "1.7.0_06"

Java(TM) SE Runtime Environment (build 1.7.0_06-b24)

Java HotSpot(TM) Client VM (build 23.2-b09, mixed mode, sharing)

2.案例分析

    第一个案例的问题是“在Java虚拟机规范中把虚拟机内存划分为Java Heap、Java VM Stack、Method Area等多个运行时区域,那当ByteCode编译为Native Code后,Java堆、栈、方法区还是原来那个吗?在Java堆、栈、方法区中的数据是如何访问的?” 

  我们通过下面这段简单代码的实验来回答这个问题: 

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);

    }

}

    代码很简单,sum()方法使用到3个变量a、b、c,按照概念模型中的划分,其中a是实例变量,来自Java Heap,b是类变量,来自Method Area,c是参数,来自VM Stack。那我们来看看JIT之后,它们是怎么访问的。使用下面命令来执行上述代码:

java -Xcomp -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp -XX:CompileCommand=dontinline,*Bar.sum -XX:CompileCommand=compileonly,*Bar.sum Bar


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


整个JAVA文件汇编后的完整代码我放gist上了,地址如下:Java代码汇编后的完整代码

3.解读

代码并不多,一句一句来看: 
1.    mov %eax,-0x4000(%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 $0x239ae978,%esi:取test.Bar在方法区的指针。 
6.    mov 0x70(%esi),%esi:取类变量b,这次是访问“方法区”中的数据。 
7.    add %esi,%eax 、add %edx,%eax:做2次加法,求a+b+c的值,前面的代码把a放在eax中,把b放在esi中,而c在[Constants]中提示了,“parm0:edx = int”,说明c在edx中。 
8.    add $0x18,%esp:撤销栈帧。 
9.    pop %ebp:恢复上一栈帧。 
10.    test %eax,0x120100:轮询方法返回处的SafePoint 
11.    ret:方法返回。
  从汇编代码中可见,访问Java堆、栈和方法区中的数据,都是直接访问某个内存地址或者寄存器,之间并没有看见有什么隔阂。HotSpot虚拟机本身 是一个运行在物理机器上的程序,Java堆、栈、方法区都在Java虚拟机进程的内存中分配。在JIT编译之后,Native Code面向的是HotSpot这个进程的内存,说变量a还在Java Heap中,应当理解为a的位置还在原来的那个内存位置上,但是Native Code是不理会Java Heap之类的概念的,因为那并不是同一个层次的概念。

附:此文只是最简单的讲解,文章内容来自Iteye上LLVM圈子的讨论,如果需要更多的信息,可以参考此文:使用IdealGraphVisualizer观察HotSpot Server Compiler编译过程的一个例子,注意,如果想比较方便的看到汇编代码,最好使用fastDebug版本的JDK,而不是正式版。
如图所示:


idealgraphvisualizer显示了JAVA从语法解析到最终生成本地代码的完整过程
另有以下地址可参考:https://wikis.oracle.com/display/HotSpotInternals/PrintAssembly
http://ssw.jku.at/General/Staff/TW/igv.html

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!