Java虚拟机实践(1)——逐个字节分析.class文件字节码二进制内容

完完整整地剖析 一个由java编译器 编译java源程序 所生成的.class文件的结构/内容

1. 说明

注:为了日后研究以及查阅文章的便捷性,我将Test.class文件进行了备份,以便以后再进行分析时,只需要用二进制文件编辑器打开它即可,无需重新对源程序进行编译,Test.class文件的地址为:Test.class

我们将使用一个简单的程序Test.java,我们将其编译成Test.class文件,并且逐个字节地分析此.class文件的二进制文件内容,分析每个字节的含义,从而帮助我们理解Java编译器(javac)将.java文件编译后生成.class文件,.class文件的结构,内容到底是怎样的。

  • 这里使用到的IDE为IDEA,所以就让idea让我们编译源文件。
  • 为了查看.class文件的二进制形式,我们需要一个记事本NotePad++,同时需要在此记事本中安装插件Hex-Editor
  • 为了验证我们的分析结果是正确的,我们需要一个查看.class文件结构的工具:jclasslib,我们直接在IDEA中安装插件jclasslib即可

至于怎么安装上述的一些软件/插件,网上有很多教程,相信很容易就能够学会的,这里集中精力分析字节码

注意,我们这里查看的16进制是以Hex-Editor默认的Big-endian大端模式查看的

2. Java源文件

下面给出一个非常简单的Java源程序Test.java,我们将其放在jvm.bytecode包下。
我们现在可以简单地认为这个程序由如下元素构成:

  • 三个字段,其中一个静态字段
  • 三个函数,main函数,和字段i的getter,setter方法
package jvm.bytecode;

public class Test {

    String str = "welcome";
    private int i = 5;
    public static Integer integer = 10;

    public int getI() {
        return i;
    }

    public void setI(int i) {
        this.i = i;
    }

    public static void main(String[] args) {
        Test test = new Test();
        test.setI(8);
        integer = 20;
    }
}

3. 编译

现在,我们在IDEAbuild project一下,确认安装好jclasslib插件后,我们鼠标点击Test.java程序源代码处,然后按照图示,点击View菜单中的jclasslib工具,能够得到右侧的面版。这个工具用来验证我们分析的字节码是否正确,等到我们分析完一遍字节码,对字节码的结构有着清晰的认识之后,我们就能够在日常生活中直接使用此工具来查看.class文件的底层结构/内容,但是我们现在必须得先分析一遍字节码,后续才能更好地使用此工具。
在这里插入图片描述
现在,我们在out目录中找到编译后生成的字节码文件Test.class,我们使用NotePad++打开
在这里插入图片描述
在这里插入图片描述
现在,我们的任务就是完完全全地分析下面列出地每一个字节,以让我们对.class文件的结构有着清晰的认识
在这里插入图片描述
在这里插入图片描述

4. Class文件格式

首先,我们必须要知道class文件的内容,结构/框架是什么样子的。这样我们才能够按照这个格式来分析其内容。下图就是Class文件的格式。
在这里插入图片描述
我们现在只需要知道类型中的u1,u2,u4其实是它在字节码中所占的字节数分别为1,2,4

5. 文件头 ( 魔数+版本号 )

现在我们分析一下Class文件的前8个字节。
在这里插入图片描述
在这里插入图片描述
通过查看Class文件结构,我们将前8个字节称为文件头。

  • 前4个字节为magic,译作魔数
  • 后两个字节为minor_version,我们称之为Java的一个小版本号
  • 最后的两个字节为major_version,我们称之为Java的一个大版本号

大版本号_小版本号确定java的一个版本。我们可以看到小版本号为0,大版本号为0x34,也就是52,通过查表,可以得到版本为JDK1.8(至于具体的数字对应的版本,可以查表,这里不详细解释。)

最后我们看一看前4个字节cafebabe(咖啡宝贝)

也就是说,Java虚拟机要运行class字节码,.class文件必须要以·cafebabe开头,随后包含java编译器生成的class文件版本

