精华内容
下载资源
问答
  • Java字节码

    千次阅读 2019-01-14 13:23:40
    Java最黑科技的玩法就是字节码编程,也就是动态修改或是动态生成 Java 字节码。使用字节码可以玩出很多高级的玩法,最高级的还是在 Java 程序运行时进行字节码修改和代码注入。听起来是不是一些很黑客,也很黑科技的...

    Java最黑科技的玩法就是字节码编程,也就是动态修改或是动态生成 Java 字节码。使用字节码可以玩出很多高级的玩法,最高级的还是在 Java 程序运行时进行字节码修改和代码注入。听起来是不是一些很黑客,也很黑科技的事?是的,这个方式使用 Java 这门静态语言在运行时可以进行各种动态的代码修改,而且可以进行无侵入的编程。
    比如,我们不需要在代码中埋点做统计或监控,可以使用这种技术把我们的监控代码直接以字节码的方式注入到别人的代码中,从而实现对实际程序运行情况进行统计和监控。但是要做到这个事,还需要学习一个叫 Java Agent 的技术(可以参考我的这篇文章:Java Agent)。

    1、Class类文件的结构

    根据 Java 虚拟机规范的规定,Class 文件格式采用一种类似 C 语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型:无符号数和表
    无符号数属于基本的数据类型,以 u1、u2、u4、u8 来分别代表 1 个字节、2 个字节、4个字节和 8 个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照 UTF-8 编码构成字符串值。
    是由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性地以“_info”结尾。表用于描述有层次关系的复合结构的数据,整个 Class 文件本质上就是一张表,它由下表所示的数据项构成。

    类型名称数量描述
    u4magic1魔数,占用4个字节,offset 0-3
    u2minor_version1次版本号,offset 4-5
    u2major_version1主版本号 ,offset 7-8
    u2constant_pool_count1常量池数量,offset 8-9
    cp_infoconstant_pollconstant_pool_count-1常量池,Class 文件之中的资源仓库。数量不固定
    u2access_flags1访问标志,用于识别一些类或者接口层次的访问信息
    u2this_class1类索引,用于确定这个类的全限定名
    u2super_class1父类索引,用于确定这个类的父类的全限定名
    u2interfaces_count1接口计数器,表示索引表的容量
    u2interfacesinterfaces_count接口索引集合,用来描述这个类实现了哪些接口
    u2fields_count1字段容量计数器,记录这个表含有多少个字段
    field_infofieldsfields_count字段表集合,用于描述接口或者类中声明的变量
    u2methods_count1方法表计数器,记录这个表含有多少个方法
    method_infomethodsmethods_count方法表集合,存放 Class 的方法集合
    u2attributes_count1属性表计数器,表示字段表或方法表有多少个属性
    attributeattributesattributes_count属性表集合,在 Class 文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息

    为方便讲解,在这里准备了一段最简单的代码,也希望大家能跟着实际操作一遍:

    package org.clazz;
    
    public class TestClazz {
        private int m;
    
        public int inc() {
            return m + 1;
        }
    }
    

    使用 javac 将这个文件转换成 Class,然后用十六进制编辑器 WinHex 打开这个 Class 文件:
    在这里插入图片描述
    有了以上的知识准备,现在我们一起分析上面的 Class 分别代表什么意思。揭开这层神秘的面纱!

    1.1、魔数

    每个 Class 文件的头 4 个字节称为魔数(Magic Number),它的唯一作用是确定这个文件是否为一个能被虚拟机接受的 Class 文件。
    很多文件存储标准中都使用魔数来进行身份识别,譬如图片格式,如 gif 或者 jpeg 等在文件头中都存有魔数。
    文件格式的制定者可以自由地选择魔数值,只要这个魔数值还没有被广泛采用过同时又不会引起混淆即可。
    我们看到 TestClazz.class 的魔数也就是头 4 个字节为 CA FE BA BE,用十六进制表示是 0xCAFEBABE(咖啡宝贝?这个名称也太浪漫了吧)。这也意味着每个 Class 文件的魔数值都必须为 0xCAFEBABE。
    在这里插入图片描述

    1.2、Class 文件的版本

    紧接着魔数的 4 个字节存储的是 Class 文件的版本号:5-6 个字节是次版本号(Minor Version),7-8 个字节是主版本号(Major Version)。Java 的版本号是从 45 开始的,JDK 1.1 之后的每个JDK 大版本发布主版本号加 1,高版本的 JDK 能向下兼容以前版本的 Class 文件,但不能运行以后版本的 Class 文件,即使文件格式并未发生任何变化,虚拟机也必须拒绝执行超过起把那本号的 Class 文件。
    Class 文件版本号:

    JDK版本号10进制版本号16进制版本号
    1.145.000 00 00 2D
    1.246.000 00 00 2E
    1.347.000 00 00 2F
    1.448.000 00 00 30
    1.549.000 00 00 31
    1.650.000 00 00 32
    1.751.000 00 00 33
    1.852.000 00 00 34

    再看看文件对应的值:
    在这里插入图片描述
    我们看到代表主版本号的 7-8 个字节的值为 0x0034,也即十进制的 52,该版本号说明这个文件是可以被 JDK 1.8 或以上版本虚拟机执行的 Class 文件。

    1.3、常量池

    紧接着主次版本号之后的是常量池入口,常量池可以理解为 Class 文件之中的资源仓库,它是占用 Class 文件空间最大的数据项目之一。
    由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项 u2 类型的数据,代表常量池容量计数值(constant_pool_count)。
    在这里插入图片描述
    如上图所示,常量池容量为十六进制数 0x0016,即十进制的 19,结合上面的 Class 表,我们能知道常量池中有 19 - 1 = 18 项常量。
    常量池容量计数值之后就是常量池,常量池中主要存放两大类常量:字面量(Literal)和符号引用(Symbolic References)。
    字面量比较接近于 Java 语言层面的常量概念,如文本字符串、声明为 final 的常量值等。
    符号引用则属于编译原理方面的概念,包括了下面三类常量:

    • 类和接口的全限定名(Fully Qualified Name)
    • 字段的名称和描述符(Descriptor)
    • 方法的名称和描述符

    常量池中每一项常量都是一个表,总共 14 种表:

    类型标志描述
    CONSTANT_Utf8_info1UTF-8 编码的字符串
    CONSTANT_Integer_info3整型字面量
    CONSTANT_Float_info4浮点型字面量
    CONSTANT_Long5长整型字面量
    CONSTANT_Double_info6双精度浮点型字面量
    CONSTANT_Class_info7类或接口的符号引用
    CONSTANT_String_info8字符串类型字面量
    CONSTANT_Fieldref_info9字段的符号引用
    CONSTANT_Methidref_info10类中方法的符号引用
    CONSTANT_InterfaceMethodref_info11接口中方法的符号引用
    CONSTANT_NameAndType_info12字段或方法的部分符号引用
    CONSTANT_MethodHandle_info15表示方法句柄
    CONSTANT_MethodType_info16标识方法类型
    CONSTANT_InvokeDynamic_info18标识一个动态方法调用点

    之所以说常量池是最烦琐的数据,是因为这 14 中常量类型各自均有自己的结构。
    我们再来看图中常量池的第一项常量,它的标志位(偏移地hi:0x0000000A)是 0x0A,转换为十进制的值为 10,查常量表中对应的标志为 10 的常量属于 CONSTANT_Methodref_info 类型。
    在这里插入图片描述
    我们看一下 CONSTANT_Methodref_info 类型常量的结构:

    名称类型描述
    tagu1值为10
    indexu2指向声明方法的类描述符 CONSTANT_Class_info 的索引项
    indexu2指向名称及类型描述符 CONSTANT_NameAndType 的索引项

    上图中的第一个 index 十六进制为 0x0004,即十进制的 4,表示指向常量池中第 4 个常量。
    第二个 index 十六进制为 0x000F,即十进制的 15,表示指向常量吃中的第 15 个常量。
    (先不管第4、15 常量表示什么)
    上面分析的是第一个常量值,接着分析第二个常量值,它的标志位(地址:0x0000000F)是 0x09,即十进制的 9,表示这个常量属于 CONSTANT_Fieldref_info 类型,此常量代表字段的符号引用。
    在这里插入图片描述
    CONSTANT_Fieldref_info 型常量的结构:

    名称类型描述
    tagu1值为9
    indexu2指向声明字段的类或者接口描述符 CONSTANT_Class_info 的索引项
    indexu2指向字段描述符 CONSTANT_NameAndType 的索引项

    以上分析了 TestClazz.class 常量池中 18 个常量中的前两个,其余的 16 个依次类推:
    在这里插入图片描述
    需要注意的是第 18 个常量,tag 标志为 0x01 表示 CONSTANT_Utf8_info :

    名称类型描述
    tagu1数量 1
    lengthu2长度,表示占用几个字节
    bytesu1占用 length 个字节

    注意 bytes 字段的长度,是根据 length 计算的,length 为 0x0010 转换十进制为 16,所以后面的 bytes 占用 16 个字节。
    最后将 14 中常量项的结构定义总结为下表,供大家参考:
    在这里插入图片描述

    1.4、访问标志

    在常量池结束之后,紧接着的两个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个 Class 是类还是接口;是否定义为 public 类型;是否定义为 abstract 类型;如果是类的话,是否被声明为 final 等。具体标志位以及标志的含义见下表:

    标志名称标志值含义
    ACC_PUBLIC0x0001是否为 public 类型
    ACC_FINAL0x0010是否被声明为 final,只有类可设置
    ACC_SUPER0x0020是否允许使用 invokespecial 字节码指令的新语意,invokespecial指令的语意在 JDK 1.0.2 发生过改变,为了区别这条指令使用哪种语意,JDK 1.0.2 之后编译出来的类的这个标志都必须为真
    ACC_INTERFACE0x0200标识这是一个接口
    ACC_ABSTRACT0x0400是否为 abstract 类型,对于接口或者抽象类来说,此标志值为真,其他类值为假
    ACC_SYNTHETIC0x1000标识这个类并非由用户代码产生的
    ACC_ANNOTATION0x2000标识这是一个注解
    ACC_ENUM0x4000标识这是一个枚举

    在这里插入图片描述
    TestClazz.class 是一个普通的 java 类,不是接口、枚举,因此它的ACC_PUBLIC、ACC_SUPER标志为真,其他标志为假,因此它的 access_flags 的值为:0x0001|0x0020 = 0x0021。

    1.5、类索引、父类索引与接口索引集合

    类索引(this_class)和父类索引(super_class)都是一个 u2 类型的数据,而接口索引结合(interfaces)是一组 u2 类型的数据的集合,Class 文件中由这三项数据来确定这个类的继承关系。
    类索引和父类索引都是指向一个类型为 CONSTANT_Class_info 的类描述符常量。
    在这里插入图片描述
    图中看到,TestClazz中的类索引指向的是第 3 个常量,父类索引指向的是第 4 个常量。
    对于接口索引集合,入口的第一项——u2 类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为 0 ,后面接口的索引表不再占用任何字节。

    1.6、字段表集合

    字段表用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例变量,但不包括在方法内部声明的局部变量。
    字段表结构:

    类型名称数量
    u2access_flags1
    u2name_index1
    u2descriptor_index1
    u2attributes_count1
    attribute_infoattributesattributes_count

    字段修饰符放在 access_flags 项目中,它与类中的 access_flags 项目是非常类似的,都是一个 u2 的数据类型,其中可以设置的标志位和含义见表:

    标志名称标志值含义
    ACC_PUBLIC0x0001字段是否为 public
    ACC_PRIVATE0x0002字段是否为 private
    ACC_PROTECTED0x0004字段是否为 proctected
    ACC_STATIC0x0008字段是否为 static
    ACC_FINAL0x0010字段是否为 final
    ACC_VOLATILE0x0040字段是否为 volatile
    ACC_TRANSIENT0x0080字段是否为 transient
    ACC_SYNTHETIC0x1000字段是否由编译器自动产生的
    ACC_ENUM0x4000字段是否为 enum

    跟随 access_flags 标志的是两项索引值:name_index 和 descriptor_index。它们都是对常量池的引用,分别代表着字段的简单名称以及字段和方法的描述符。
    在这里插入图片描述

    1.7、方法表集合

    方法表的内容和字段表几乎采用了完全一致的方式,方法表结构:

    类型名称数量
    u2access_flags1
    u2name_index1
    u2descriptor_index1
    u2attributes_count1
    attribute_infoattributesattributes_count

    access_flags 方法访问标志:

    标志名称标志值含义
    ACC_PUBLIC0x0001方法是否为 public
    ACC_PRIVATE0x0002方法是否为 private
    ACC_PROTECTED0x0004方法是否为 proctected
    ACC_STATIC0x0008方法是否为 static
    ACC_FINAL0x0010方法是否为 final
    ACC_SYNCHRONIZED0x0020方法是否为 synchronized
    ACC_BRIDGE0x0040方法是否是由编译器产生的桥接方法
    ACC_VARARGS0x0080方法是否接受不定参数
    ACC_NATIVE0x0100方法是否为 native
    ACC_ABSTRACT0x0400方法是否为 abstract
    ACC_STRICTFP0x0800方法是否为 strictfp
    ACC_SYNTHETIC0x1000方法是否由编译器自动产生的

    TestClazz对应的位置:
    在这里插入图片描述
    注意,方法表集合只存放了方法名称,索引等,方法里的代码存放在方法属性表集合中一个名为“Code”的属性里面,这就是下面需要将到的属性表集合。

    1.8、属性表集合

    属性表(attribute_info)在 Class 文件、字段表、方法表都可以携带自己的属性表集合,以用于描述某些场景专有的信息。
    与 Class 文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松些,不再要求各个属性表具有严格顺序,并且只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java 虚拟机运行时会忽略掉它不认识的属性。
    虚拟机规范预定义的属性:

    属性名称使用位置含义
    Code方法表Java 代码编译成的字节码指令
    ConstantValue字段表final 关键字定义的常量值
    Deprecated类、方法表、字段表被声明为 deprecated 的方法和字段
    Exceptions方法表方法抛出的异常
    EnclosingMethod类文件仅当一个类为局部类或者匿名类时才能拥有这个属性,这个属性用于标识这个类所在的外围方法
    InnerClasses类文件内部类列表
    LineNumberTableCode 属性Java 源码的行号与字节码指令的对应关系
    LocalVariableTableCode 属性方法的局部变量描述
    StackMapTableCode 属性JDK 1.6 中新增的属性,供新的类型检查验证器(Tyoe Checker)检查和处理目标方法的局部变量和操作数栈所需要的类型是否匹配
    Signature类、方法表、字段表这个属性用于支持泛型情况下的方法签名,在 Java 语言中,任何类、接口、初始化方法或成员的泛型签名如果包含了类型变量(Type Variables)或参数化类型(Parameterized Types),则 Signature 属性会为它记录泛型签名信息。由于 Java 的泛型采用擦除法实现,在为了避免类型信息被擦除后导致签名混乱,需要这个属性记录泛型中的相关信息
    SourceFile类文件记录源文件名称
    SourceDebugExtension类文件JDK 1.6 中新增的属性,SourceDebugExtension 属性用于存储额外的调试信息。譬如在进行 JSP 文件调试时,无法通过 Java 堆栈来定位到 JSP 文件的行号,JSR-45 规范为这些非 Java 语言编写,却需要编异常字节码并运行在 Java 虚拟机中的程序提供了一个进行调试的标准机制,使用 SourceDebugExtension 属性就可以用于存储这个标准所新加入的调试信息
    Synthetic类、方法表、字段表标识方法或字段为编译器自动生成的
    LocalVariableTypeTable它使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
    RuntimeVisibleAnnotations类、方法表、字段表为动态注解提供支持。RuntimeVisibleAnnotations 属性用于指明哪些注解是运行时(实际上运行时就是进行反射调用)可见的
    RuntimeInvisibleAnnotations类、方法表、字段表与 RuntimeVisibaleAnnotations 属性作用刚好相反,用于指明哪些注解是运行时不可见的
    RuntimeVisibleParameterAnnotations方法表作用与 RuntimeVisibleAnnotations 属性类似,只不过作用对象为方法参数
    RuntimeInvisibleParameterAnnotations方法表作用与 RuntimeInvisibleAnnotations 属性类似,只不过作用对象为方法参数
    AnnotationDefault方法表用于记录注解类元素的默认值
    BootstrapMethods类文件JDK 1.7 中新增的属性,用于保存 invokedynamic 指令引用的引导方法限定符

    对于每个属性,它的名称需要从常量池中引用一个 CONSTANT_Utf8_info 类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个 u4 的长度属性去说明属性值所占用的位数即可,一个符合规则的属性表应该满足下表所定义的结构。
    属性表结构:

    类型名称数量
    u2attribute_name_index1
    u4attribute_length1
    u1infoattribute_length

    1.8.1、Code 属性

    Java 程序方法体中的代码经过 Javac 编译器处理后,最终变为字节码指令存储在 Code 属性内。
    Code 属性表结构:

    类型名称数量描述
    u2attibute_name_index1属性名称,指向 CONSTANT_Utf8_info 型常量的索引,常量值固定为“Code”
    u4attribute_length1属性值长度,由于属性名称索引与属性长度一共为 6 字节,所以属性值的长度固定为整个属性表长度减去 6 个字节
    u2max_stack1操作数栈深度最大值,在方法执行的任意时刻,操作数栈都不会超过这个深度。虚拟机运行的时候需要根据这个值来分配栈帧中的操作栈深度
    u2max_locals1局部变量表所需的存储空间,max_locals 的单位是 Slot,Slot 是虚拟机为局部变量分配内存所使用的最小单位
    u4code_length1字节码长度
    u1codecode_length字节码指令字节流,用于存储字节码指令的一系列字节流。文章的末尾会给出“虚拟机字节码指令表”。
    u2exception_table_length1
    exception_infoexception_tableexception_table_length
    u2attributes_count1
    attribute_infoattributesattributes_count

    Code 属性是 Class 文件中最重要的一个属性,如果把一个 Java 程序中的信息分为代码(java代码)和元数据(类、字段、方法定义及其他信息)两部分,那么在整个 Class 文件中,Code 属性用于描述代码,所有其他数据项目都用于描述元数据。
    继续以 TestClazz.class 文件为例
    在这里插入图片描述
    它的操作数栈的最大深度和本地变量表的容量都为 0x0001,字节码区域所占空间的长度为 0x0005。
    虚拟机读取到字节码长度后,按照顺序依次读入紧随的 5 个字节,并根据字节码指令表翻译出所对应的字节码指令。
    翻译 “2A B7 00 0A B1” 的过程:
    1.读入 2A,查表得 0x2A 对应得指令为 aload_0,这个指令得含义是将第 0 个 Slot 中为 reference 类型得本地变量推送到操作数栈顶。
    2.读入 B7,查表得 0xB7 对应得指令为 invokespecial,这条指令的作用是以栈顶的 reference 类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private 方法或者它的父类的方法。
    3.读入 00 0A,这是 invokespecial 的参数,查常量池得 0x000A 对应的常量为实例构造器“”方法的符号引用。
    4.读入 B1,查表得 0xB1 对应得指令为 return,含义是返回此方法,这条指令执行后,当前方法结束。
    属性表集合除了 Code 属性,还有 Exceptions 属性、LineNumberTable 属性等等,这里就不一一介绍了。有兴趣得童鞋可以自行了解。

    2、javap 工具分析 Class

    在 JDK 的 bin 目录中,Oracle 公司已经为我们准备好一个专门用于分析 Class 文件字节码的工具:javap
    使用命令:

    javap -verbose TestClazz.class
    

    代码清单:

     Last modified 2019-1-14; size 285 bytes
      MD5 checksum c434da45f0fff84f21348a725448f2f5
      Compiled from "TestClazz.java"
    public class org.clazz.TestClazz
      minor version: 0
      major version: 52
      flags: ACC_PUBLIC, ACC_SUPER
    Constant pool:
       #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
       #2 = Fieldref           #3.#16         // org/clazz/TestClazz.m:I
       #3 = Class              #17            // org/clazz/TestClazz
       #4 = Class              #18            // java/lang/Object
       #5 = Utf8               m
       #6 = Utf8               I
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               inc
      #12 = Utf8               ()I
      #13 = Utf8               SourceFile
      #14 = Utf8               TestClazz.java
      #15 = NameAndType        #7:#8          // "<init>":()V
      #16 = NameAndType        #5:#6          // m:I
      #17 = Utf8               org/clazz/TestClazz
      #18 = Utf8               java/lang/Object
    {
      public org.clazz.TestClazz();
        descriptor: ()V
        flags: ACC_PUBLIC
        Code:
          stack=1, locals=1, args_size=1
             0: aload_0
             1: invokespecial #1                  // Method java/lang/Object."<init>":()V
             4: return
          LineNumberTable:
            line 3: 0
    
      public int inc();
        descriptor: ()I
        flags: ACC_PUBLIC
        Code:
          stack=2, locals=1, args_size=1
             0: aload_0
             1: getfield      #2                  // Field m:I
             4: iconst_1
             5: iadd
             6: ireturn
          LineNumberTable:
            line 7: 0
    }
    SourceFile: "TestClazz.java"
    

    结束

    到此,相信大家能对字节码有一个较深的认识,Java 语言中的各种变量、关键字和运算符号的语义最终都是由多条字节码命令组合而成的,因此字节码命令所能提供的语义描述能力肯定会比 Java 语言本身更强大。

    附录

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    展开全文
  • java字节码java字节码

    2010-04-02 09:51:54
    关于java字节码的开源介绍,可以参考来吧 关于java字节码的开源介绍,可以参考来吧
  • Java字节码和Dalvik字节码

    千次阅读 2017-06-17 00:03:37
    Java字节码和Dalvik字节码的区别,生成JAVA字节码和Dalvik字节码
    public class Hello {
    public int foo (int a,int b){
    return (a+b)*(a-b);
    }

    public static void main(String args[]){
    Hello hello = new Hello();
    System.out.print(hello.foo(5,3));
    }


    }


    将以上内容保存为Hello.java 

    打开cmd 执行命令 javac Hello.java (1.6版本的)

     Javac -sourse 1.6 -target 1.6 Hello.java(要编译的文件) 高版本比如1.7.8的强制为1.6

    接下来用Javap -c -classpath . Hello 

    命令执行后得到如下代码

      public int foo(int, int);
        Code:
           0: iload_1
           1: iload_2
           2: iadd
           3: iload_1
           4: iload_2
           5: isub
           6: imul
           7: ireturn

    执行dx --dex --output=Hello.dex Hello.class 生成dex文件 (dx位于Android SDK的platform-tools目录中,生成的.dex也在这个文件夹里)

    使用dx工具编译Hello.class时,如提示无法找到Hello.classHello.class文件与dx放同一目录后重新编译

    使用dexdump.exe 查看foo()函数的Dalvik字节码

    dexdump.exe -d Hello.dex (记得把Hello.dex移到你当前文件夹,或者你去配置路径也行)



    Hello.foo:(II)I
    0000: add-int v0, v3, v4
    0002: sub-int v1, v3, v4
    0004: mul-int/2addr v0, v1
    0005: return v0
    这个是编译时没有定义1.6版本的报错,强制编译1.6就行了
    C:\Users\Administrator>dx --dex --output=Hello.dex Hello.calss
    
    UNEXPECTED TOP-LEVEL EXCEPTION:
    java.lang.RuntimeException: Hello.calss: file not found
            at com.android.dx.util.FileUtils.readFile(FileUtils.java:55)
            at com.android.dx.cf.direct.ClassPathOpener.processOne(ClassPathOpener.j
    ava:134)
            at com.android.dx.cf.direct.ClassPathOpener.process(ClassPathOpener.java
    :109)
            at com.android.dx.command.dexer.Main.processOne(Main.java:422)
            at com.android.dx.command.dexer.Main.processAllFiles(Main.java:333)
            at com.android.dx.command.dexer.Main.run(Main.java:209)
            at com.android.dx.command.dexer.Main.main(Main.java:174)
            at com.android.dx.command.Main.main(Main.java:91)
    1 error; aborting

    上面的JAVA字节码,foo()函数一共占用了8个字节,代码中每条指令占用一行

    展开全文
  • 主要给大家介绍了关于java字节码框架ASM如何操作字节码的相关资料,文中通过示例代码介绍的很详细,有需要的朋友可以参考借鉴,下面来一起看看吧。
  • java字节码加密

    2017-03-13 17:32:16
    java字节码加密
  • 从一个class文件深入理解Java字节码结构

    万次阅读 多人点赞 2018-05-15 10:01:56
    我们都知道,Java程序最终是转换成class文件执行在虚拟机上的,那么class文件是个怎样的结构,虚拟机又是如何处理去执行class文件里面的内容呢,这篇文章带你深入理解Java字节码中的结构。 1.Demo源码 首先,...

    前言

    我们都知道,Java程序最终是转换成class文件执行在虚拟机上的,那么class文件是个怎样的结构,虚拟机又是如何处理去执行class文件里面的内容呢,这篇文章带你深入理解Java字节码中的结构。

    1.Demo源码

    首先,编写一个简单的Java源码:

    package com.april.test;
    
    public class Demo {
        private int num = 1;
    
        public int add() {
            num = num + 2;
            return num;
        }
    }
    

    这段代码很简单,只有一个成员变量num和一个方法add()

    2.字节码

    要运行一段Java源码,必须先将源码转换为class文件,class文件就是编译器编译之后供虚拟机解释执行的二进制字节码文件,可以通过IDE工具或者命令行去将源码编译成class文件。这里我们使用命令行去操作,运行下面命令:

    javac Demo.java
    

    就会生成一个Demo.class文件。

    我们打开这个Demo.class文件看下。这里用到的是Notepad++,需要安装一个HEX-Editor插件。
    1.字节码-完整版.png

    3.class文件反编译java文件

    在分析class文件之前,我们先来看下将这个Demo.class反编译回Demo.java的结果,如下图所示:
    源码与class转java对比.png
    可以看到,回编译的源码比编写的代码多了一个空的构造函数this关键字,为什么呢?先放下这个疑问,看完这篇分析,相信你就知道答案了。

    4.字节码结构

    从上面的字节码文件中我们可以看到,里面就是一堆的16进制字节。那么该如何解读呢?别急,我们先来看一张表:

    类型名称说明长度
    u4magic魔数,识别Class文件格式4个字节
    u2minor_version副版本号2个字节
    u2major_version主版本号2个字节
    u2constant_pool_count常量池计算器2个字节
    cp_infoconstant_pool常量池n个字节
    u2access_flags访问标志2个字节
    u2this_class类索引2个字节
    u2super_class父类索引2个字节
    u2interfaces_count接口计数器2个字节
    u2interfaces接口索引集合2个字节
    u2fields_count字段个数2个字节
    field_infofields字段集合n个字节
    u2methods_count方法计数器2个字节
    method_infomethods方法集合n个字节
    u2attributes_count附加属性计数器2个字节
    attribute_infoattributes附加属性集合n个字节

    这是一张Java字节码总的结构表,我们按照上面的顺序逐一进行解读就可以了。

    首先,我们来说明一下:class文件只有两种数据类型:无符号数。如下表所示:

    数据类型定义说明
    无符号数无符号数可以用来描述数字、索引引用、数量值或按照utf-8编码构成的字符串值。其中无符号数属于基本的数据类型。
    以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节
    表是由多个无符号数或其他表构成的复合数据结构。所有的表都以“_info”结尾。
    由于表没有固定长度,所以通常会在其前面加上个数说明。

    实际上整个class文件就是一张表,其结构就是上面的表一了。

    那么我们现在再来看表一中的类型那一列,也就很简单了:

    类型说明长度
    u11个字节1
    u22个字节2
    u44个字节4
    u88个字节8
    cp_info常量表n
    field_info字段表n
    method_info方法表n
    attribute_info属性表n

    上面各种具体的表的数据结构后面会详细说明,这里暂且不表。

    好了,现在我们开始对那一堆的16进制进行解读。
    b.jpg

    4.1 魔数

    从上面的总的结构图中可以看到,开头的4个字节表示的是魔数,其值为:
    2.字节码-魔数.png
    嗯,其值为0XCAFE BABE。CAFE BABE??What the fxxk?
    好了,那么什么是魔数呢?魔数就是用来区分文件类型的一种标志,一般都是用文件的前几个字节来表示。比如0XCAFE BABE表示的是class文件,那么为什么不是用文件名后缀来进行判断呢?因为文件名后缀容易被修改啊,所以为了保证文件的安全性,将文件类型写在文件内部可以保证不被篡改。
    再来说说为什么class文件用的是CAFE BABE呢,看到这个大概你就懂了。
    java.jpg

    4.2 版本号

    紧跟着魔数后面的4位就是版本号了,同样也是4个字节,其中前2个字节表示副版本号,后2个字节
    表示主版本号。再来看看我们Demo字节码中的值:
    3.字节码-版本号.png
    前面两个字节是0x0000,也就是其值为0;
    后面两个字节是0x0034,也就是其值为52.
    所以上面的代码就是52.0版本来编译的,也就是jdk1.8.0

    4.3 常量池

    4.3.1 常量池容量计数器

    接下来就是常量池了。由于常量池的数量不固定,时长时短,所以需要放置两个字节来表示常量池容量计数值。Demo的值为:
    4.字节码-常量池容量计数值.png
    其值为0x0013,掐指一算,也就是19。
    需要注意的是,这实际上只有18项常量。为什么呢?

    通常我们写代码时都是从0开始的,但是这里的常量池却是从1开始,因为它把第0项常量空出来了。这是为了在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义,这种情况可用索引值0来表示。

    Class文件中只有常量池的容量计数是从1开始的,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都与一般习惯相同,是从0开始的。

    4.3.2 字面量和符号引用

    在对这些常量解读前,我们需要搞清楚几个概念。
    常量池主要存放两大类常量:字面量符号引用。如下表:

    常量具体的常量
    字面量文本字符串
    声明为final的常量值
    符号引用类和接口的全限定名
    字段的名称和描述符
    方法的名称和描述符

    4.3.2.1 全限定名

    com/april/test/Demo这个就是类的全限定名,仅仅是把包名的".“替换成”/",为了使连续的多个全限定名之间不产生混淆,在使用时最后一般会加入一个“;”表示全限定名结束。

    4.3.2.2 简单名称

    简单名称是指没有类型和参数修饰的方法或者字段名称,上面例子中的类的add()方法和num字段的简单名称分别是addnum

    4.3.2.3 描述符

    描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及代表无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示,详见下表:

    标志符含义
    B基本数据类型byte
    C基本数据类型char
    D基本数据类型double
    F基本数据类型float
    I基本数据类型int
    J基本数据类型long
    S基本数据类型short
    Z基本数据类型boolean
    V基本数据类型void
    L对象类型,如Ljava/lang/Object

    对于数组类型,每一维度将使用一个前置的[字符来描述,如一个定义为java.lang.String[][]类型的二维数组,将被记录为:[[Ljava/lang/String;,,一个整型数组int[]被记录为[I

    用描述符来描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组小括号“( )”之内。如方法java.lang.String toString()的描述符为( ) LJava/lang/String;,方法int abc(int[] x, int y)的描述符为([II) I

    4.3.3 常量类型和结构

    常量池中的每一项都是一个表,其项目类型共有14种,如下表格所示:

    类型标志描述
    CONSTANT_utf8_info1UTF-8编码的字符串
    CONSTANT_Integer_info3整形字面量
    CONSTANT_Float_info4浮点型字面量
    CONSTANT_Long_info5长整型字面量
    CONSTANT_Double_info6双精度浮点型字面量
    CONSTANT_Class_info7类或接口的符号引用
    CONSTANT_String_info8字符串类型字面量
    CONSTANT_Fieldref_info9字段的符号引用
    CONSTANT_Methodref_info10类中方法的符号引用
    CONSTANT_InterfaceMethodref_info11接口中方法的符号引用
    CONSTANT_NameAndType_info12字段或方法的符号引用
    CONSTANT_MethodHandle_info15表示方法句柄
    CONSTANT_MothodType_info16标志方法类型
    CONSTANT_InvokeDynamic_info18表示一个动态方法调用点

    这14种类型的结构各不相同,如下表格所示:
    常量池中常量项的结构总表.png

    注:此表格的类型的单位不对,不是bit,应该是byte(字节)。后面的同理。

    从上面的表格可以看到,虽然每一项的结构都各不相同,但是他们有个共同点,就是每一项的第一个字节都是一个标志位,标识这一项是哪种类型的常量。

    4.3.4 常量解读

    好了,我们进入这18项常量的解读,首先是第一个常量,看下它的标志位是啥:
    5.字节码-第一个常量的标志位.png
    其值为0x0a,即10,查上面的表格可知,其对应的项目类型为CONSTANT_Methodref_info,即类中方法的符号引用。其结构为:
    CONSTANT_Methodref_info的结构.png
    即后面4个字节都是它的内容,分别为两个索引项:
    6.字节码-第一个常量的项目.png
    其中前两位的值为0x0004,即4,指向常量池第4项的索引;
    后两位的值为0x000f,即15,指向常量池第15项的索引。
    至此,第一个常量就解读完毕了。
    我们再来看下第二个常量:
    7.字节码-第二个常量.png
    其标志位的值为0x09,即9,查上面的表格可知,其对应的项目类型为CONSTANT_Fieldref_info,即字段的符号引用。其结构为:
    CONSTANT_Fieldref_info的结构.png
    同样也是4个字节,前后都是两个索引。分别指向第3项的索引和第16项的索引。

    后面还有16项常量就不一一去解读了,因为整个常量池还是挺长的:
    8.字节码-所有常量.png

    你看,这么长的一大段16进制,看的我都快瞎了:
    你说什么,我没带眼镜听不清.jpg

    实际上,我们只要敲一行简单的命令:

    javap -verbose Demo.class
    

    其中部分的输出结果为:

    Constant pool:
       #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
       #2 = Fieldref           #3.#16         // com/april/test/Demo.num:I
       #3 = Class              #17            // com/april/test/Demo
       #4 = Class              #18            // java/lang/Object
       #5 = Utf8               num
       #6 = Utf8               I
       #7 = Utf8               <init>
       #8 = Utf8               ()V
       #9 = Utf8               Code
      #10 = Utf8               LineNumberTable
      #11 = Utf8               add
      #12 = Utf8               ()I
      #13 = Utf8               SourceFile
      #14 = Utf8               Demo.java
      #15 = NameAndType        #7:#8          // "<init>":()V
      #16 = NameAndType        #5:#6          // num:I
      #17 = Utf8               com/april/test/Demo
      #18 = Utf8               java/lang/Object
    

    你看,一家大小,齐齐整整,全都出来了。
    但是,通过我们手动去分析才知道这个结果是怎么出来的,要知其然知其所以然嘛~

    4.4 访问标志

    常量池后面就是访问标志,用两个字节来表示,其标识了类或者接口的访问信息,比如:该Class文件是类还是接口,是否被定义成public,是否是abstract,如果是类,是否被声明成final等等。各种访问标志如下所示:

    标志名称标志值含义
    ACC_PUBLIC0x0001是否为Public类型
    ACC_FINAL0x0010是否被声明为final,只有类可以设置
    ACC_SUPER0x0020是否允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真
    ACC_INTERFACE0x0200标志这是一个接口
    ACC_ABSTRACT0x0400是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
    ACC_SYNTHETIC0x1000标志这个类并非由用户代码产生
    ACC_ANNOTATION0x2000标志这是一个注解
    ACC_ENUMx4000标志这是一个枚举

    再来看下我们Demo字节码中的值:
    9.字节码-访问标志.png
    其值为:0x0021,是0x00200x0001的并集,即这是一个Public的类,再回头看看我们的源码。
    确认过眼神,我遇上对的了。

    4.5 类索引、父类索引、接口索引

    访问标志后的两个字节就是类索引;
    类索引后的两个字节就是父类索引;
    父类索引后的两个字节则是接口索引计数器。
    通过这三项,就可以确定了这个类的继承关系了。

    4.5.1 类索引

    我们直接来看下Demo字节码中的值:
    10.字节码-类索引.png
    类索引的值为0x0003,即为指向常量池中第三项的索引。你看,这里用到了常量池中的值了。
    我们回头翻翻常量池中的第三项:

       #3 = Class              #17            // com/april/test/Demo
    

    通过类索引我们可以确定到类的全限定名。

    4.5.2 父类索引

    从上图看到,父类索引的值为0x0004,即常量池中的第四项:

       #4 = Class              #18            // java/lang/Object
    

    这样我们就可以确定到父类的全限定名。
    可以看到,如果我们没有继承任何类,其默认继承的是java/lang/Object类。
    同时,由于Java不支持多继承,所以其父类只有一个。

    4.5.3 接口计数器

    从上图看到,接口索引个数的值为0x0000,即没有任何接口索引,我们demo的源码也确实没有去实现任何接口。

    4.5.4 接口索引集合

    由于我们demo的源码没有去实现任何接口,所以接口索引集合就为空了,不占地方,嘻嘻。
    可以看到,由于Java支持多接口,因此这里设计成了接口计数器和接口索引集合来实现。

    4.6 字段表

    接口计数器或接口索引集合后面就是字段表了。
    字段表用来描述类或者接口中声明的变量。这里的字段包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。

    4.6.1 字段表计数器

    同样,其前面两个字节用来表示字段表的容量,看下demo字节码中的值:
    11.字节码-字段表容量计数器.png
    其值为0x0001,表示只有一个字段。

    4.6.2 字段表访问标志

    我们知道,一个字段可以被各种关键字去修饰,比如:作用域修饰符(public、private、protected)、static修饰符、final修饰符、volatile修饰符等等。因此,其可像类的访问标志那样,使用一些标志来标记字段。字段的访问标志有如下这些:

    标志名称标志值含义
    ACC_PUBLIC0x0001字段是否为public
    ACC_PRIVATE0x0002字段是否为private
    ACC_PROTECTED0x0004字段是否为protected
    ACC_STATIC0x0008字段是否为static
    ACC_FINAL0x0010字段是否为final
    ACC_VOLATILE0x0040字段是否为volatile
    ACC_TRANSTENT0x0080字段是否为transient
    ACC_SYNCHETIC0x1000字段是否为由编译器自动产生
    ACC_ENUM0x4000字段是否为enum

    4.6.3 字段表结构

    字段表作为一个表,同样有他自己的结构:

    类型名称含义数量
    u2access_flags访问标志1
    u2name_index字段名索引1
    u2descriptor_index描述符索引1
    u2attributes_count属性计数器1
    attribute_infoattributes属性集合attributes_count

    4.6.4 字段表解读

    我们先来回顾一下我们demo源码中的字段:

        private int num = 1;
    

    由于只有一个字段,还是比较简单的,直接看demo字节码中的值:
    12.字节码-字段表.png
    访问标志的值为0x0002,查询上面字段访问标志的表格,可得字段为private
    字段名索引的值为0x0005,查询常量池中的第5项,可得:

       #5 = Utf8               num
    

    描述符索引的值为0x0006,查询常量池中的第6项,可得:

       #6 = Utf8               I
    

    属性计数器的值为0x0000,即没有任何的属性。

    确认过眼神,我遇上对的了。

    至此,字段表解读完成。

    4.6.5 注意事项

    1. 字段表集合中不会列出从父类或者父接口中继承而来的字段。
    2. 内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。
    3. 在Java语言中字段是无法重载的,两个字段的数据类型,修饰符不管是否相同,都必须使用不一样的名称,但是对于字节码来讲,如果两个字段的描述符不一致,那字段重名就是合法的.

    4.7 方法表

    字段表后就是方法表了。

    4.7.1 方法表计数器

    前面两个字节依然用来表示方法表的容量,看下demo字节码中的值:
    13.字节码-方法表容量计数器.png
    其值为0x0002,即有2个方法。

    4.7.2 方法表访问标志

    跟字段表一样,方法表也有访问标志,而且他们的标志有部分相同,部分则不同,方法表的具体访问标志如下:

    标志名称标志值含义
    ACC_PUBLIC0x0001方法是否为public
    ACC_PRIVATE0x0002方法是否为private
    ACC_PROTECTED0x0004方法是否为protected
    ACC_STATIC0x0008方法是否为static
    ACC_FINAL0x0010方法是否为final
    ACC_SYHCHRONRIZED0x0020方法是否为synchronized
    ACC_BRIDGE0x0040方法是否是有编译器产生的方法
    ACC_VARARGS0x0080方法是否接受参数
    ACC_NATIVE0x0100方法是否为native
    ACC_ABSTRACT0x0400方法是否为abstract
    ACC_STRICTFP0x0800方法是否为strictfp
    ACC_SYNTHETIC0x1000方法是否是有编译器自动产生的

    4.7.3 方法表结构

    方法表的结构实际跟字段表是一样的,方法表结构如下:

    类型名称含义数量
    u2access_flags访问标志1
    u2name_index方法名索引1
    u2descriptor_index描述符索引1
    u2attributes_count属性计数器1
    attribute_infoattributes属性集合attributes_count

    4.7.4 属性解读

    还是先回顾一下Demo中的源码:

        public int add() {
            num = num + 2;
            return num;
        }
    

    只有一个自定义的方法。但是上面方法表计数器明明是2个,这是为啥呢?
    这是因为它包含了默认的构造方法,我们来看下下面的分析就懂了,先看下Demo字节码中的值:
    14.字节码-方法表1.png
    这是第一个方法表,我们来解读一下这里面的16进制:
    访问标志的值为0x0001,查询上面字段访问标志的表格,可得字段为public;

    方法名索引的值为0x0007,查询常量池中的第7项,可得:

       #7 = Utf8               <init>
    

    这个名为<init>的方法实际上就是默认的构造方法了。

    描述符索引的值为0x0008,查询常量池中的第8项,可得:

       #8 = Utf8               ()V
    

    注:描述符不熟悉的话可以回头看看4.3.2.3的内容。

    属性计数器的值为0x0001,即这个方法表有一个属性。
    属性计数器后面就是属性表了,由于只有一个属性,所以这里也只有一个属性表。
    由于涉及到属性表,这里简单说下,下一节会详细介绍。
    属性表的前两个字节是属性名称索引,这里的值为0x0009,查下常量池中的第9项:

       #9 = Utf8               Code
    

    即这是一个Code属性,我们方法里面的代码就是存放在这个Code属性里面。相关细节暂且不表。下一节会详细介绍Code属性。

    先跳过属性表,我们再来看下第二个方法:
    16.字节码-方法表2.png
    访问标志的值为0x0001,查询上面字段访问标志的表格,可得字段为public;

    方法名索引的值为0x000b,查询常量池中的第11项,可得:

      #11 = Utf8               add
    

    描述符索引的值为0x000c,查询常量池中的第12项,可得:

      #12 = Utf8               ()I
    

    属性计数器的值为0x0001,即这个方法表有一个属性。
    属性名称索引的值同样也是0x0009,即这是一个Code属性。
    可以看到,第二个方法表就是我们自定义的add()方法了。

    4.7.5 注意事项

    1. 如果父类方法在子类中没有被重写(Override),方法表集合中就不会出现父类的方法。
    2. 编译器可能会自动添加方法,最典型的便是类构造方法(静态构造方法)<client>方法和默认实例构造方法<init>方法。
    3. 在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名,特征签名就是一个方法中各个参数在常量池中的字段符号引用的集合,也就是因为返回值不会包含在特征签名之中,因此Java语言里无法仅仅依靠返回值的不同来对一个已有方法进行重载。但在Class文件格式中,特征签名的范围更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个class文件中。

    4.8 属性表

    前面说到了属性表,现在来重点看下。属性表不仅在方法表有用到,字段表和Class文件中也会用得到。本篇文章中用到的例子在字段表中的属性个数为0,所以也没涉及到;在方法表中用到了2次,都是Code属性;至于Class文件,在末尾时会讲到,这里就先不说了。

    4.8.1 属性类型

    属性表实际上可以有很多类型,上面看到的Code属性只是其中一种,下面这些是虚拟机中预定义的属性:

    属性名称使用位置含义
    Code方法表Java代码编译成的字节码指令
    ConstantValue字段表final关键字定义的常量池
    Deprecated类,方法,字段表被声明为deprecated的方法和字段
    Exceptions方法表方法抛出的异常
    EnclosingMethod类文件仅当一个类为局部类或者匿名类是才能拥有这个属性,这个属性用于标识这个类所在的外围方法
    InnerClass类文件内部类列表
    LineNumberTableCode属性Java源码的行号与字节码指令的对应关系
    LocalVariableTableCode属性方法的局部变量描述
    StackMapTableCode属性JDK1.6中新增的属性,供新的类型检查检验器检查和处理目标方法的局部变量和操作数有所需要的类是否匹配
    Signature类,方法表,字段表用于支持泛型情况下的方法签名
    SourceFile类文件记录源文件名称
    SourceDebugExtension类文件用于存储额外的调试信息
    Synthetic类,方法表,字段表标志方法或字段为编译器自动生成的
    LocalVariableTypeTable使用特征签名代替描述符,是为了引入泛型语法之后能描述泛型参数化类型而添加
    RuntimeVisibleAnnotations类,方法表,字段表为动态注解提供支持
    RuntimeInvisibleAnnotations表,方法表,字段表用于指明哪些注解是运行时不可见的
    RuntimeVisibleParameterAnnotation方法表作用与RuntimeVisibleAnnotations属性类似,只不过作用对象为方法
    RuntimeInvisibleParameterAnnotation方法表作用与RuntimeInvisibleAnnotations属性类似,作用对象哪个为方法参数
    AnnotationDefault方法表用于记录注解类元素的默认值
    BootstrapMethods类文件用于保存invokeddynamic指令引用的引导方式限定符

    4.8.2 属性表结构

    属性表的结构比较灵活,各种不同的属性只要满足以下结构即可:

    类型名称数量含义
    u2attribute_name_index1属性名索引
    u2attribute_length1属性长度
    u1infoattribute_length属性表

    即只需说明属性的名称以及占用位数的长度即可,属性表具体的结构可以去自定义

    4.8.3 部分属性详解

    下面针对部分常见的一些属性进行详解

    4.8.3.1 Code属性

    前面我们看到的属性表都是Code属性,我们这里重点来看下。
    Code属性就是存放方法体里面的代码,像接口或者抽象方法,他们没有具体的方法体,因此也就不会有Code属性了。

    4.8.3.1.1 Code属性表结构

    先来看下Code属性表的结构,如下图:

    类型名称数量含义
    u2attribute_name_index1属性名索引
    u4attribute_length1属性长度
    u2max_stack1操作数栈深度的最大值
    u2max_locals1局部变量表所需的存续空间
    u4code_length1字节码指令的长度
    u1codecode_length存储字节码指令
    u2exception_table_length1异常表长度
    exception_infoexception_tableexception_length异常表
    u2attributes_count1属性集合计数器
    attribute_infoattributesattributes_count属性集合

    可以看到:Code属性表的前两项跟属性表是一致的,即Code属性表遵循属性表的结构,后面那些则是他自定义的结构。

    4.8.3.1.2 Code属性解读

    同样,解读Code属性只需按照上面的表格逐一解读即可。
    我们先来看下第一个方法表中的Code属性:
    15.字节码-Code属性表1.png
    属性名索引的值为0x0009,上面也说过了,这是一个Code属性;
    属性长度的值为0x00000026,即长度为38,注意,这里的长度是指后面自定义的属性长度,不包括属性名索引和属性长度这两个所占的长度,因为这哥俩占的长度都是固定6个字节了,所以往后38个字节都是Code属性的内容;
    max_stack的值为0x0002,即操作数栈深度的最大值为2;
    max_locals的值为0x0001,即局部变量表所需的存储空间为1;max_locals的单位是Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。
    code_length的值为0x00000000a,即字节码指令的10;
    code的值为0x2a b7 00 01 2a 04 b5 00 02 b1,这里的值就代表一系列的字节码指令。一个字节代表一个指令,一个指令可能有参数也可能没参数,如果有参数,则其后面字节码就是他的参数;如果没参数,后面的字节码就是下一条指令。
    这里我们来解读一下这些指令,文末最后的附录附有Java虚拟机字节码指令表,可以通过指令表来查询指令的含义。

    1. 2a 指令,查表可得指令为aload_0,其含义为:将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
    2. b7 指令,查表可得指令为invokespecial,其含义为:将操作数栈顶的reference类型的数据所指向的对象作为方法接受者,调用此对象的实例构造器方法、private方法或者它的父类的方法。其后面紧跟着的2个字节即指向其具体要调用的方法。
    3. 00 01,指向常量池中的第1项,查询上面的常量池可得:#1 = Methodref #4.#15 // java/lang/Object."<init>":()V 。即这是要调用默认构造方法<init>
    4. 2a 指令,同第1个。
    5. 04 指令,查表可得指令为iconst_1,其含义为:将int型常量值1推送至栈顶。
    6. b5 指令,查表可得指令为putfield,其含义为:为指定的类的实例域赋值。其后的2个字节为要赋值的实例。
    7. 00 02,指向常量池中的第2项,查询上面的常量池可得:#2 = Fieldref #3.#16 // com/april/test/Demo.num:I。即这里要将num这个字段赋值为1。
    8. b1 指令,查表可得指令为return,其含义为:返回此方法,并且返回值为void。这条指令执行完后,当前的方法也就结束了。

    所以,上面的指令简单点来说就是,调用默认的构造方法,并初始化num的值为1。
    同时,可以看到,这些操作都是基于栈来完成的。

    如果要逐字逐字的去查每一个指令的意思,那是相当的麻烦,大概要查到猴年马月吧。实际上,只要一行命令,就能将这样字节码转化为指令了,还是javap命令哈:

     javap -verbose Demo.class 
    

    截取部分输出结果:

       public com.april.test.Demo();
        descriptor: ()V
        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 num:I
             9: return
          LineNumberTable:
            line 7: 0
            line 8: 4
    

    看看,那是相当的简单。关于字节码指令,就到此为止了。继续往下看。

    exception_table_length的值为0x0000,即异常表长度为0,所以其异常表也就没有了;

    attributes_count的值为0x0001,即code属性表里面还有一个其他的属性表,后面就是这个其他属性的属性表了;
    所有的属性都遵循属性表的结构,同样,这里的结构也不例外。
    前两个字节为属性名索引,其值为0x000a,查看常量池中的第10项:

      #10 = Utf8               LineNumberTable
    

    即这是一个LineNumberTable属性。LineNumberTable属性先跳过,具体可以看下一小节。

    再来看下第二个方法表中的的Code属性:
    17.字节码-Code属性表2.png
    属性名索引的值同样为0x0009,所以,这也是一个Code属性;
    属性长度的值为0x0000002b,即长度为43;
    max_stack的值为0x0003,即操作数栈深度的最大值为3;
    max_locals的值为0x0001,即局部变量表所需的存储空间为1;
    code_length的值为0x00000000f,即字节码指令的15;
    code的值为0x2a 2a b4 20 02 05 60 b5 20 02 2a b4 20 02 ac,使用javap命令,可得:

      public int add();
        descriptor: ()I
        flags: ACC_PUBLIC
        Code:
          stack=3, locals=1, args_size=1
             0: aload_0
             1: aload_0
             2: getfield      #2                  // Field num:I
             5: iconst_2
             6: iadd
             7: putfield      #2                  // Field num:I
            10: aload_0
            11: getfield      #2                  // Field num:I
            14: ireturn
          LineNumberTable:
            line 11: 0
            line 12: 10
    

    可以看到,这就是我们自定义的add()方法;
    exception_table_length的值为0x0000,即异常表长度为0,所以其异常表也没有;
    attributes_count的值为0x0001,即code属性表里面还有一个其他的属性表;
    属性名索引值为0x000a,即这同样也是一个LineNumberTable属性,LineNumberTable属性看下一小节。

    4.8.3.2 LineNumberTable属性

    LineNumberTable属性是用来描述Java源码行号字节码行号之间的对应关系。

    4.8.3.2.1 LineNumberTable属性表结构
    类型名称数量含义
    u2attribute_name_index1属性名索引
    u4attribute_length1属性长度
    u2line_number_table_length1行号表长度
    line_number_infoline_number_tableline_number_table_length行号表

    line_number_info(行号表),其长度为4个字节,前两个为start_pc,即字节码行号;后两个为line_number,即Java源代码行号

    4.8.3.2.2 LineNumberTable属性解读

    前面出现了两个LineNumberTable属性,先看第一个:
    18.字节码-LineNumberTable属性1.png
    attributes_count的值为0x0001,即code属性表里面还有一个其他的属性表;
    属性名索引值为0x000a,查看常量池中的第10项:

      #10 = Utf8               LineNumberTable
    

    即这是一个LineNumberTable属性。
    attribute_length的值为0x00 00 00 0a,即其长度为10,后面10个字节的都是LineNumberTable属性的内容;
    line_number_table_length的值为0x0002,即其行号表长度长度为2,即有两个行号表;
    第一个行号表其值为0x00 00 00 07,即字节码第0行对应Java源码第7行;
    第二个行号表其值为0x00 04 00 08,即字节码第4行对应Java源码第8行。

    同样,使用javap命令也能看到:

          LineNumberTable:
            line 7: 0
            line 8: 4
    

    第二个LineNumberTable属性为:
    19.字节码-LineNumberTable属性2.png
    这里就不逐一看了,同样使用javap命令可得:

          LineNumberTable:
            line 11: 0
            line 12: 10
    

    所以这些行号是有什么用呢?当程序抛出异常时,我们就可以看到报错的行号了,这利于我们debug;使用断点时,也是根据源码的行号来设置的。

    4.8.3.2 SourceFile属性

    前面将常量池、字段集合、方法集合等都解读完了。最终剩下的就是一些附加属性了。
    先来看看剩余还未解读的字节码:
    18.字节码-附加属性.png
    同样,前面2个字节表示附加属性计算器,其值为0x0001,即还有一个附加属性。

    最后这一个属性就是SourceFile属性,即源码文件属性。
    先来看看其结构:

    4.8.3.2.1 SourceFile属性结构
    类型名称数量含义
    u2attribute_name_index1属性名索引
    u4attribute_length1属性长度
    u2sourcefile_index1源码文件索引

    可以看到,其长度总是固定的8个字节。

    4.8.3.2.2 SourceFile属性解读

    属性名索引的值为0x000d,即常量池中的第13项,查询可得:

      #13 = Utf8               SourceFile
    

    属性长度的值为0x00 00 00 02,即长度为2;
    源码文件索引的值为0x000e,即常量池中的第14项,查询可得:

     #14 = Utf8               Demo.java
    

    所以,我们能够从这里知道,这个Class文件的源码文件名称为Demo.java。同样,当抛出异常时,可以通过这个属性定位到报错的文件。

    至此,上面的字节码就完全解读完毕了。

    4.8.4 其他属性

    Java虚拟机中预定义的属性有20多个,这里就不一一介绍了,通过上面几个属性的介绍,只要领会其精髓,其他属性的解读也是易如反掌。

    5.总结

    通过手动去解读字节码文件,终于大概了解到其构成和原理了。断断续续写了比较长的时间,终于写完了,撒花~

    实际上,我们可以使用各种工具来帮我们去解读字节码文件,而不用直接去看这些16进制,神烦啊,哈哈。溜了溜了。

    溜了.gif

    6. 附录

    6.1 Java虚拟机字节码指令表

    字节码助记符指令含义
    0x00nop什么都不做
    0x01aconst_null将null推送至栈顶
    0x02iconst_m1将int型-1推送至栈顶
    0x03iconst_0将int型0推送至栈顶
    0x04iconst_1将int型1推送至栈顶
    0x05iconst_2将int型2推送至栈顶
    0x06iconst_3将int型3推送至栈顶
    0x07iconst_4将int型4推送至栈顶
    0x08iconst_5将int型5推送至栈顶
    0x09lconst_0将long型0推送至栈顶
    0x0alconst_1将long型1推送至栈顶
    0x0bfconst_0将float型0推送至栈顶
    0x0cfconst_1将float型1推送至栈顶
    0x0dfconst_2将float型2推送至栈顶
    0x0edconst_0将do le型0推送至栈顶
    0x0fdconst_1将do le型1推送至栈顶
    0x10bipush将单字节的常量值(-128~127)推送至栈顶
    0x11sipush将一个短整型常量值(-32768~32767)推送至栈顶
    0x12ldc将int, float或String型常量值从常量池中推送至栈顶
    0x13ldc_w将int, float或String型常量值从常量池中推送至栈顶(宽索引)
    0x14ldc2_w将long或do le型常量值从常量池中推送至栈顶(宽索引)
    0x15iload将指定的int型本地变量
    0x16lload将指定的long型本地变量
    0x17fload将指定的float型本地变量
    0x18dload将指定的do le型本地变量
    0x19aload将指定的引用类型本地变量
    0x1aiload_0将第一个int型本地变量
    0x1biload_1将第二个int型本地变量
    0x1ciload_2将第三个int型本地变量
    0x1diload_3将第四个int型本地变量
    0x1elload_0将第一个long型本地变量
    0x1flload_1将第二个long型本地变量
    0x20lload_2将第三个long型本地变量
    0x21lload_3将第四个long型本地变量
    0x22fload_0将第一个float型本地变量
    0x23fload_1将第二个float型本地变量
    0x24fload_2将第三个float型本地变量
    0x25fload_3将第四个float型本地变量
    0x26dload_0将第一个do le型本地变量
    0x27dload_1将第二个do le型本地变量
    0x28dload_2将第三个do le型本地变量
    0x29dload_3将第四个do le型本地变量
    0x2aaload_0将第一个引用类型本地变量
    0x2baload_1将第二个引用类型本地变量
    0x2caload_2将第三个引用类型本地变量
    0x2daload_3将第四个引用类型本地变量
    0x2eiaload将int型数组指定索引的值推送至栈顶
    0x2flaload将long型数组指定索引的值推送至栈顶
    0x30faload将float型数组指定索引的值推送至栈顶
    0x31daload将do le型数组指定索引的值推送至栈顶
    0x32aaload将引用型数组指定索引的值推送至栈顶
    0x33baload将boolean或byte型数组指定索引的值推送至栈顶
    0x34caload将char型数组指定索引的值推送至栈顶
    0x35saload将short型数组指定索引的值推送至栈顶
    0x36istore将栈顶int型数值存入指定本地变量
    0x37lstore将栈顶long型数值存入指定本地变量
    0x38fstore将栈顶float型数值存入指定本地变量
    0x39dstore将栈顶do le型数值存入指定本地变量
    0x3aastore将栈顶引用型数值存入指定本地变量
    0x3bistore_0将栈顶int型数值存入第一个本地变量
    0x3cistore_1将栈顶int型数值存入第二个本地变量
    0x3distore_2将栈顶int型数值存入第三个本地变量
    0x3eistore_3将栈顶int型数值存入第四个本地变量
    0x3flstore_0将栈顶long型数值存入第一个本地变量
    0x40lstore_1将栈顶long型数值存入第二个本地变量
    0x41lstore_2将栈顶long型数值存入第三个本地变量
    0x42lstore_3将栈顶long型数值存入第四个本地变量
    0x43fstore_0将栈顶float型数值存入第一个本地变量
    0x44fstore_1将栈顶float型数值存入第二个本地变量
    0x45fstore_2将栈顶float型数值存入第三个本地变量
    0x46fstore_3将栈顶float型数值存入第四个本地变量
    0x47dstore_0将栈顶do le型数值存入第一个本地变量
    0x48dstore_1将栈顶do le型数值存入第二个本地变量
    0x49dstore_2将栈顶do le型数值存入第三个本地变量
    0x4adstore_3将栈顶do le型数值存入第四个本地变量
    0x4bastore_0将栈顶引用型数值存入第一个本地变量
    0x4castore_1将栈顶引用型数值存入第二个本地变量
    0x4dastore_2将栈顶引用型数值存入第三个本地变量
    0x4eastore_3将栈顶引用型数值存入第四个本地变量
    0x4fiastore将栈顶int型数值存入指定数组的指定索引位置
    0x50lastore将栈顶long型数值存入指定数组的指定索引位置
    0x51fastore将栈顶float型数值存入指定数组的指定索引位置
    0x52dastore将栈顶do le型数值存入指定数组的指定索引位置
    0x53aastore将栈顶引用型数值存入指定数组的指定索引位置
    0x54bastore将栈顶boolean或byte型数值存入指定数组的指定索引位置
    0x55castore将栈顶char型数值存入指定数组的指定索引位置
    0x56sastore将栈顶short型数值存入指定数组的指定索引位置
    0x57pop将栈顶数值弹出 (数值不能是long或do le类型的)
    0x58pop2将栈顶的一个(long或do le类型的)或两个数值弹出(其它)
    0x59dup复制栈顶数值并将复制值压入栈顶
    0x5adup_x1复制栈顶数值并将两个复制值压入栈顶
    0x5bdup_x2复制栈顶数值并将三个(或两个)复制值压入栈顶
    0x5cdup2复制栈顶一个(long或do le类型的)或两个(其它)数值并将复制值压入栈顶
    0x5ddup2_x1dup_x1 指令的双倍版本
    0x5edup2_x2dup_x2 指令的双倍版本
    0x5fswap将栈最顶端的两个数值互换(数值不能是long或do le类型的)
    0x60iadd将栈顶两int型数值相加并将结果压入栈顶
    0x61ladd将栈顶两long型数值相加并将结果压入栈顶
    0x62fadd将栈顶两float型数值相加并将结果压入栈顶
    0x63dadd将栈顶两do le型数值相加并将结果压入栈顶
    0x64is将栈顶两int型数值相减并将结果压入栈顶
    0x65ls将栈顶两long型数值相减并将结果压入栈顶
    0x66fs将栈顶两float型数值相减并将结果压入栈顶
    0x67ds将栈顶两do le型数值相减并将结果压入栈顶
    0x68imul将栈顶两int型数值相乘并将结果压入栈顶
    0x69lmul将栈顶两long型数值相乘并将结果压入栈顶
    0x6afmul将栈顶两float型数值相乘并将结果压入栈顶
    0x6bdmul将栈顶两do le型数值相乘并将结果压入栈顶
    0x6cidiv将栈顶两int型数值相除并将结果压入栈顶
    0x6dldiv将栈顶两long型数值相除并将结果压入栈顶
    0x6efdiv将栈顶两float型数值相除并将结果压入栈顶
    0x6fddiv将栈顶两do le型数值相除并将结果压入栈顶
    0x70irem将栈顶两int型数值作取模运算并将结果压入栈顶
    0x71lrem将栈顶两long型数值作取模运算并将结果压入栈顶
    0x72frem将栈顶两float型数值作取模运算并将结果压入栈顶
    0x73drem将栈顶两do le型数值作取模运算并将结果压入栈顶
    0x74ineg将栈顶int型数值取负并将结果压入栈顶
    0x75lneg将栈顶long型数值取负并将结果压入栈顶
    0x76fneg将栈顶float型数值取负并将结果压入栈顶
    0x77dneg将栈顶do le型数值取负并将结果压入栈顶
    0x78ishl将int型数值左移位指定位数并将结果压入栈顶
    0x79lshl将long型数值左移位指定位数并将结果压入栈顶
    0x7aishr将int型数值右(符号)移位指定位数并将结果压入栈顶
    0x7blshr将long型数值右(符号)移位指定位数并将结果压入栈顶
    0x7ciushr将int型数值右(无符号)移位指定位数并将结果压入栈顶
    0x7dlushr将long型数值右(无符号)移位指定位数并将结果压入栈顶
    0x7eiand将栈顶两int型数值作“按位与”并将结果压入栈顶
    0x7fland将栈顶两long型数值作“按位与”并将结果压入栈顶
    0x80ior将栈顶两int型数值作“按位或”并将结果压入栈顶
    0x81lor将栈顶两long型数值作“按位或”并将结果压入栈顶
    0x82ixor将栈顶两int型数值作“按位异或”并将结果压入栈顶
    0x83lxor将栈顶两long型数值作“按位异或”并将结果压入栈顶
    0x84iinc将指定int型变量增加指定值(i++, i–, i+=2)
    0x85i2l将栈顶int型数值强制转换成long型数值并将结果压入栈顶
    0x86i2f将栈顶int型数值强制转换成float型数值并将结果压入栈顶
    0x87i2d将栈顶int型数值强制转换成do le型数值并将结果压入栈顶
    0x88l2i将栈顶long型数值强制转换成int型数值并将结果压入栈顶
    0x89l2f将栈顶long型数值强制转换成float型数值并将结果压入栈顶
    0x8al2d将栈顶long型数值强制转换成do le型数值并将结果压入栈顶
    0x8bf2i将栈顶float型数值强制转换成int型数值并将结果压入栈顶
    0x8cf2l将栈顶float型数值强制转换成long型数值并将结果压入栈顶
    0x8df2d将栈顶float型数值强制转换成do le型数值并将结果压入栈顶
    0x8ed2i将栈顶do le型数值强制转换成int型数值并将结果压入栈顶
    0x8fd2l将栈顶do le型数值强制转换成long型数值并将结果压入栈顶
    0x90d2f将栈顶do le型数值强制转换成float型数值并将结果压入栈顶
    0x91i2b将栈顶int型数值强制转换成byte型数值并将结果压入栈顶
    0x92i2c将栈顶int型数值强制转换成char型数值并将结果压入栈顶
    0x93i2s将栈顶int型数值强制转换成short型数值并将结果压入栈顶
    0x94lcmp比较栈顶两long型数值大小,并将结果(1,0,-1)压入栈顶
    0x95fcmpl比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
    0x96fcmpg比较栈顶两float型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶
    0x97dcmpl比较栈顶两do le型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将-1压入栈顶
    0x98dcmpg比较栈顶两do le型数值大小,并将结果(1,0,-1)压入栈顶;当其中一个数值为NaN时,将1压入栈顶
    0x99ifeq当栈顶int型数值等于0时跳转
    0x9aifne当栈顶int型数值不等于0时跳转
    0x9biflt当栈顶int型数值小于0时跳转
    0x9cifge当栈顶int型数值大于等于0时跳转
    0x9difgt当栈顶int型数值大于0时跳转
    0x9eifle当栈顶int型数值小于等于0时跳转
    0x9fif_icmpeq比较栈顶两int型数值大小,当结果等于0时跳转
    0xa0if_icmpne比较栈顶两int型数值大小,当结果不等于0时跳转
    0xa1if_icmplt比较栈顶两int型数值大小,当结果小于0时跳转
    0xa2if_icmpge比较栈顶两int型数值大小,当结果大于等于0时跳转
    0xa3if_icmpgt比较栈顶两int型数值大小,当结果大于0时跳转
    0xa4if_icmple比较栈顶两int型数值大小,当结果小于等于0时跳转
    0xa5if_acmpeq比较栈顶两引用型数值,当结果相等时跳转
    0xa6if_acmpne比较栈顶两引用型数值,当结果不相等时跳转
    0xa7goto无条件跳转
    0xa8jsr跳转至指定16位offset位置,并将jsr下一条指令地址压入栈顶
    0xa9ret返回至本地变量
    0xaatableswitch用于switch条件跳转,case值连续(可变长度指令)
    0xablookupswitch用于switch条件跳转,case值不连续(可变长度指令)
    0xacireturn从当前方法返回int
    0xadlreturn从当前方法返回long
    0xaefreturn从当前方法返回float
    0xafdreturn从当前方法返回do le
    0xb0areturn从当前方法返回对象引用
    0xb1return从当前方法返回void
    0xb2getstatic获取指定类的静态域,并将其值压入栈顶
    0xb3putstatic为指定的类的静态域赋值
    0xb4getfield获取指定类的实例域,并将其值压入栈顶
    0xb5putfield为指定的类的实例域赋值
    0xb6invokevirtual调用实例方法
    0xb7invokespecial调用超类构造方法,实例初始化方法,私有方法
    0xb8invokestatic调用静态方法
    0xb9invokeinterface调用接口方法
    0xba无此指令
    0xbbnew创建一个对象,并将其引用值压入栈顶
    0xbcnewarray创建一个指定原始类型(如int, float, char…)的数组,并将其引用值压入栈顶
    0xbdanewarray创建一个引用型(如类,接口,数组)的数组,并将其引用值压入栈顶
    0xbearraylength获得数组的长度值并压入栈顶
    0xbfathrow将栈顶的异常抛出
    0xc0checkcast检验类型转换,检验未通过将抛出ClassCastException
    0xc1instanceof检验对象是否是指定的类的实例,如果是将1压入栈顶,否则将0压入栈顶
    0xc2monitorenter获得对象的锁,用于同步方法或同步块
    0xc3monitorexit释放对象的锁,用于同步方法或同步块
    0xc4wide<待补充>
    0xc5multianewarray创建指定类型和指定维度的多维数组(执行该指令时,操作栈中必须包含各维度的长度值),并将其引用值压入栈顶
    0xc6ifnull为null时跳转
    0xc7ifnonnull不为null时跳转
    0xc8goto_w无条件跳转(宽索引)
    0xc9jsr_w跳转至指定32位offset位置,并将jsr_w下一条指令地址压入栈顶
    展开全文
  • java字节码查看工具

    2017-08-23 15:37:34
    java字节码查看工具
  • 轻松看懂Java字节码,对java字节码的详细分析,理解java字节码
  • Java字节码简单说明
  • Java 字节码编辑工具

    2018-10-11 20:39:37
    优秀的Java字节码可视化编辑工具,使用方便上手简单。
  • 主要介绍了Javassist如何操作Java 字节码,帮助大家更好的理解和学习Java,感兴趣的朋友可以了解下
  • java字节码文件反编译

    2018-01-17 11:12:14
    jd-gui java字节码反编译 class反编译 jd-gui java字节码反编译 class反编译
  • java jclazz java字节码编译器
  • java字节码查看器,jclasslib bytecode viewer可以对字节码文件进行修改和查看对应的变量
  • 关于Java字节码

    千次阅读 2018-09-24 10:25:23
    关于Java字节码 概述 从写Java文件到编译成字节码文件(也就是.class文件)的过程也就是Java文件编译的过程,我们所写的是Java文件而Java虚拟机编译的是字节码文件 class文件格式 ...

    关于Java字节码

    1. 概述

    从写Java文件到编译成字节码文件(也就是.class文件)的过程也就是Java文件编译的过程,我们所写的是Java文件而Java虚拟机编译的是字节码文件

    2. class文件格式

    class文件格式

    3. 举个栗子来说明一下

    ①先写一个.java文件
    

    Demo的java文件

    ②打开.class文件
    

    Demo的class文件

    3.1 magic 魔数

    前四个字节为魔数,值为:0xCAFE BABE Java创始人 James Gosling制定 用来表示是class 文件 有人会问为什么为cafebaby,联想一下java的咖啡杯图标就知道了。

    3.2 minor_version & major_version 版本号

    minor_version & major_version对应 的16进制为 0000 0034 前两个字节为minor_version 为此版本号 后两个字节为主版本号。所有该文件的版本号为1.8。

    版本号JDK版本号
    2eJDK1.2
    2fJDK1.3
    30JDK1.4
    31JDK1.5
    32JDK1.6
    33JDK1.7
    34JDK1.8

    3.3 常量池

    在版本号之后的为常量池,常量池可以理解为Class文件中的资源仓库。他是Class文件结构中与其他项目关联最多的数据类型,也是占用Class文件空间最大的数据项之一,同时它还是在Class文件中第一个出现的表类型数据项目。由于常量池中的常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据,代表常量池容量计数值(constant_pool_count),这个计数是从1开始而不是从0开始的
    常量池主要存放两大类字面量和符号引用,字面量如文本字符串,声明为final的常量值等,符号引用属于编译原理方面的概念包括三种常量:①类和接口的权限定名 ②字段的名称和描述符 ③方法名和描述符
    常量池每一项常量都是一个表,jdk1.7之前有11种结构不同的表结构数据,在jdk1.7中又增加了三种下图为14种常量类型所代表的具体含义
    常量所代表的具体含义

    在魔数和版本号后面为常量池,0x0016 为常量池容量,十进制为22,这就代表了常量池中有21项常量,索引值范围为1~~21。在Class 文件格式规范制定的时候,设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何常量池项目的含义”这种情况就可以吧索引置为0来表示。
    0x07查表的标志列可以知道这个常量属于CONSTANT_Class_info类型 此类型代表一个类或者接口的符号引用。
    CONSTANT_Class_info类型比较简单具体结构看下图:
    CONSTANT_Class_info的结构
    tag是标志位,它用于区分常量类型。name_index 是一个索引值 在这里值为0x0002 也即是指向了常量池里的第二个常量
    0x01为常量池中的第二个常量根据标志列可知这个常量为CONSTANT_Utf8_info类型的常量,下图为常量结构

    CONSTANT_Utf8_info常量的结构
    length值说明了这个utf-8编码的字符串长度是多少字节,在这里为0x013表示19个字节,他后面紧跟着的长度为length字节的连续数据是一个使用utf-8缩略编码表示的字符串。
    636f~~~6f 19个字节表示的是使用utf-8缩略编码表示的字符串。翻译完为:com/baidu/test/demo
    我们现在已经分析了两个常量,后续的19个都可以这样分析完成但是Oracle公司给我们提供了一个专门用于分析class文件字节码的工具:Javap 具体如下图
    通过javap解析出的常量池
    从图中可知计算机已经帮我们吧整个常量池都计算了出来。

    3.4 access_flage 访问标志

    在常量池结束后,紧接着的两个字节代表访问标志 access_flags,这个标志用于识别一些类或者接口层测的访问信息,比如:是类还是接口,是否定义为public,是否定义为abstract类型,如果是类的话是否被声明为final等等下面是具体的含义表

    访问标准的具体含义表
    在这里具体值为0x0021 0x0021为0x0021和0x0001的与运算结果,为啥要进行与运算呢,因为access_flags 中共有16个标志位可以使用,当前只定义了8个,没有使用到的标志要求为0,Demo是一个普通java类,不是接口、枚举、或者注解,只有public关键字修饰

    3.5 this_class & super_class &interaces 类索引、父类索引与接口索引集合

    this_class(类索引)、super_class(父类索引)都是一个u2类型的数据,而interfaces(接口索引集合)是一组u2类型的数据的集合class文件由这三个数据来确定这个类的继承关系
    Demo的class文件
    0x0001、0x0003、0x0000 分别表示类索引为1 、父类索引为3、接口索引集合大小为0
    通过前面用javap算出来的常量池可以知道
    类索引为:com/baidu/test/Demo;
    父类索引为: java/lang/Object;

    3.6 field_info字段表集合

    字段表用于描述接口或者类中声明的变量,但是不包括在方法内部声明的局部变量。可以包括的信息有:public、private、protected、static、final、volatile、transient、基本类型、对象、数组、字段名称
    下图为字段表的格式
    字段表结构
    字段访问标注如下图
    字段访问标志
    Demo的class文件
    0x0001 为fields_count
    0x0002 为access_flags
    0x0005 为name_index
    0x0006 为descriptor_index

    3.7 methods 方法表集合

    方法表集合与字段表集合的结构完全一致,结构如下
    在这里插入图片描述
    在访问标志和属性集合的可选项中有所不同,下图为方法访问标志
    在这里插入图片描述
    有人会提出疑问,不知道方法中的代码去哪里了,方法中的Java代码经过编译器编译成字节码指令后存放到方法属性表的一个名为code的属性中了,在下面属性表会进行讲解在这里插入图片描述
    0x0002:methods_count
    0x0001:access_flags
    0x0007:descriptor_index
    0x0008:attribute_count
    0x0009:name_index

    3.8attribute_info属性表集合

    为了能正确解析 class文件,《Java虚拟机规范(第二版)》中预定了9项虚拟机实现应当能识别的属性,而在最新的《java虚拟机规范(java SE 7)》版本中,预定义属性已经增加到了21项,见下图。

    虚拟机规范预定义的属性
    接下来对其中一些关键字段进行讲解
    (1).code
    Java程序方法体中的代码经过javac编译器处理之后,最终变为字节码指令存在code属性内。code属性表结构如下:
    code属性表结构
    attribute_name_index : 是项指向CONSTANT_Utf8_info 型常量的索引,常量值固定为“code”,他代表该属性的名称;
    attribute_length:指示了属性的长度,由于属性名称索引与属性长度一共为6个字节,所以属性值的长度固定为整个属性表长度减去6个字节。
    max_stack 代表了操作数栈深度的最大值,虚拟机运行的时候需要根据这个值来分配栈帧中的操作栈深度。
    max_locals:代表了局部变量表所需的存储空间。
    code_length:和code用来存储java源程序编译后生成的字节码指令。code_length 表示字节码的长度,code是用于存储字节码指令的一系列字节流。
    code 属性是class文件中最重要的一个属性,如果把java程序中的信息分为代码和元数据两部分,那么在整个class文件中,code属性用于描述代码(方法体内的),所有的其他数据项目都用于描述元数据。
    (2). exceptions属性
    exception属性的作用列举方法中可能抛出的受查异常的(Checked Exceptions)也就是方法描述时在throws关键字后面列举的异常。它的结构见下表
    exceptions属性表结构
    (3).LineNumberTable 属性
    LineNumberTable 属性用于描述java源代码行号与字节码行号之间的对应关系。属性表结构如下。
    在这里插入图片描述
    (4).LocalVariableTable属性
    LocalVariableTable属性用于描述栈帧中局部变量表中的变量与java源码中定义的变量之间的关系。属性表结构见下图。
    LocalVariableTable属性表结构
    (6).SourceFile属性
    SourceFile属性用于记录生成这个class文件的源码文件名称。结构见下图。
    SourceFile属性表结构
    (6).ConstantValue属性
    ConstantValue属性的作用是通知虚拟机自动为静态变量赋值。只有被static修饰的变量才能使用这个属性。结构见下图。
    ContantValue属性表结构
    (7).InnerClasses属性
    InnerClasses这个属性会在虚拟机类加载的字节码验证阶段被新类型检查验证其使用。结构见下表
    InnerClasses属性表结构
    (8).Depercated & Synthetic
    Depercated & Synthetic 两个属性的属于标志类型的布尔属性,只存在有和没有的区别,没有属性值的概念。结构加下图
    Depercated & Synthetic属性结构

    4总结

    class文件是java虚拟机执行引擎的数据入口,也是java技术体系的基础构成之一,了解class文件的结构对进一步了解虚拟机执行引擎有很重要的意义。

    展开全文
  • Java字节码优化框架

    2014-01-26 17:17:52
    Java字节码优化框架
  • Java 字节码简单说明
  • JBE - Java 字节码编辑器

    热门讨论 2016-03-18 15:04:42
    Java 字节码编辑器
  • 本篇文章对Java字节码指令集的使用进行了详细的介绍。需要的朋友参考下
  • 注意:javap 查看【class文件的字节码】信息 Java字节码深入解析 一:Java字节代码的组织形式  类文件{  OxCAFEBABE,小...
  • Java字节码揭秘

    2011-12-03 22:05:00
    Java字节码揭秘(Java体系结构、执行引擎、类加载器 等等)
  • JavaBytecodeAnalyzer 解析 Java 字节码,学习字节码指令集 解析 class 文件为字节码,以熟悉 class 文件结构 学习 java bytecode 指令集
  • Recaf 一个现代Java字节码编辑器
  • Java字节码工程工具包 千叶茂(Shigeru Chiba)版权所有(C)1999-2020,保留所有权利。 Javassist(JAVA编程ASSISTant)使Java字节码操作变得简单。 它是一个用Java编辑字节码的类库。 它使Java程序可以在运行时...
  • Java Bytecode Editor是修改和分析java字节码结构和类文件的有效工具:下载解压后,双击jbe. bat(Windows)或jbe.sh(Linux)即可运行JBE。src目录下的是JBE的源码。
  •  ASM是一个java字节码操纵框架,它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 433,407
精华内容 173,362
关键字:

java字节码

java 订阅