在上文中我们分析到了常量池(具体代码请看上文)。阅读class文件重点在于了解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];
}
访问权限
紧接着常量池的2个字节是access_flags,它的值表示某个类或者接口的访问权限及属性。每个标志的取值及其含义如下表所示:
标志名 | 值 | 含义 |
ACC_PUBLIC | 0x0001 | 声明为public,可以包外访问 |
ACC_FINAL | 0x0010 | 声明为final,不允许有子类 |
ACC_SUPER | 0x0020 | 当用到invokespecial指令时,需要对父类方法做特殊处理 |
ACC_INTERFACE | 0x0200 | 该class文件定义的是接口而不是类 |
ACC_ABSTRACT | 0x0400 | 声明为abstract,不能被实例化 |
ACC_SYNTHETIC | 0x1000 | 声明为synthetic,表示该class文件并非由java源代码所生成 |
ACC_ANNOTATION | 0x2000 | 标识注解类型 |
ACC_ENUM | 0x4000 | 标识枚举类型 |
示例代码的字节码显示access_flags为0x0021
,可是表中没有这个值呀??这是因为JVM将不同的值进行或运算得到不同的组合,这里就是ACC_SUPER
和ACC_PUBLIC
。ACC_SUPER可以不用管它,只需要知道ACC_PUBLIC,即这个类使用public修饰,确实如此。
类和父类
继access_flags后的4个字节表当前类名索引和父类名索引,具体的值在常量池中。如图所示,this_class值为3,super_class值为4,分别对应常量池中的bytecode/Test1和java/lang/Object。如果这个class文件的super_class值为0,那么这个class文件只可能用来表示Object类,因为它是唯一没有父类的类。
接口
接下来的4个字节与接口有关(如果有父接口的话是4个字节,无父接口则仅为2个字节)。前2个字节的interfaces_count(接口计数器)表示当前类或接口的直接超接口量。后2个字节的interfaces[](接口表)中的每个成员的值必须是对常量池表中某项的有效索引,它的长度为interfaces_count。每个interfaces[i]必须为CONSTANT_Class_info结构,其中0<=i<interfaces_count。在interfaces[]中,各成员所表示的接口顺序和对应的源代码中给定的接口顺序(从左至右)一样,即interfaces[0]对应的是源代码中最左边的接口。如果interfaces_count为0,那么在字节码中是不存在interfaces[]的。
成员字段
接下来的2个字节表示fields_count(字段计数器),它的值表示当前class文件fields表的成员个数。当前字节码中的值为0x0001(十进制为1)
,我只声明了一个实例字段a
。fields表中每个成员都是一个field_info结构(如下所示),用于表示该类或接口所声明的类字段(static修饰)或者实例字段。
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
field_info结构各项的说明如下:
- access_flags:它的值用来表示字段的访问权限和基本属性。具体如下表所示:
标志名 | 值 | 含义 |
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)的成员 |
- name_index:顾名思义,这是表示字段名称的索引。它的值必须是常量池中的一个有效索引,且必须为CONSTANT_Utf8_info结构。
- descriptor_index:它的值也必须是常量池中的一个有效索引,且必须是CONSTANT_Utf8_info结构。
- attributes_count:它的值表示当前字段的附加属性(attribute_info)的数量。
- attributes[]:属性表(attributes[])中的每个成员,其值必须是attribute_info结构(如下表示的是一个通用属性结构)。一个字段可以管理任意多个属性。
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
示例class文件的分析如下:
方法
接下来的2个字节表示methods_count(方法计数器),表示当前class文件methods表的成员个数。methods表中的每个成员都是一个method_info结构。methods[](方法表)中的每个成员都必须是一个method_info结构,用于表示当前类或接口中某个方法的完整描述。
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
method_info结构各项的说明如下:
- access_flags:它的值用来表示字段的访问权限和基本属性。具体如下表所示:
标志名 | 值 | 含义 |
ACC_PUBLIC | 0x0001 | 声明为public,可以包外访问 |
ACC_PRIVATE | 0x0002 | 声明为private,只能在定义该方法的类中访问 |
ACC_PROTECTED | 0x0004 | 声明为protected,子类可以访问 |
ACC_STATIC | 0x0008 | 声明为static |
ACC_FINAL | 0x0010 | 声明为final,不能被覆盖 |
ACC_SYNCHRONIZED | 0x0020 | 声明为synchronized,对该方法的调用,将包装在同步锁(monitor)里 |
ACC_BRIDGE | 0x0040 | 声明为bridge方法,由编译器产生 |
ACC_TRANSIENT | 0x0080 | 声明为transient,被标识的字段不会为持久化对象管理器所写入或读取 |
ACC_VARARGS | 0x0080 | 表示方法带有变长参数 |
ACC_NATIVE | 0x0100 | 声明为native,该方法不是用Java语言实现 |
ACC_ABSTRACT | 0x0400 | 声明为abstract,该方法没有实现代码 |
ACC_STRICT | 0x0800 | 声明为strictfp,使用FP-strict浮点模式 |
ACC_SYNTHETIC | 0x1000 | 该方法是由编译器合成的,而不是由源代码编译 |
- name_index:顾名思义,这是表示字段名称的索引。它的值必须是常量池中的一个有效索引,且必须为CONSTANT_Utf8_info结构。
- descriptor_index:它的值也必须是常量池中的一个有效索引,且必须是CONSTANT_Utf8_info结构。
- attributes_count:它的值表示当前字段的附加属性(attribute_info)的数量。
- attributes[]:属性表(attributes[])中的每个成员,其值必须是attribute_info结构。一个字段可以管理任意多个属性。方法中重点在于Code属性。有点复杂
在实例代码的class文件中可以看见methods_count值为0x0003
,说明代码中有3个方法,虽然我们只是声明了两个方法,可是我们不要忘记了无参构造方法,所以共有3个方法。
接下来开始分析第一个方法。根据method_info结构,第一个access_flag属性为0x0001(public)
,第二个name_index属性为0x0007(#7)
,第三个descriptor_index属性为0x0008(#8)
,第四个attributes_count属性为0x0001(十进制为1)
,说明附加属性数量为1。不难发现第一个方法就是自动生成的无参构造方法了。
根据attribute_info结构,第一个属性的name_index为0x0009
,对应常量池中的Code
。紧接着是4个字节的属性长度0x00000038(十进制为56)
,说明Code属性长度为56字节。接下来是分析这56个字节的内容了,前提是我们需要了解Code这个属性,这是JVM预定义的一个属性。其结构如下:
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[exceptionn_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Code_attribute结构各项的说明如下:
- attribute_name_index:其值必须是对常量池的一个有效索引。常量池表在该索引处的成员必须是CONSTANT_Utf8_info结构,用以表示字符串"Code"。
- attribute_length:表示当前属性的长度,不包括初始的6个字节(attribute_name_index和attribute_length)
- max_stack:当前方法的操作数栈在方法执行的任何时间点的最大深度
- max_locals:分配在当前方法引用的局部变量表中的局部变量个数,其中也包括调用此方法时用于传递参数的局部变量。
- code_length:当前方法code[]数组的字节数,值必须大于0,即code[]数组不能为空
- code[]:实现当前方法的Java虚拟机代码的实际字节内容
- exception_table_length:exception_table表的成员个数
exception_table[]:数组每个成员表示code[]数组中的一个异常处理器。同时exception_table[]的每个成员包含如下4项:
- start_pc和end_pc:表明了异常处理器在code[]中的有效范围。start_pc的值必须是对当前code[]中某一指令操作码的有效索引,end_pc的值要么是对当前code[]中某一指令操作码的有效索引,要么等于code_length的值。start_pc的值必须比end_pc小。当程序计数器在范围[start_pc,end_p)内时,异常处理器就将生效。
- handler_pc:表示一个异常处理器的起点。handler_pc的值必须同时是对当前code[]和其中某一指令操作码的有效索引。
- catch_type:如果catch_type的值不为0,那么它必须是对常量池表的一个有效索引。常量池表在该索引处的成员必须是CONSTANT_Class_info结构,用以表示当前异常处理器需要捕捉的异常类型。只有当抛出的异常是指定的类或者其子类的实例时,才会调用异常处理器。
- attributes_count:表示Code属性中attributes[]数组中成员的个数。
- attribute[]:属性表中的每个值都必须是attribute_info结构体,Code属性可以关联任意多个属性。
接下来数56个字节,开始从max_stack开始分析(切勿从attribute_name_index开始分析,已经分析过了)。
maxstack 2
maxlocals 1
code_length 10
也许大家会有疑惑了,无参构造方法是没有参数的呀!怎么maxlocals的值是1呢?那不是说明有一个局部变量吗?如果您学习过pyhton就知道python的方法中第一个参数必须是self,表示当前对象。Java在方法内部我们经常使用
this.xxx
获取对象中的属性,方法。所以这个隐藏的局部变量就是this。
知道的code_length为10我们可以很简单的查看code具体的内容了,注意,这里说的内容不是Java代码,而是指令集对应的十六进制编号:
我们通过这10个字节的16进制是很难得到下面的指令集的,这时候我们就可以开始使用插件来帮助我们了。
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
IDEA为我们提供了名为jclasslib
的插件,通过它也提供非插件版供我们下载使用。https://github.com/ingokegel/jclasslib
我们点击具体的助记符还会自动打开对应的官方文档,帮助我们更清楚的了解助记符的含义。以上助记符对应的十六进制编号如下所示:
aload_0 0x2a
invokespecial 0xb7
iconst_1 0x4
putfield 0xb5
return 0xb1
注意:红色框部分表示的是额外的参数。
接下来分析exception_table_length,值为0x0000
,说明异常表成员数量为0。
LineNumberTable
紧接着是attributes_count,值为0x0002
,说明有两个附件属性。根据attribute_info结构,第一个属性的name_index为0x000A
,对应常量池中的LineNumberTable
。
LineNumberTable属性是可选的变长属性,位于Code结构的属性中。它被调试器用于确定源文件中由给定的行号所表示的内容,对应于Java虚拟机cod[]数组中的哪一部分。在Code属性的属性表中,LineNumberTable属性可以按照任意顺序出现。在Code属性attributes表中,可以有不止一个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];
}
LineNumberTable结构各项的说明如下:
- attribute_name_index:其值必须是对常量池的一个有效索引。常量池表在该索引处的成员必须是CONSTANT_Utf8_info结构,用以表示字符串"LineNumberTable"。
- attribute_length:表示当前属性的长度,不包括初始的6个字节(attribute_name_index和attribute_length)
- line_number_table_length:表示line_number_table[]数组的成员个数。
line_number_table[]:数组的每个成员都表明源文件中的行号会在code数组中的哪一条指令处发生变化。line_number_table的每个成员都具有如下两项:
- start_pc:其值必须是code[]数组的一个索引,code[]在该索引处的指令码,表示源文件中新的行的起点。start_pc的值必须小于当前LineNumberTable属性所在的Code属性的code_length的值。
- line_number:其值必须与源文件中对应的行号相匹配。
接着分析attribute_length,值为0x0000000A(十进制为10)
,说明属性的长度为10。后续line_number_table_length的值为0x0002
表示line_number_table的个数为2。对应插件生成的结果是一致的。至此,第一个属性分析完成。
LocalVariableTable
开始分析第二个属性。根据attribute_info结构,第一个属性的name_index为0x000B
,对应常量池中的LocalVariableTable
。
LocalVariableTable属性是可选变长属性,位于Code属性的属性表中,调试器在执行方法的过程中可以用它来确定某个局部变量的值。在Code属性的属性表中,多个LineVariableTable属性可以按照任意顺序出现。Code属性attribute表中的每个局部变量,最多只能有一个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];
}
LocalVariableTable结构各项的说明如下:
- attribute_name_index:其值必须是对常量池的一个有效索引。常量池表在该索引处的成员必须是CONSTANT_Utf8_info结构,用以表示字符串"LocalVariableTable"。
- attribute_length:表示当前属性的长度,不包括初始的6个字节(attribute_name_index和attribute_length)
- local_variable_table_length:表示local_variable_table[]数组中成员的数量。
local_variable_table:数组中的每一项都以偏移量的形式给出了code数组中的某个范围。当局部变量处在这个范围内的时候,它是有值的。此项还会给出局部变量再当前帧的局部变量表(local variable array)中的索引。local_variable_table[]的每个成员都有如下5个项:
- start_pc和length:当给定的局部变量处在code数组的[start_pc,start_pc + length)范围内,也就是处在由偏移量大于等于start_pc且小于start_pc + length的字节码所构成的范围内时,该局部变量必定具备某个值。start_pc的值必须是对当前Code属性的code[]的一个有效索弓|, code[]在这个索引处必须是一条指令的操作码。start_pc+length要么是当前Code属性的code[]数组的有效索引,且code[]在该索引处必须是一条指令的操作码,要么是刚超过code[]数组末尾的首个索引值。
- name_index:值必须是对常量池表的一个有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,用来表示一个有效的非限定名,以指代这个局部变量。
- descriptor_index:值必须是对常量池表的一个有效索引。常量池在该索引处的成员必须是CONSTANT_Utf8_info结构,此结构是个用来表示源程序中局部变量类型的字段描述符。
- index:值为此局部变量在当前栈帧的局部变量表中的索引。如果在index索引处的局部变量是long或double类型,则占用index和index+1两个位置
接下来的local_variable_table_length值为0x0001
表示local_variable_table的个数为1。
接着分析attribute_length,值为0x0000000C(十进制为10),说明属性的长度为10。后续local_variable_table_length的值为0x0001表示local_variable_table的个数为1。对应插件生成的结果是一致的。至此,第二个属性分析完成。
查看local_variable_table可以看出无参构造方法中的一个局部变量为this。
至此我们已经完整的分析完了第一个方法,后续方法也是一样的去分析。根据插件结构判断正确与否吧^_^
版权属于:带翅膀的猫
本文链接:https://www.chengpengper.cn/archives/57/
转载时须注明出处及本声明