在这文件头之后,才是我们真正编写代码后生成的内容。

6. 常量池

紧接着文件头的是所谓的常量池(constant_pool)。常量池部分的开头2字节表示有多少常量,随后就紧跟着这么多的的常量。
在这里插入图片描述
首先,我们看到常量的个数为0x31,也就是49个
在这里插入图片描述
现在我们来验证我们的想法是不是对的,我们查看IDEA Jclasslib面板的信息

在这里插入图片描述在这里插入图片描述
通过Jclasslib,我们看到只有48个常量。下面是深入理解Java虚拟机给出的解释

设计者将第0项空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项”的含义

对于每一个常量,它的信息需要查找下面的表来确定。
在这里插入图片描述在这里插入图片描述
常量信息的第一项是一个字节的Tag,用于表示常量的类型,据此我们通过查表,可以找到第一个常量所占的字节如下所示,0x0a表示其为方法引用信息(Constant_Methodref_info)
在这里插入图片描述
所以通过查表我们现在能够确定常量池的所有常量,耐心查表,然后对照jclasslib,看自己的分析结果是否正确,下面我给出了我的分析结果,与jclasslib的结果一致:

在这里插入图片描述

7. 类的基本信息

按照Class文件的格式,我们现在应该开始分析类的一般信息了。这里的一般信息我们可以理解为对应class类{}外的信息,比如:

package jvm.bytecode;

public class Test extends Base implements I1, I2{
	// ...
}

我们现在研究的就是这部分内容:
package jvm.bytecode;
public class Test extends Base implements I1, I2
在这里插入图片描述

  • access_flag表示类的访问修饰
  • this_class类索引。用来确定类的全限定名(这里的Test类的全限定名为jvm.bytecode.Test)
  • super_class。用来确定父类的全限定名(这里对应的应该是java.lang.Object)
  • interfaces_count和interfaces。用来表示类所实现的接口信息。

现在我们接着常量池字节码的后面,开始分析

首先我们根据下表来分析访问标识:
在这里插入图片描述
在这里插入图片描述

  • 访问标识符为0x21 = 0x20 | 0x01,所以访问标识为ACC_PUBLIC, ACC_SUPER

  • this_class标识的索引为0x05,通过查找常量池,我们知道0x05代表的常量值为jvm/bytecode/Test。也就是说this_class也就是Test的全限定名为jvm/bytecode/Test

  • super_class标识的索引为0x0a,通过查找常量池,我们知道0x0a代表的常量为java/lang/Object。也就是说super_class也就是Test父类的全限定名为java/lang/Object

  • interfaces_count为0,根据Test的源代码,我们知道,Test类并没有实现任何接口,所以interfaces_count确实为0。那么后面的interfaces字节码就不必分析了,它并不存在

到此为止,我们已经完成了Class的一般属性的分析。其实还挺简单。

8. 类的字段(Filed)

根据Class文件的结构,我们现在要接着分析类的字段信息了。
在这里插入图片描述
首先,紧跟着的是2个字节大小的数字,表示有多少个字段。而对于每个字段,它包含的字段信息由下表组成:
在这里插入图片描述
字段的信息包括访问标识access_flags名字索引name_index描述信息索引desc_index,还有若干个属性

其中,access_flags可以通过查下面的表来确定其访问修饰信息,而name_index, desc_index则要查找常量池的相关索引来确定其信息。
在这里插入图片描述
通过查表,我们能够将各个字段的信息划分出来:
在这里插入图片描述
首先00 03表示有3个字段信息,所以后面紧跟着3个字段信息。

  • 第一个字段。access_flags为00 00,表示没有任何访问修饰符,根据我们的源代码,我们可以初步判断此字段就是String str = "welcome";,因为我们并没有给它加任何访问修饰符。再看name_index为00 0b,通过查找常量池,可以知道其值为utf8字符串str。接着是desc_index值为00 0c,同理通过查找常量池得到其值为Ljava/lang/String;,表示此字段为一个引用String类型(L表示引用类型)。紧接着的00 00表示属性的个数为0,那么字段后面的属性信息我们就不需要考虑了。所以,通过这8个字节就确定了一个字段的信息为str : Ljava/lang/String

  • 第二个字段。同理,我们快速得可以得到字段的信息为 ACC_PRIVATE i I

    • access_flags == 0x02表示private
    • name_index == 0x0d,查常量池表得i
    • desc_index == 0x0e,查常量池表得I (大写的i),I是int的简写。
    • attr_count == 0,没有任何附加属性。

