• JVM: 字节码解析


    参考书目:《Java虚拟机规范》/第四章:class文件解析

    不同语言编写的源文件(e.g. java, groovy, kotlin, scala)编译后生成的.class字节码文件都能在JVM上运行。

     

    小端&大端 – 从内存中读取数据的两种方式

    大端模式:低地址存放数据的高位,高地址存放数据的低位。

    小端模式:低地址存放数据的低位,高地址存放数据的高位。

    字节码在JVM(计算机)内存中是小端存储的。在写JVM时需要用大端模式读取。证明:

    void JavaVirtualMachine::parse_single_class(JavaClassContent *class_content) {
        printf(“%X 
    ”, *(int*)class_content->get_content_ptr());
        printf(“%X
    ”, htonl(*(int*)class_content->get_content_ptr())); //htonl: 转换为大端模式
        …
    }

    输出:BEBAFECA  CAFEBABE

    原因:小端;大端

     

     

    描述符

    根据 Class文件结构中常量池中11种数据类型的结构总表, CONSTANT_Methodref_Info、CONSTANT_InterfaceMethodref_Info和CONSTANT_NameAndType_Info中都含有CONSTANT_NameAndType_info。而CONSTANT_NameAndType_Info中又包含名称和描述符。

    数据类型的描述符

    void类型 V e.g. ()v 类型为v的方法

    引用类型 e.g. String的描述符: Ljava/lang/String; //引用类型后面记得有分号

    数组类型 e.g. byte数组的描述符: [B

             e.g. String数组的描述符: [Ljava/lang/String; 

    方法的描述符

     (参数数据类型的描述符)返回值的描述符

    如果有多个参数,描述符用逗号分隔

    e.g.  javap –verbose输出中查看到main方法的描述符

    public static void main(java.lang.String[]);

      descriptor: ([Ljava/lang/String;]V //main方法的描述符

    e.g.  某个方法的描述符

    ([[Ljava/lang/String;. I, Ljava/AA/BB;]Ljava/lang/String;

    原形:String XXX(String[][], int, BB)

     

     

    实验:手动解析字节码

    Example java code:

    public class Simple {
        static int a = 10;
        public static void main(String[] args) {}
    }

    使用hexdump -C查看字节码文件,再打开jclasslib对比确认手动解析结果是否正确。

    class文件的组成部分

     uX:占X个字节。

    !:字节长度不确定,需要动态计算。

     (u4): 值为ca fe ba be。每个Java class文件都以0xCAFEBABE开头。用于判断一个文件是否为合格class文件。如果不是则不执行。

     

    次版本号 (u2) & 主版本号 (u2):JDK的版本对应的major、minor号。

    e.g.  00000000  ca fe ba be 00 00 00 34  00 19 0a 00 04 00 15 09

    次版本号为0,主版本号为16进制34转为10进制52,所以表示jdk版本为1.8。jclasslib查看genral information可证。

     

    常量池大小 (u2):常量池个数。

    *注意:常量池最小index不是0而是1。真实常量池个数=字节码文件中常量池个数-1。

    e.g.  00000000  ca fe ba be 00 00 00 34  00 19 0a 00 04 00 15 09

    00 19->25,真实常量池个数为25-1=24。 Jclasslib中查看Constant Pool可证。

    常量池:每个数据结构tag都占前1个字节。根据tag值对应数据类型的结构解析其中存储的信息。

    CONSTANT Fieldref_info {
        u1 tag;
        u2 class_index; 
        u2 name_and_type_index;
    }
    
    CONSTANT_Methodref_info {
         u1 tag;
        u2 class_index;
        u2 name_and_type_index;
    }
    
    CONSTANT_Class_info {
        u1 tag;
        u2 name_index;
    }
    …

    常量池中的11种数据结构类型:

    e.g.  解析常量池

    常量池中第1个元素:

    00000000  ca fe ba be 00 00 00 34  00 19 0a 00 04 00 15 09

    tag: 0a==10 –> CONSTANT_Methodref

    class_index: 00 04 == 4 –> 04位置还没解析出来,这里只能放符号引用,后面才能转为直接引用

    name_and_type: 00 15 ==21

    Jclasslib中查看Constant Pool中第1个元素可证。

    常量池中第2个元素:

    00000000  ca fe ba be 00 00 00 34  00 19 0a 00 04 00 15 09

    00000010  00 03 00 16 07 00 17 07 00 18 01 00 01 61 01 00

    tag: 09 ==9 -> CONSTANT_Fieldref_info

    class_index: 00 03 ==3 -> 符号引用第3个元素,所以该field的类型为第3个元素的类。

    name_and_type: 00 16 ==22

    Jclasslib中查看Constant Pool中第2个元素可证。查看静态常量池也可证明符号引用了第3个元素,因为#2 field的值为#3。

    常量池中第3个元素:

    00000010  00 03 00 16 07 00 17 07 00 18 01 00 01 6101 00

    tag: 07 ==7 -> CONSTANT_Class_Info

    name_index: 17

    常量池中第4个元素:

    00000010  00 03 00 16 07 00 17 07 00 18 01 00 01 61 01 00

    tag: 07 ==7 -> CONSTANT_Class_Info

    常量池中第5个元素:

    00000010  00 03 00 16 07 00 17 07 00 18 01 00 01 61 01 00

    tag: 01 ==1 -> CONSTANT_Utf8_info,说明是String类型

    length: 00 01==1 –> 长度为1,所以byte只往后取1位

    bytes: 61 == 97 -> ASCII码对应‘a’

    access flags (u2)

    通过或运算解析access flag值的含义.e.g. 0x11表示public final

    e.g.  解析access flag

    00000120  61 6e 67 2f 4f 62 6a 65  63 74 00 21 00 03 00 04

    0x0021 -> public

     

    this_class (u2): 当前类

    e.g.  解析this class

    00000120  61 6e 67 2f 4f 62 6a 65  63 74 00 21 00 03 00 04

    查看静态常量池,当前类是常量池中第3个元素

     

    super_class (u2):

    e.g.  解析super class

    00000120  61 6e 67 2f 4f 62 6a 65  63 74 00 21 00 03 00 04

    查看静态常量池,super class是常量池中第4个元素

    interfaces_count (u2): 接口数量

    e.g.  解析Interfaces_count

    00000130  00 00 00 01 00 08 00 05  00 06 00 00 00 03 00 01

    接口数量是0。

    *一个类最多可以实现多少个接口?

    65535。//因为接口数量占2个字节=2*8=16位,二进制16位数最大为1111 1111 1111 1111=十进制2^16-1。

    interfaces[] (!): 如果接口数量是0,则该区域在字节码文件中不会出现。

    fields_count[] (u2): fields(类的属性)的数量

    e.g.  解析fields数量

    00000130  00 00 00 01 00 08 00 05  00 06 00 00 00 03 00 01

    fields_info (!): field在常量池中的存储方式。

    field_info {
        u2 access_flags;
        u2 name_index; //指向常量池
        u2 descriptor_index;
        u2 attributes_count; //属性数量,
        attribute_info attributes[attributes_count]; //属性内容
    }

    attributes指的是field的属性(e.g. 加了final就会有ConstantValue属性)。如果attributes_count为0,attributes区域在字节码文件中就不会出现。

    e.g.  解析一个field

    先前fields_count的解析结果说明该类中只有一个field。

    00000130  00 00 00 01 00 08 00 05  00 06 00 00 00 03 00 01

    第1个属性:

      access_flags: 00 08 -> static (参照类访问和属性修饰符标志 表)
      name_index: 00 05 -> 指向常量池中第5个元素

      descriptor_index: 00 06

      attribute_count: 00 00

      attributes: 因为count为0,attributes没有出现

    methods_count (u2):方法数量

    e.g.解析方法数量

    00000130  00 00 00 01 00 08 00 05  00 06 00 00 00 03 00 01

    -> 有3个方法:静态方法clint、默认构造方法init、main。

    methods_info (!):方法

    method_info {
        u2 access_flags;
        u2 name_index;
        u2 descriptor_index;
        u2 attributes_count; //属性、Code、Exception
        attribute_info attributes[attributes_count]; 
    }

    如果attributes_count为0,attributes区域在字节码文件中就不会出现。

    Code_attribute {
        u2 attribute_name_index; //即为method_info中解析出的attributes值
        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]; //Code属性的属性
    }

    exceptions分别在method_info和Code_attribute中都有记录,可验证得知throws异常和try catch异常在字节码中有不同的表示。

    Code_attribute中有max_stack和max_locals,说明操作数栈和局部变量表的大小在编译时就已经确定。

    Jclasslib打开/Methods/方法名/Code,在specific info查看指令内容。

    Code的属性包括LineNumberTable(代码行号,存储Java代码所在行数)和LocalVariableTable(局部变量表,存储参数和局部变量),Jclasslib打开/Methods/方法名/Code可查。

    e.g.解析第1个方法

    00000130  00 00 00 01 00 08 00 05  00 06 00 00 00 03 00 01

    00000140  00 07 00 08 00 01 00 09  00 00 00 2f 00 01 00 01

    access_flag: 00 01

    name_index: 00 07 

    descriptor_index: 00 08

    attr_count: 00 01

    attributes: 00 09

    name_index==7 -> 常量池中第7个元素为CONSTANT_Utf8_info, String为<init>,说明是构造方法。

    Attributes==9 -> 第1个属性为Code,按照Code_attribute结构继续解析:

    e.g. 解析init方法的code属性

    00000140  00 07 00 08 00 01 00 09  00 00 00 2f 00 01 00 01

    00000150  00 00 00 05 2a b7 00 01  b1 00 00 00 02 00 0a 00

      attribute_name_index: 00 09 //00 09为Code_attribute的attribute_name_index值
      attribute_length: 00 00 00 2f 

     max_stack: 00 01

      max_locals: 00 01

      code_length: 00 00 00 05

      code: 2a b7 00 01 b1

      exception_table_length: 00 00

      exception_table:

      attributes_count: 00 02

    attribute_length==47,说明从attribute_length往后所有东西加起来共47个字节。

    方法体:

      0x2a == 42 -> aload_0

      …

      0xb1 == 177 -> return

    Jclasslib->Methods/<init>/Code查看Bytecode可证:

      aload_0
      invokespecial #1 <java/lang/Object.<init>>
      return

    根据attributes_count确定code有2个attributes后,继续解析code的attributes:

    00000150  00 00 00 05 2a b7 00 01  b1 00 00 00 02 00 0a 00

    00000160  00 00 06 00 01 00 00 00  07 00 0b 00 00 00 0c 00

    Code的第1个属性:

      attr_name_index: 00 0a 

    Code第1个属性的attr_name_index==10, jclasslib查看常量池第10个元素为CONSTANT_Utf8_info, 其String值为LineNumberTable。所以第1个属性是代码行号表。

    确定当前code属性为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];
    }

    attr_name_index: 00 0a

    attribute_length: 00 00 00 06

    line_number_table_length: 00 01

      [ start_pc: 00 00

       line_number: 00 07 ]

    Code的第2个属性:

    00000160  00 00 06 00 01 00 00 00  07 00 0b 00 00 00 0c 00

    00000170  01 00 00 00 05 00 0c 00  0d 00 00 00 09 00 0e 00

    attr_name_index: 00 0b

    Code第2个属性的attr_name_index==11, jclasslib查看常量池第11个元素为CONSTANT_Utf8_info, 其String值为LocalVariableTable。所以第2个属性是局部变量表。

    确定当前code属性为LocalVariableTable后,根据LocalVariableTable的结构继续解析:

    LocalVariableTable_attribute {
        u2 attribute_name_index;
        u4 attribute_length;
        u2 local_variable_table_length;
        { u2 start_pc;
           u2 length;
           u2 name_index;
           u2 descriptor_index;
           u2 index;
         } local_variable _table[local_variable _table_length];
    }

    attr_name_index: 00 0b

    attribute_length: 00 00 00 0c

    local_variable_table_length: 00 01

      [ start_pc: 00 00

       length: 00 05

       name_index: 00 0c

       descriptor_index: 00 0d

       index: 00 00 ]

     

    attributes_count (u2)

    attributes[] (!)

    attribute_info {
        u2 attribute_name_index;
        u4 attribute_length;
        u1 info[attribute_length];
    }
  • 相关阅读:
    java web数据可视化
    全国疫情统计可视化地图
    数组中的学问
    软件工程第二周开课博客
    梦断代码阅读笔记1
    补充urllib
    多用户登录
    学期课后个人总结
    团队冲刺第二十六天
    团队冲刺第二十五天
  • 原文地址:https://www.cnblogs.com/RDaneelOlivaw/p/13529474.html
Copyright © 2020-2023  润新知