• 【JVM技术专题】针对于Class字节码的文件分析和研究指南 「 进阶篇」


    任何足够先进的科技,都与魔法无异

    承接上篇:

    【JVM技术专题】针对于Class字节码的文件分析和研究指南 「 入门篇」

    字段表集合

    字段表集合用于描述接口或者类中声明的变量,这里的数据为:0000。

    fields_count 字段计数器

    fields_count 的值表示当前 Class 文件 fields[]数组的成员个数。 fields[]数组中每一项都是一个 field_info 结构的数据项,它用于表示该类或接口声明的类字段或者实例字段。

    fields[] 字段表

    fields[]数组中的每个成员都必须是一个 fields_info 结构的数 据项,用于表示当前类或接口中某个字段的完整描述。fields[]数组描述当前类或接口 声明的所有字段,但不包括从父类或父接口继承的部分。

    field_info 结构格式如下:

    field_info { 
        u2 access_flags;
        u2 name_index;
        u2 descriptor_index;
        u2 attributes_count;
        attribute_info attributes[attributes_count];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    属性集合

    attributes_count 属性计数器

    attributes_count 的值表示当前 Class 文件 attributes 表的成员个 数。attributes 表中每一项都是一个 attribute_info 结构的数据项。

    attributes[]属性表

    属性表,attributes 表的每个项的值必须是 attribute_info 结构(§4.7)。

    属性(Attributes)在 Class 文件格式中的 ClassFile 结构、field_info 结构,method_info 结构和 Code_attribute 结构都有使用,所有属性的通用格式如下:


    对于任意属性,attribute_name_index 必须是对当前 Class 文件的常量池的有效16 位无符号索引。常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示当前属性的名字。attribute_length 项的值给出了跟随其后的字节的长度,这个长度不包括 attribute_name_index 和 attribute_name_index 项的 6 个字节。

    有些属性因 Class 文件格式规范所需,已被预先定义好。这些属性在表 4.6 中列出,同时,被列出的信息还包括它们首次出现的Class文件版本和 Java SE 版本号。在本规范定义的环境 中,也就是已包含这些预定义属性的 Class 文件中,它们的属性名称被保留,不能再被属性表中其他的自定义属性所使用。 下表是 class 文件的属性:

    方法表集合

    在字段表后的 2 个字节是一个方法计数器,表示类中总有有几个方法,在字段计数器后,才是具体的方法数据。这里数据为:0002 。

    methods_count 方法计数器

    methods[] 数组中的每个成员都必须是一个 method_info 结构的 数据项,用于表示当前类或接口中某个方法的完整描述。如果某个 method_info 结构 的 access_flags 项既没有设置 ACC_NATIVE 标志也没有设置 ACC_ABSTRACT 标志, 那么它所对应的方法体就应当可以被 Java 虚拟机直接从当前类加载,而不需要引用其它 类。method_info 结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、 实例初始化方法方法和类或接口初始化方法方法。methods[]数组 只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。

    method_info { 
        u2 access_flags;
        u2 name_index;
        u2 descriptor_index;
        u2 attributes_count;
        attribute_info attributes[attributes_count];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    Log 类的字节码文件中,方法计数器的值为 00 02,表示一共有 2 个方法。

    第一个方法:

    0001 000700 0800 0100 0900 0000 1d00 0100 0100 0000 052a b700 01b1 0000 0001 000a 0000 0006 0001 0000 0003

    • 方法计数器后 2 个字节表示方法访问标识,这里是 0001,表示其实 ACC_PUBLIC 标识,对比上面的图表可知其表示 public 访问标识。
    • 紧接着 2 个字节表示方法名称的索引,这里是 00 07 表示指向了常量池第 7 个常量,查阅可知其指向了。
    • 紧接着的 2 个字节表示方法描述符索引项,这里是 00 08 表示指向了常量池第 8 个常量,查阅可知其指向了()V。
    • 紧接着 2 个字节表示属性表计数器,这里是 00 01 表示该方法的属性表一共有 1 个属性。属性表的表结构如下:
    
    attribute_info {
        u2 attribute_name_index;
        u4 attribute_length;
        u1 info[attribute_length];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    前两个字节是名字索引、接着 4 个字节是属性长度、接着是属性的值。这里前两个字节为 0009,指向了常量池第 9 个常量,查询可知其值为 Code,说明此属性是方法的字节码描述。


    Code 属性是一个变长属性,位于method_info结构的属性表。一个 Code 属性只为唯一一个方法、实例类初始化方法或类初始化方法保存 Java 虚拟机指令及相关辅助信息。** 所有 Java 虚拟机实现都必须能够识别 Code 属性。如果方法被声明为 native 或者 abstract 类型,那么对应的 method_info 结构不能有明确的 Code 属性,其它情况下, method_info 有必须有明确的 Code 属性。**

    Code 属性的格式如下:

    Code_attribute {
        u2 attribute_name_index; 
     u4 attribute_length;
        u2 max_stack;
        u2 max_locals;
        u4 code_length;
        u1 code[code_length];
        u2 exception_table_length; { u2 start_pc;
                u2 end_pc;
                u2 handler_pc;
                u2 catch_type;
        } exception_table[exception_table_length];
        u2 attributes_count;
        attribute_info attributes[attributes_count];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 根据 Code 属性对应表结构知道,前 2 个字节为 0009,即常量池第 9 个常量,查询知道是字符串常量 Code。
    • 接着 4 个字节表示属性长度,这里值为 0000 001d,即 29 的长度。下面我们继续分析 Code 属性的数据内容。
    • 紧接着 2 个字节为 max_stack 属性。这里数据为 00 01,表示操作数栈深度的最大值。
    • 紧接着 2 个字节为 max_locals 属性。这里是数据为 00 01,表示局部变量表所需的存储空间为 1 个 Slot。在这里 max_locals 的单位是 Slot,Slot 是虚拟机为局部变量分配内存所使用的最小单位。
    • 接着 4 个字节为 code_length,表示生成字节码这里给的长度。
    • 这里数据为 00 00 00 05,表示生成字节码长度为 5 个字节。

    那么紧接着 5 个自己就是对应的数据,这里数据为 2a b7 00 01 b1,这一串数据其实就是字节码指令。通过查询字节码指令表,可知其对应的字节码指令:


    读入 2A,查表得 0x2A 对应的指令为 aload_0,这个指令的含义是将第 0 个 Slot 中为 reference 类型的本地变量推送到操作数栈顶。


    读入 B7,查表得0xB7对应的指令为 invokespecial,这条指令的作用是以栈顶的 reference 类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private 方法或者它的父类的方法。这个方法有一个 u2 类型的参数说明具体调用哪一个方法,它指向常量池中的一个 CONSTANT_Methodref_info 类型常量,即此方法的方法符号引用。

    读入 00 01,这是 invokespecial 的参数,查常量池得 0x0001 对应的常量为实例构造器“”方法的符号引用。

    读入 B1,查表得0xB1对应的指令为 return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束。

    着 2 个字节为异常表长度,这里数据为 00 00,表示没有异常表数据。那么接下来也就不会有异常表的值。

    紧接着 2 个字节是属性表的长度,这里数据为 00 01,表示有一个属性。该属性长度为一个 attribute_info 那么长。

    首先,前两个字节表示属性名称索引,这里数据为:00 0A。指向了第 10 个常量,查阅可知值为:LineNumberTable。

    LineNumberTable 属性是可选变长属性,位于 Code(§4.7.3)结构的属性表。它被调试 器用于确定源文件中行号表示的内容在 Java 虚拟机的 code[]数组中对应的部分。在 Code 属性 的属性表中,LineNumberTable 属性可以按照任意顺序出现,此外,多个 LineNumberTable 属性可以共同表示一个行号在源文件中表示的内容,即 LineNumberTable 属性不需要与源文件 的行一一对应。

    LineNumberTable 属性格式如下:

    LineNumberTable_attribute {
        u2 attribute_name_index;
        u4 attribute_length;
        u2 line_number_table_length; 
        { 
            u2 start_pc;
            u2 line_number;
        } line_number_table[line_number_table_length];
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    其前两个字节是属性名称索引,就是上面已经分析过的 00 0A。

    接着 4 个字节是属性长度,这里数据为 00 00 00 06,表示有 6 个字节的数据。接着 2 个字节是 LineNumberTable 的长度,这里数据是 00 01,表示长度为 1。接着跟着 1 个 line_number_info 类型的数据,下面是 line_number_info 表的结构,其包含了 start_pc 和 line_number 两个 u2 类型的数据项。前者是字节码行号,后者是 Java 源码行号。

    那么接下来 2 个字节为 00 00,即 start_pc 表示的字节码行号为第 0 行。接着 00 03,即 line_number 表示 Java 源码行号为第 3 行。

    从上面的反编译来看,上面的分析也是对的:

    {
      public com.hello.test.Log();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."":()V
             4: return
          LineNumberTable:
            line 3: 0
    
      public static void main(java.lang.String[]);
        descriptor: ([Ljava/lang/String;)V
        flags: ACC_PUBLIC, ACC_STATIC
        Code:
          stack=2, locals=1, args_size=1
             0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
             3: ldc           #3                  // String hello world!
             5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
             8: return
          LineNumberTable:
            line 5: 0
            line 6: 8
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    第二个方法这里就不详细分析了,大家可以自己对着上面的反编译结果进行分析。
    第二个方法开头是 0009,表示的是 ACC_PUBLIC 与 ACC_STATIC 合在一起的结果。

  • 相关阅读:
    「纯干货」接口项目/接口框架通通都有,都给你
    弹性数据库连接池探活策略调研(二)——Druid
    在业务开发中遇到的树形结构(部门、区域、职位),递归处理。
    LSF如何看job预留slot是否合理?
    推荐系统笔记(四):NGCF推荐算法理解
    kali——tcpdump的使用
    TPA3045-ASEMI光伏二极管TPA3045
    WPF 主窗口最大化、最小化、关闭、可拖动
    【玩转 Cloud Studio】 Cloud Studio的入门教程
    【嵌入式】堆栈与单片机内存
  • 原文地址:https://blog.csdn.net/l569590478/article/details/127414072