简单分析Class类文件

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/GD_Hacker/article/details/81428877

阅读《深入理解Java虚拟机》第六章后,记录下使用WinHex工具分析二进制文件的过程。


源码沿用书中的例子:

public class Hex {

    private int m;

    public int inc(){
        return m+1;
    }
}

使用WinHex打开编译后的.class文件:
hex
首先需要知道的是:Class文件中只有两种数据类型:无符号数和表。无符号数属于基本的数据类型,以u1,u2,u4,u8来代表占用1个字节,2个字节,4个字节和8个字节。表则为多个无符号数或其他表构成的复合数据类型,惯以“_info”结尾来表示表结构。所以一个Class文件也可以看做是一张表。

先列出Class文件中包含哪些数据项:

类型 名称 数量
u4 magic 1
u2 minor_version 1
u2 major_version 1
u2 constant_pool_count 1
cp_info constant_pool constant_pool_count-1
u2 access_flags 1
u2 this_class 1
u2 super_class 1
u2 interfaces_count 1
u2 interfaces interfaces_count
u2 fields_count 1
field_info fields fields_count
u2 methods_count 1
method_info methods methods_count
u2 attributes_count 1
attribute_info attributes attributes_count

开始分析各数据项:

魔数(magic)

魔数的唯一作用就是用来识别文件,并且不是Class文件所独有,其他诸如gif或者jpeg图片文件头中都有魔数。在Class文件中,魔数的作用就是告诉虚拟机,我这个文件是Class文件,使得虚拟机能够接受该文件。
magic
Class文件的魔数值为CAFEBABE,占用4个字节。“咖啡宝贝”?可能这就是Java用咖啡作为商标的原因吧~

次版本号(minor_version)和主版本号(major_version)

各占用两个字节,表示Class文件是由哪个JDK版本编译输出
version
高版本JDK可以向下兼容低版本,但不能运行更高版本的Class文件,只要超过虚拟机所能接受的版本,即使Class内容无任何变化,虚拟机都会拒绝执行。这里我使用的JDK次版本号为0x0000=0,主版本号为0x0034=52,所以该Class文件是由JDK1.8.0所编译的,其他版本号对应的JDK可以在某度找到。

常量池

constant_pool_count:常量池容量计数值,就是指常量池中一共有多少项常量,用两个字节表示。值得注意的是,这里的计数是从1开始的,而不是我们习惯的从0开始,原因为当某些数据需要表达“不引用任何一个常量池项目”的含义时,就可以将索引值置为0。
counstant_count
0x0016转为十进制是22,表示池中共有21项常量,索引值为1~21。

常量池中主要存放字面量和符号引用,字面量近似于Java中所说的常量概念,如文本字符串String s="123",声明为final的常量值static final int V=123。而符号引用相对比较抽象,个人理解为虚拟机在加载Class文件时,会依据符号引用找到真正指向内存的引用或者句柄。符号引用包括三类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符

常量池中共有14项常量,每项常量都是结构互不相同的表结构数据,这里列出常量池中的项目类型:

类型 标志 描述
CONSTANT_Utf8_info 1 URF-8编码的字符串
CONSTANT_Integer_info 3 整型字面量
CONSTANT_Float_info 4 浮点型字面量
CONSTANT_Long_info 5 长整型字面量
CONSTANT_Double_info 6 双精度浮点型字面量
CONSTANT_Class_info 7 类或接口的符号引用
CONSTANT_String_info 8 字符串类型字面量
CONSTANT_Fieldref_info 9 字段的符号引用
CONSTANT_Methodref_info 10 类中方法的符号引用
CONSTANT_InterfaceMethodref_info 11 接口中方法的符号引用
CONSTANT_NameAndType_info 12 字段或方法的部分符号引用
CONSTANT_MethodHandle_info 15 表示方法句柄
CONSTANT_MethodType_info 16 标识方法类型
CONSTANT_InvokeDynamic_info 18 表示一个动态方法调用点

每一项常量表中,都有一项类型为u1的标志位tag,用于标识属于哪一种常量类型,我们继续分析Class文件:
c1
0x0A=10,从表中我们查找标志为10的常量项,找到CONSTANT_Methodref_info:类中方法的符号引用。它的表结构为:

扫描二维码关注公众号,回复: 3053985 查看本文章
类型 名称 数量 描述
u1 tag 1 值为10
u2 index 1 指向声明方法的类描述符CONSTANT_Class_info的索引项
u2 index 1 指向名称及类型描述符CONSTANT_NameAndType_info的索引项