这里有必要说明一下各个数据类型在字段描述信息中的表示形式:
在这里插入图片描述

  • 同理,我们能够得到第三个字段的信息:
    ACC_PUBLIC ACC_STATIC integer Ljava/lang/Integer
    • access_flags == 0x09 == 0x08 | 0x01 表示ACC_PUBLIC,ACC_STATIC
    • name_index == 0x0f,查常量池表得integer
    • desc_index == 0x10,查常量池表得Ljava/lang/Integer
    • attr_count == 0,没有任何附加属性。

9. 类的方法信息(Method)

类的方法,是Java程序的业务逻辑核心所在,所以以字节码形式保存的方法信息是我们必须要深刻理解的。
与字段结构类似,都是开头2个字节标识信息的个数,这里是Method的个数
在这里插入图片描述
方法信息的结构,我们也需要知道:
在这里插入图片描述
我们可以看到,method_info方法结构与field_info字段结构是一一模一样。根据我们对字段信息的分析,似乎套用到Method信息上,有点不对劲。因为我们编写的方法,不仅仅只有一般的描述性信息,我们最重要的是函数中的代码,那么代码信息保存在哪里呢?答案是代码信息保存在attribute_info,也就是属性中。

我们另外,还需要知道方法的访问标志:
在这里插入图片描述

所以我们现在需要知道,属性项的内容到底是什么。下面列出了attr_info的构成:
在这里插入图片描述
我们现在开始分析method_info!!!
首先,我们根据methods_counts方法表结构属性表结构,能够准确的找到5个模块的方法字节码,如下图所示。

在这里插入图片描述
首先我们看到methods_count == 0x0005,知道:有5个方法。这似乎有点出乎我们的意料:在我们的源程序代码中,我们只编写了3个方法:getI,setI,main,但是经过编译后形成的字节码却有5个方法,不急,我们慢慢分析...

在分析之前我们需要给出一个Code属性表,方法得Code属性中保存着代码方法的代码信息:
在这里插入图片描述

9.1 方法1(<init>)

