• JVM字节码(二)


    在分析完常量池后,我们来看下Java字节码的整体结构:

    魔术码(Magic Number) 4个字节
    版本(version) 2+2个字节
    常量池(Constant pool) 2+n个字节
    访问标志(Access flags) 2个字节
    当前类(This Class Name) 2个字节
    父类(Super Class Name) 2个字节
    接口(Interfaces) 2+n个字节
    字段(Fields) 2+n个字节
    方法(Methods) 2+n个字节
    属性 (Attributes) 2+n个字节

    整个class文件,如果我们想用对象的形式来表达,可以编写成如下形式:

    ClassFile { 
    	u4 magic; 
    	u2 minor_version; 
    	u2 major_version; 
    	u2 constant_pool_count; 
    	cp_info constant_pool[constant_pool_count-1]; 
    	u2 access_flags; 
    	u2 this_class; 
    	u2 super_class; 
    	u2 interfaces_count; 
    	u2 interfaces[interfaces_count]; 
    	u2 fields_count; 
    	field_info fields[fields_count]; 
    	u2 methods_count; 
    	method_info methods[methods_count]; 
    	u2 attributes_count; 
    	attribute_info attributes[attributes_count]; 
    }
    

      

    其中constant_pool_count、interfaces_count、fields_count、methods_count和attributes_count分别代表常量池元素数量、接口数量、字段数量、方法数量、属性数量。

    可以看到,在常量池之后,就是类的访问标志,我们来看下标志的类型:

    标志名称 标志值 含义
    ACC_PUBLIC 0x0001 访问标志是否为public
    ACC_PRIVATE 0x0002 访问标志是否为private
    ACC_PROTECTED 0x0004 访问标志是否为protected
    ACC_STATIC 0x0008 是否被标记为static
    ACC_FINAL 0x0010 是否被标记为final 
    ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语义
    ACC_VOLATILE 0x0040 是否被标记为volatile
    ACC_TRANSIENT 0x0080 是否被标记为transient
    ACC_INTERFACE 0x0200 标志这是一个接口
    ACC_ABSTRACT 0x0400 标志这是一个抽象类
    ACC_SYNTHETIC 0x1000 标志这个类并非由用户代码产生
    ACC_ANNOTATION 0x2000 标志这是一个注解
    ACC_ENUM 0x4000 标志这是一个枚举

    我们看MyTest1.class文件转换为十六进制之后的内容:

    根据字节码结构,我们知道类的访问标志占两个字节,MyTest1常量池之后的两个字节是0x0021,对比之前的访问标志类型,我们发现并没有0x0021,这是因为访问标志可以多种多样,比如:public static或者public final,所以如果有多个标志位一般采用相加的形式,比如:public static是0x0009,public final是0x0011。所以MyTest1的访问标志是0x0021,拆分一下就是ACC_PUBLIC和ACC_SUPER,代表这个类是公开的,并且可以访问父类的方法。根据我们之前用java -verbose,可以看到类的访问标志确实是ACC_PUBLIC和ACC_SUPER:

    ……
    public class com.leolin.jvm.bytecode.MyTest1
      SourceFile: "MyTest1.java"
      minor version: 0
      major version: 51
      flags: ACC_PUBLIC, ACC_SUPER
    ……
    

      

    访问标志之后的两个字节,存储的是当前类,这里存储的索引值是0x0003,指向常量池第三个元素:com/leolin/jvm/bytecode/MyTest1。再往后推两个字节,存储的是父类,存储的索引值是0x0004,指向常量池第四个元素:java/lang/Object。

    父类之后是接口接口包含两个部分,一个是占据两个字节的接口数量(interfaces_count),还有是类所实现的接口表(interfaces)。由于MyTest1没有实现任何接口,所以父类之后的两个字节为0x0000,因此也就不存在接口表。

    接口之后是字段字段同样包含两部分,两个字节长度的字段数量(fields_count)字段表(fields),MyTest1的字段数量为0x0001,代表只有一个字段,接着我们来看下字段信息field_info的结构: 

    field_info {
    	u2 access_flags;
    	u2 name_index;
    	u2 descriptor_index;
    	u2 attributes_count;
    	attribute_info attributes[attributes_count];
    }
    

      

    第一个字段是访问标志,第二个字段是字段在常量池的索引值,第三个字段是字段的描述符在常量池的索引值,第四个是字段的属性数量,第五个字段是属性表。

    我们根据field_info读取之后的数据,首先读到的是访问标志0x0002,标志名称是ACC_PRIVATE,代表这是一个私有变量,接着字段名索引值为0x0005,常量池第五个元素的值为a,字段的描述符为0x0006,代表这个字段的类型为int,到这里我们能确定这是我们在MyTest1里面定义的那个类型为int的私有变量a。接着是属性数量,这里是0x0000,由于属性数量为0,所以没有属性表。

    字段之后便是方法了,方法同样包含两部分,方法数量(methods_count)方法表(methods),这里方法数量是0x0003,有三个方法,这里我们应该很清楚为什么会有三个,除了两个我们之前生成的对字段a取值getA()和赋值setA(int a)的方法,另外一个就是编译器默认为我们生成的构造方法。下面,我们来看下方法的结构:

    method_info {
    	u2 access_flags; 
    	u2 name_index; 
    	u2 descriptor_index; 
    	u2 attributes_count; 
    	attribute_info attributes[attributes_count]; 
    }
    

    和之前的字段结构一样,所以下面我们来逐个分析每个方法:

    第一个方法的访问标志是0x0001,是个public方法,名字索引值是0x0007,常量池第七个元素是<init>,而描述符索引是0x0008,常量池第八个元素是()V,所以我们基本能确定这是一个构造方法,当我们再往后读两个字节,是属性值数量,这里是0x0001,构造方法有一个属性,所以我们要来看看attribute_info的结构,以便分析后面的数据。

    attribute_info {
    	u2 attribute_name_index; 
    	u4 attribute_length; 
    	u1 info[attribute_length]; 
    }
    

      

    第一个字段是属性名在常量池的索引,构造方法的第一个属性名在常量池的索引是0x0009,对应的是:Code,这里我们来看下Code_attribute的结构:

    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 attribute_count;
    	attribute_info attributes[attribute_count];
    }
    

      

    • attribute_length:表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段。
    • max_stack:表示这个方法运行的任何时刻所能达到的操作数栈的最大深度。
    • max_locals:表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量。
    • code_length:表示该方法所包含的字节码的字节数。
    • code:具体字节码,即是该方法被调用时虚拟机所执行的字节码。
    • exception_table:这里存放的是处理异常的信息,每个exception_table 都是由 start_pc、end_pc、handler_pc、catch_type组成。start_pc和end_pc表示在code数组中的从start_pc到end_pc处(包含start_pc,不包含end_pc)的指令抛出的异常会由这个表项来处理。handler_pc表示处理异常的代码的开始处。catch_type表示会被处理的异常类型,它指向常量池里的一个异常类。当catch_type为0时,表示处理所有的异常。 

    根据上面的Code_attribute结构,我们在0x0009(Code)之后,再读取四个字节0x00000038,十进制是56。往后的两个字节0x0002是栈能达到的最大深度,再往后的两个字节0x0001是局部变量的数目。之后的4个字节是方法所对应具体的字节码的数目,这里是0x0000000a,所以我们要再往后数十个字节:

    2a b7 00 01 2a 04 b5 00 02 b1
    

      

    我们节选之前反编译的内容,将构造方法的字节码贴过来:

      public com.leolin.jvm.bytecode.MyTest1();
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: aload_0
             5: iconst_1
             6: putfield      #2                  // Field a:I
             9: return
          LineNumberTable:
            line 3: 0
            line 4: 4
          LocalVariableTable:
            Start  Length  Slot  Name   Signature
                   0      10     0  this   Lcom/leolin/jvm/bytecode/MyTest1;
    

      

    我们专注Code部分从0~9这六条指令,这里我们介绍下Oracle的JVM官方文档,这里面有介绍像aload_0、invokespecial……这类助记符的作用,及助记符所对应的十六进制编码。我们先来分析上面六条指令:

    • aload_0代表从本地变量表索引为0的对象推到栈顶,根据上面的局部变量表LocalVariableTable,我们知道索引为0的对象为this引用,后面还会介绍本地变量表这个属性。而aload_0的十六进制码为0x2a,正好对应上面字节指令的第一个字节。
    • invokespecial代表针对父类方法、私有方法和实例构造方法的特殊调用。这里需要传入一个参数,常量池第一个元素,即:调用父类的构造方法,整体的指令是0xb7 00 01,正好与上面第二到第四个字节对应。
    • 第三个指令是aload_0,之前介绍过就不介绍,对应上面第五个字节。
    • iconst_1代表将常量值1推到栈顶,对应的指令是0x04,对应上面第六个字节。
    • putfield表示对对象中的字段设值,这里需要传入一个参数,常量池第二个元素,即int类型的成员变量a,将栈顶的1赋值给这个变量,整体指令是0xb5 00 02,对应上面第七个到第九个字节。
    • return代表从方法中返回一个void对象,对应的指令是0xb1。

    至此,上面的六条指令我们都分析完毕,该方法整体的步骤就是先初始化一个对象,然后对对象中的字段设值,最后返回。

    code字段结束后,就是两个字节长度的异常表长度(exception_table_length),这里是0x0000,因为编译器为我们生成的构造方法没有捕捉任何异常,所以异常表长度为0,也就没有exception_table。

    之后,又是属性表长度(attribute_count)属性表(attributes)。我们往后读两个字节,0x0002,这里有两个属性。我们先来分析第一个属性:第一个属性的索引值为0x000a,指向常量池第十个元素:行号表(LineNumberTable,现在我们来看下lineNumberTable_attribute的结构:

    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];
    }
    

      

    之后,我们向后读取4个字节:0x0000000a,代表之后的行号表有10个字节长度的内容:

    00 02 00 00 00 03 00 04 00 04  
    

      

    这里行号line_number_table_length占据两个字节:0x0002,代表有两行的映射,剩下的8个字节的数据,0x0000和0x0003是一对,代表偏移量为0对应到源代码第3行,0x0004和0x0004是一对,代表偏移量为4对应到源代码第四行。因为我们没有手动编写构造方法,所以编译器生成的构造方法和类名同一行,而行号4则为源代码中a=1那段,这段代码的完成是在构造方法里完成的。

    至此,第一个属性就分析完毕了。我们开始第二个属性的分析,第二个属性指向常量池索引值为0x000b,即:本地变量表(LocalVariableTable),现在我们来看下LocalVariableTable_attribute的结构:

    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];
    }
    

      

    start_pc项给出代码数组中指令开始位置的偏移量。length项给出从start_pc开始的、所有局部变量有效的、代码的长度。位于从代码数组开始偏移量start_pc +length位置处的字节只能有下列两种情况:或者为一个指令的首字节,或者为代码数组结束后的首字节。index项给出了在此方法栈帧中本地变量部分的索引,这是当方法执行时该本地变量数据所保存的位置。

    向后读取四个字节0x0000000c,本地变量表我们还要往后读取12个字节。往后读取两个字节是0x0001,只有一个本地变量。开始偏移量(start_pc)是0x0000,长度(length)是0x000a,变量的名字索引是0x000c,在常量池中对应是:this,变量的描述符索引是0x000d,在常量是中对应的是:Lcom/leolin/jvm/bytecode/MyTest1;。最后一个是index,往后读取两个字节就是0x0000,this存储在索引0的位置上。至此,MyTest1的构造方法中的两个属性,我们分析完毕了。

  • 相关阅读:
    linux下nginx的安装
    [转载]QTP中DataTable的使用
    Selenium RC 与 Web Driver 的区别
    Ant 编译时 Unable to find a javac compiler的解决
    Windows如何在cmd命令行中查看、修改、删除与添加、设置环境变量
    selenium页面级自动化测试元素定位问题
    mysqldump导入导出mysql数据库
    阴符经
    将excel文件中的数据导入到mysql
    Selenium2.0 WebDriver入门指南
  • 原文地址:https://www.cnblogs.com/beiluowuzheng/p/12877368.html
Copyright © 2020-2023  润新知