index索引的值表示常量池中的第几个常量,c2 0x00040x0012分别表示第4项常量和第18项常量。继续分析Class文件:c3 tag=0x09对应的常量项为:CONSTANT_Fieldref_info,它的表结构:

类型 名称 数量 描述
u1 tag 1 值为9
u2 index 1 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项
u2 index 1 指向字段描述符CONSTANT_NameAndType_info的索引项

c4
红蓝两项常量都为CONSTANT_Class_info,表结构为:

类型 名称 数量 描述
u1 tag 1 值为7
u2 index 1 指向全限定名常量项索引项

c5
接下来到CONSTANT_Utf8_info常量项,表结构

类型 名称 数量 描述
u1 tag 1 值为1
u2 length 1 指向声明字段的类或者接口描述符CONSTANT_Class_info的索引项
u1 bytes 1 指向字段描述符CONSTANT_NameAndType_info的索引项

补充说明,引用书中原话:

length值说明了这个UTF-8编码的字符串长度是多少字节,后面紧跟着的长度为length字节的连续数据是一个使用UTF-8缩略编码表示的字符串。UTF-8缩略编码与普通的缩略编码区别是:从 ‘\u0001’ 到 ‘u\007f’ 之间的字符(相当于1~127的ASCII码)的缩略编码使用一个字节表示,从 ‘\u0080’ 到 ‘\u07ff’ 之间的所有字符的缩略编码用两个字节表示,从 ‘\u0800’ 到 ‘\uffff’ 之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示。

为了演示,我将源码private int m;改为private int 你;,再看看Class文件:
c6
长度由一个字节变成三个字节,m:'\u006d' 你:'\u4f60'。篇幅有限,剩余的常量项可以照这个思路理解,遇到新的常量项可以在某度找到相关的表结构。

访问标志

常量池结束后,紧接着的两个字节就是访问标志(access_flags),访问标志用于标识该Class文件的访问信息,详情参考下表:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否被声明final,只有类可以设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语意,invokespecial指令的语意在JDK 1.0.2发生过改变,为了区别这条指令使用哪种语意,JDK 1.0.2之后编译出来的类的这个标志都必须为真
ACC_INTERFACE 0x0200 标识这是一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,对于接口或者抽象类来说,此标志值为真,其他类值为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举

那么,由源码可以知道,除ACC_PUBLICACC_SUPER标志为真之外,其余都为假,所以最终这两个字节的值为:0x0001|0x0020=0x0021,查看Class文件,确实如此:
a1

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

类索引和父类索引都是一个u2类型的数据,接口索引集合是一组u2类型的数据的集合,一个Class文件就是由这三项数据来确定类的继承关系。
i1
类索引和父类索引都是指向CONSTANT_Utf8_info类型常量,而接口索引入口的第一项是u2类型的接口计数器,表示索引表的容量,源码并没有实现任何接口,所以这里计数器为0。

字段表集合

用于描述接口或者类中声明的变量,注意这里的变量只包括类变量和实例变量,而不包括方法中的局部变量。回想一下Java中的变量,无非就包含几种信息:字段作用域(public、private……),字段修饰符(static、final、volatile……),字段类型(基本数据类型,对象,数组),字段名称。看看字段表结构:

类型 名称 数量 描述
u2 access_flags 1 字段访问标志
u2 name_index 1 字段简单名称
u2 descriptor_index 1 描述符
u2 attributes_count 1 属性数量
u2 attributes attributes_count 属性值

字段访问标志:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 字段是否public
ACC_PRIVATE 0x0002 字段是否private
ACC_PROTECTED 0x0004 字段是否protected
ACC_STATIC 0x0008 字段是否static
ACC_FINAL 0x0010 字段是否final
ACC_VOLATILE 0x0040 字段是否volatile
ACC_TRANSIENT 0x0080 字段是否transient
ACC_SYNTHETIC 0x1000 字段是否由编译器自动产生
ACC_ENUM 0x4000 字段是否enum