第一个方法的完整信息就是由如下字节码构成。
在这里插入图片描述

  • access_flags == 0x01ACC_PUBLICpublic方法
  • name_index == 0x11,查找常量池对应索引:<init>
  • desc_index == 0x12,查找常量池对应索引:()V
  • attr_count == 0x01,一个属性
    • attr_name_index == 0x13,查找常量池索引得属性名:Code

    • attr_length == 0x 00 00 00 42Code属性内容长度为66

    • max_stack == 0x02,操作数栈的最大深度为2

    • max_locals == 0x01,局部变量所需的存储空间(以slot为单位)

    • code_length == 0x 00 00 00 10,代码长度为16个字节

    • code (代码)

      2a aload_0
      b7 invokespecial 00 01 Methodref: java/lang/Object: <init>:()V)
      2a aload_0
      12 ldc 02 CONSTANT_String_info: "welcome"
      b5 putfield 00 03 Fieldref: jvm/bytecode/Test: str:Ljava/lang/String;
      2a aload_0
      08 iconst_5
      b5 putfield 00 04 Fieldref jvm/bytecode/Test: i:I
      b1 return
      /*****************************************************/
      // aload_0 将第一个引用类型本地变量推送至栈顶
      // invokespecial 调用超类构造方法,实例初始化方法,私有方法
      // ldc 将int/float/String常量值从常量池中推送至栈顶
      // putfield 为指定类的实例域赋值
      // iconst_5 将int型5推送至栈顶
      // return 从当前方法返回void

      我们能够从字节码中得到<init>方法做的事情如下:

      public void init(){ // ACC_PUBLIC init<> ()V
      // 由于Test并没有显示地继承自任何类,那么它调用地就是父类Object的<init>方法
      	super();// Object的<init>
      	this.str = "welcome";
      	this.i = 5;
      }
      

      我们可以将<init>理解为编译器自动添加的无参构造方法

    • exception_table_length == 0由于<init>方法不会抛出异常,所以不考虑异常表

    • attr_count == 0x02Code属性中内嵌2个属性

      • ①. attr_name_index == 0x14,Code的第1个属性名为LineNumberTable
        其中LineNumberTable属性的结构如下所示,其中每个line_number_info包含4个字节,前2个字节为start_pc,后2个字节为line_number前者为字节码行号,后者为Java源码行号。 LineNumber的作用就是将Java源码中的行号映射到与之对应的字节码行号。
        在这里插入图片描述
      • attr_length == 0x0e 属性长度为14
      • line_number_table_length == 0x03,3个行号映射信息
      • start_pc -> line_number
        0x00 -> 0x03
        0x04 -> 0x05
        0x0a -> 0x06
      • ②. attr_name_index == 0x15 ,Code的第2个属性名LocalVariableTable
        LocalVariableTable的属性结构如下图所示。
        在这里插入图片描述
      • attr_length == 0x0c,属性长度为12个字节,恰好就是剩下的12个字节。
      • local_var_table_length == 0x01,一个local_var表项
      • local_variable_info信息
        start_pc == 0x00,局部变量声明周期开始的字节码偏移量
        length == 0x10 局部变量的作为范围长度
        name_index == 0x16 变量名this
        desc_index == 0x17 变量类型Ljvm/bytecode/Test;
        index == 0x00 局部变量在栈帧局部变量表中的slot位置

9.2 方法2(getI)

第2个方法的完整信息就是由如下字节码构成。
在这里插入图片描述

  • access_flags == 0x01ACC_PUBLICpublic方法
  • name_index == 0x18,查找常量池对应索引:getI
  • desc_index == 0x19,查找常量池对应索引:()I
  • attr_count == 0x01,一个属性
    • attr_name_index == 0x13,查找常量池索引得属性名:Code

    • attr_length == 0x 00 00 00 2fCode属性内容长度为47

    • max_stack == 0x01,操作数栈的最大深度为1

    • max_locals == 0x01,局部变量所需的存储空间(以slot为单位)

    • code_length == 0x 00 00 00 05,代码长度为5个字节

    • code (代码)

      2a aload_0
      b4 getfield 00 04 // Fieldref: jvm/bytecode/Test i:I
      ac ireturn // 从当前方法返回int

      我们再对照一下Java源码:

        public int getI() {
            return i; // this.i;
        }
      
    • exception_table_length == 0x00 没有异常信息表

    • attr_count == 0x02Code有2个属性

      • ①. attr_name_index == 0x14,属性名为LineNumberTable
      • attr_length == 0x06,属性长度为6个字节
      • line_number_table_length == 0x01一个line_number_info
        start_pc->line_number : 0x00 -> 0x0b
      • ②. attr_name_index == 0x15,属性名为LocalVariableTable
      • attr_length == 0x0c,属性长度为12个字节
      • local_var_table_length == 0x01,一个local_var表项
      • local_variable_info信息
        start_pc == 0x00,局部变量声明周期开始的字节码偏移量
        length == 0x05 局部变量的作为范围长度
        name_index == 0x16 变量名this
        desc_index == 0x17 变量类型Ljvm/bytecode/Test;
        index == 0x00 局部变量在栈帧局部变量表中的slot位置

9.3 方法3(setI)

第3个方法的完整信息就是由如下字节码构成。
在这里插入图片描述

  • access_flags == 0x01ACC_PUBLICpublic方法
  • name_index == 0x1a,查找常量池对应索引:setI
  • desc_index == 0x1b,查找常量池对应索引:(I)V
  • attr_count == 0x01,一个属性
    • attr_name_index == 0x13,查找常量池索引得属性名:Code

    • attr_length == 0x 00 00 00 3eCode属性内容长度为62

    • max_stack == 0x02,操作数栈的最大深度为2

    • max_locals == 0x02,局部变量所需的存储空间(以slot为单位)

    • code_length == 0x 00 00 00 06,代码长度为6个字节

    • code (代码)

      2a aload_0
      1b iload_i
      b5 putfield 00 04 // Fieldref: jvm/bytecode/Test i:I
      b1 return

      我们可以对照一下Java源码:

       public void setI(int i) {
          this.i = i; 
       }
      
    • exception_table_length == 0x00 没有异常信息表

    • attr_count == 0x02Code有2个属性

      • ①. attr_name_index == 0x14,属性名为LineNumberTable
      • attr_length == 0x0a,属性长度为10个字节
      • line_number_table_length == 0x022个line_number_info
        start_pc->line_number : [0x00 -> 0x0f],[0x05 -> 0x10]
      • ②. attr_name_index == 0x15,属性名为LocalVariableTable
      • attr_length == 0x16,属性长度为22个字节
      • local_var_table_length == 0x02,2个local_var表项
      • 1. )local_variable_info信息
        start_pc == 0x00,局部变量声明周期开始的字节码偏移量
        length == 0x06 局部变量的作为范围长度
        name_index == 0x16 变量名this
        desc_index == 0x17 变量类型Ljvm/bytecode/Test;
        index == 0x00 局部变量在栈帧局部变量表中的slot位置
      • 2. )local_variable_info信息
        start_pc == 0x00,局部变量声明周期开始的字节码偏移量
        length == 0x06 局部变量的作为范围长度
        name_index == 0x0d 变量名i
        desc_index == 0x0e 变量类型I
        index == 0x01 局部变量在栈帧局部变量表中的slot位置

9.3 方法4(main)

第4个方法的完整信息就是由如下字节码构成。
在这里插入图片描述

  • access_flags == 0x09 == 0x01 | 0x08ACC_PUBLIC,即ACC_STATICpublic static方法
  • name_index == 0x1c,查找常量池对应索引:main
  • desc_index == 0x1d,查找常量池对应索引:([Ljava/lang/String;)V
  • attr_count == 0x01,一个属性
    • attr_name_index == 0x13,查找常量池索引得属性名:Code

    • attr_length == 0x 00 00 00 57Code属性内容长度为87

    • max_stack == 0x02,操作数栈的最大深度为2

    • max_locals == 0x02,局部变量所需的存储空间(以slot为单位)

    • code_length == 0x 00 00 00 17,代码长度为23个字节

    • code (代码)

      new: 创建一个对象,并将其引用值压入栈顶
      bb new 00 05 // Class_info: jvm/bytecode/Test
      59 dup // 复制栈顶数值并将其压入栈顶
      b7 invokespecial 00 06 // Methodref: jvm/bytecode/Test: <init>:()V
      4c astore_1 // 将栈顶引用型数值存入第2个本地变量
      2b aload_1 // 将第二个引用类型本地变量推送至栈顶
      10 bipush // 将单字节(-128~127)常量值推送至栈顶 08
      b6 invokevirtual 00 07 // Methodref: jvm/bytecode/Test: setI:(I)V
      10 bipush 14
      b8 invokestatic 00 08 // java/lang/Integer: valueOf:(I)Ljava/lang/Integer;
      b3 putstatic 00 09 // Fieldref: jvm/bytecode/Test: integer:Ljava/lang/Integer;
      b1 return

      我们可以对照一下Java源码:

       public static void main(String[] args) {
          Test test = new Test();
          test.setI(8);
          integer = 20;
      }
      
    • exception_table_length == 0x00 没有异常信息表

    • attr_count == 0x02Code有2个属性

      • ①. attr_name_index == 0x14,属性名为LineNumberTable
      • attr_length == 0x12,属性长度为18个字节
      • line_number_table_length == 0x044个line_number_info
        start_pc->line_number : [0x00 -> 0x13],[0x08 -> 0x14],[0x0e, 0x15],[0x16, 0x16]
      • ②. attr_name_index == 0x15,属性名为LocalVariableTable
      • attr_length == 0x16,属性长度为22个字节
      • local_var_table_length == 0x02,2个local_var表项
      • 1. )local_variable_info信息
        start_pc == 0x00,局部变量声明周期开始的字节码偏移量
        length == 0x17 局部变量的作为范围长度
        name_index == 0x1e 变量名args
        desc_index == 0x1f 变量类型[Ljava/lang/String;
        index == 0x00 局部变量在栈帧局部变量表中的slot位置
      • 2. )local_variable_info信息
        start_pc == 0x08,局部变量声明周期开始的字节码偏移量
        length == 0x0f 局部变量的作为范围长度
        name_index == 0x20 变量名test
        desc_index == 0x17 变量类型Ljvm/bytecode/Test;
        index == 0x01 局部变量在栈帧局部变量表中的slot位置