我们结合Class文件来分析:
f1
0x0001是Class文件的fields_count数据项,忘记了可以查看本文的第一张表,代表共有多少个变量,源码中只有一个,所以这里的值为1。0x0002字段的访问标志,明显我们只用了private修饰符,所以除ACC_PRIVATE以外的访问标志都为0。0x0005字段的简单名称,指向常量池中索引值为5的常量,值是m0x0006字段的描述符,指向常量池中索引值为6的常量,值是I,表示int类型,其他数据类型网上都有介绍。0x0000是字段表的倒数第二项,代表属性数量,属性之后再介绍,这里属性数量为0,故不存在之后的属性内容。

方法表

方法表结构和字段表结构一样,不同在于表中数据项的值有所变化。对于访问标志,volatiletransient关键字无法修饰方法,去除;相对地,增加了synchronizednativestrictfpabstract关键字。而方法中的代码,存放在code属性中,属性之后介绍。我们继续看看Class文件:
m1
0x0002表示共有两个方法,分别为默认无参构造方法和inc()成员方法。0x0001第一个方法的访问标志,因为只有ACC_PUBLIC为真,所以值等于1。0x0007指向构造器<init>0x0008指向()V,其中V表示特殊类型void。0x0001属性数量,代表包含1个属性。
另外,需要注意的是,除非子类重写了父类中的方法,子类方法表中才会出现父类方法的信息。并且,除了父类方法外,编译器也有可能自动添加方法,例如类构造器<cinit>方法和实例构造器<init>方法。

属性表

在《Java虚拟机规范(Java SE 7)》版中,预定义属性共有21项,每项属性只能在规定的范围内使用,例如Code属性只能在方法表中使用,Deprecated属性可以在“类,方法表,字段表”中使用 。一个符合规则的属性表应该满足以下结构:

类型 名称 数量
u2 attribute_name_index 1
u4 attribute_length 1
u1 info attribute_length

在讲述方法表时,最后提到<init>方法有一个属性,我们接着分析:
m2
该属性的值为0x0009,指向常量池中的第9项Code,表示属性名为
Code。Code属性是Class文件中最重要的一个属性,用于描述方法中的代码。先看看Code属性表结构:

类型 名称 数量 描述
u2 attribute_name_index 1 属性名称
u4 attribute_length 1 属性长度
u2 max_stack 1 最大操作数栈深度,在方法中涉及大量栈的压入和弹出指令,虚拟机运行时根据这个参数来分配栈帧
u2 max_locals 1 局部变量表所需的存储空间,Slot为单位,对于不超过32位的数据类型,占用一个Slot;而类似double,long就要占用两个Slot。用于存放方法参数,显示异常处理参数,方法体定义的局部变量。
u4 code_length 1 字节码指令数量
u1 code code_length 字节码指令
u2 exception_table_length 1 异常处理表数量
exception_info exception_table exception_table_length 异常处理表
u2 attributes_count 1 属性数量
attribute attributes attributes_count 属性

关于max_locals比较疑惑,大多数都会认为这里的存储空间就是局部变量数量相加,其实不然,每个Slot都可以复用,举个栗子:

//占用一个Slot
void inc1(String s){}

//占用两个Slot
void inc2(String s){s+=this.toString();}

首先需要知道的是,在每个实例方法中,this都会作为方法参数隐式地传入,也就是说每个方法至少需要一个Slot。那为什么inc1方法只占用一个Slog,不是另外传入了参数s么?原因很简单,inc1方法中没有一个操作同时需要sthis参与,一个Slot可以被两者复用,而inc2方法中完成s+=this.toString();需要thiss的同时参与,所以占用两个Slot。

code代表字节码指令,也是我们分析Class文件的重点关注对象。占用一个字节,每个字节代表一条字节码指令,每条字节码指令都有不同的含义,例如在上面的截图可以看到共有五条指令:

  • 2A aload_0 将第一个引用类型本地变量推送至栈顶
  • B7 invokespecial 调用超类构造方法,实例初始化方法,私有方法
  • 00 nop 什么都不做,还记得之前说过常量池是从1开始计数,需要保留0来表示不引用任何一个常量池项目
  • 01 aconst_null 将int型-1推送至栈顶
  • B1 return 从当前方法返回void

这五条字节码指令描述了本文实例代码中inc方法的执行过程。目前Java虚拟机规范大概定义了200条指令,具体分析时可以通过查表获取各指令含义。之后就是异常处理表相关项,和属性表相关项,相关表结构可自行查阅资料。另外,配合javap命令可以更方便分析Class文件。

猜你喜欢

转载自blog.csdn.net/GD_Hacker/article/details/81428877