9.3 方法5(<clinit>)

第5个方法的完整信息就是由如下字节码构成。
在这里插入图片描述

  • access_flags == 0x08ACC_STATICstatic方法
  • name_index == 0x21,查找常量池对应索引:<clinit>
  • desc_index == 0x12,查找常量池对应索引:()V
  • attr_count == 0x01,1个属性
    • attr_name_index == 0x13,查找常量池索引得属性名:Code

    • attr_length == 0x 00 00 00 21Code属性内容长度为33

    • max_stack == 0x01,操作数栈的最大深度为1

    • max_locals == 0x00,局部变量所需的存储空间(以slot为单位)

    • code_length == 0x 00 00 00 09,代码长度为9个字节

    • code (代码)

      10 bipush 0a
      b8 invokestatic 00 08 // java/lang/Integer: valueOf:(I)Ljava/lang/Integer;
      b3 putstatic 00 09 // Fieldref: jvm/bytecode/Test: integer:Ljava/lang/Integer;
      b1 return

      很显然,<clinit>编译器自动生成的一个为静态成员初始化的函数。

      public static Integer integer = 10;
      
    • exception_table_length == 0x00 没有异常信息表

    • attr_count == 0x01Code有1个属性

      • ①. attr_name_index == 0x14,属性名为LineNumberTable
      • attr_length == 0x06,属性长度为6个字节
      • line_number_table_length == 0x011个line_number_info
        start_pc->line_number : [0x00 -> 0x08]

10. 类的属性(Attribute)

根据.class文件的结构,我们现在已经分析到类文件的最后一部分了!!属性
在这里插入图片描述
首先我们查看一下剩下的仍未分析过的字节码
在这里插入图片描述
00 01 == attr_count 属性的个数为1个
00 22 == attr_name_index 属性的名字为"SourceFile"
00 00 00 02 == attr_length 属性的长度为2个字节
00 23 生成此class文件的源代码文件名称为Test.java

11. 最后

到目前为止,我们已经完完整整地分析完整个class文件的二进制含义了。
其中常量池方法区是最为重要的部分,分析过程非常繁琐。

分析的开始就是常量池,它为后面的方法表等各个表提供了具体的语义。

  • 值得注意的是,当我们没有为类指定构造函数的时候,编译器会为我们自动生成一个<init>方法,充当类的默认无参构造器实例字段会在此init方法中进行初始化

  • 与之对应的,编译器同时也会自动一个<clinit>方法,用来初始化静态字段/代码块

  • 分析方法的汇编代码/助记符是很有意思的,因为它不同于广为我们所知的基于寄存器的指令集java编译器生成的指令流,是基于栈的指令集

现在,我们可以说已经掌握了.class文件字节码的基本结构与分析方法,我们现在有足够的信心来使用class文件查看工具来提升我们的工作效率。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值