对java的class文件的字节码的分析

最近有时会聊到java类的字节码,做了这么多年android开发,还真不了解它,于是想探索一下。

先写了一个简单的java类MyClass,代码如下:

1
2
3
4
5
6
7
public class MyClass {
private int i;

public void inc() {
i++;
}
}

然后分析它的class文件。找到生成的MyClass.class之后,用vim -b MyClass.class打开。打开后,是这样的,一脸懵逼。

不要紧,在vim中输入命令:%! xxd,展现这样的内容:

把字节码取出来,逐个分析。

cafe babe

每个Class文件的头四个字节称为魔数,它的唯一作用是用来确定该文件是否为一个能被虚拟机接受的Class文件

0000 0035

对应java class的版本号。

1
2
u2 minor_version;//次版本号
u2 major_version;//主版本号

次版本号在前,主版本号在后。minor_version是0,major_version是16进制的35。

0015

1
u2  constant_pool_count;//常量池容量计数

换算成10进制,常量的个数是21,这就代表常量池中有20项常量,索引值范围为1~20。(计数从1开始,其他计数从0开始,因为0有其他作用)

选中部分既是常量池。

分析常量池得出以下几项

如果用javap -p -v MyClass.class看,则是

接下来分析一下每一项的含义

0a 00 04 00 11

该常量类型是CONSTANT_Methodref_info
它的结构体是

1
2
3
4
5
CONSTANT_Methodref_info {
u1 tag;
u2 class_index;
u2 name_and_type_index;
}

items 描述
tag CONSTANT_Methodref_info结构的tag值为10
class_index CONSTANT_Methodref_info结构的class_index必须是一个类类型,而不是接口类型
name_and_type_index name_and_type_index的值必须是constant_pool表中的一个有效索引;这索引值上的对应的常量池条目(The constant_pool entry)也一定是CONSTANT_NameAndType_info结构;这个条目也是具有字段或方法作为成员的类或接口类型

它的class_index为4,对应的是第四项,它的name_and_type_index是17。我们先分析下第四项是什么。

07 00 14

这是第4项,07代表CONSTANT_class,它的结构是

1
2
3
4
CONSTANT_Class_info {
u1 tag;
u2 index;
}

index为14,代表的是索引值是20的那一项,我们再顺着分析下第20项。

第20项是CONSTANT_Utf8_info,tag是1,它的结构是

1
2
3
4
5
CONSTANT_Utf8_info {
u1 tag;
u2 length;
u1 bytes[length];
}

第20项的值是01 00 10 6a 61 76 61 2f 6c 61 6e 67 2f 4f 62 6a 65 63 74,其中tag是1,length是16,bytes是java/lang/Object(表示的字符串是 java/lang/Object)

回过头来我们分析下第17项的name_and_type_index

0c 00 07 00 08

这是第17项,它的结构是

1
2
3
4
5
CONSTANT_Name_AndType_info {
u1 tag; // 值是12
u2 index; // 指向该字段或方法名称常量项的索引
u2 index; // 指向该字段或方法描述符常量项的索引
}

在这里,两个index分别是7和8,于是我们又转向第7项和第8项,第7项和第8项可以看出来,他们的值分别是01 00 06 3c 69 6e 69 74 3e01 00 03 28 29 56。它们又都是CONSTANT_Utf8_info结构,字符串分别是<init>()V

方法名称<init>和方法描述符()V指的是什么呢?指的是一个实例初始方法,没有参数,返回类型也一定是 void。

回过头来我理一下它们的关联,用图来表示如下。


所以#1 Methodref表示的含义是java/lang/Object."<init>":()V

09 00 03 00 12

同样道理,分析第2项是。用图来表示如下。


所以#2 Fieldref表示的含义是MyClass.i:I,MyClass类的字段i,类型是整型。

至于别的常量,后边再分析。

常量池后边的00 21

MyClass是一个普通Java类,不是接口、枚举或者注解,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK 1.2之后的编译器进行编译,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而ACC_FINAL、ACC_INTERFACE、ACC_ABSTRACT、ACC_SYNTHETIC、ACC_ANNOTATION、CC_ENUM这6个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021。

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

00 21之后就是00 03 00 0400 03代表上边常量池中的第3项MyClass,这是类索引,00 04代表上边常量池中第4项java/lang/Object,这是父类索引。

紧接着是00 00表示接口索引的个数。因为MyClass没有实现接口,所以个数是0。

field

field count

紧接着是字段个数 00 01,表示有一个字段。那当然是i了,接下来看字段i在字节码中是如何表示的。

field 结构

这8个字节表示的结构是:

1
2
3
4
5
6
7
field_info {
u2 access_flags; // 2
u2 name_index; // 5
u2 descriptor_index; // 6
u2 attributes_count; // 0
attribute_info attributes[attributes_count]; // null
}

access_flags是2,表示ACC_PRIVATE,私有成员。name_index是5,是常量池中的第5项i,descriptor_index是6,是常量池中的第6项,I,代表整型。

留下来个问题,attributes_count为什么是0,attribute_info在这里是什么含义?

methods

接下来的字节码存储的是java的方法

methods count

00 02表示两个方法。为什么是两个方法,当然是和inc了。且看是如何存储在字节码中的。

第一个方法

第一个方法在字节码中的存储是

首先映入眼帘的是00 01 00 07 00 08 00 0100 01表示ACC_PUBLIC,说明是public方法,00 07是常量池第7项索引,表示方法名<init>00 08是常量池第8项索引,表示方法描述符()V00 01表示attributes_count, 对应的结构是

1
2
3
4
5
6
7
method_info {
u2 access_flags; // 1
u2 name_index; // 7
u2 descriptor_index; // 8
u2 attributes_count; // 1
attribute_info attributes[attributes_count];
}

那么剩下的就是属性信息了,对应的结构是

1
2
3
4
5
attribute_info {
u2 attribute_name_index; // 9
u4 attribute_length; // 00 00 00 2f,即47
u1 info[attribute_length];
}

00 09表示常量池中属性名称索引是第9项,常量池中第9项是Code,说明属性名称是Code。

u1 info[attribute_length]对应的字节码信息是:

这些字节码存储的信息是:

其中字节码指令就是2a b7 00 01 b1,分析这些指令。

  1. 读入2a,查表得0x2A对应的指令为aload_0,这个指令的含义是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。
  2. 读入b7,查表得0xB7对应的指令为invokespecial,这条指令的作用是以栈顶的reference类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private方法或者它的父类的方法。这个方法有一个u2类型的参数说明具体调用哪一个方法,它指向常量池中的一个CONSTANT_Methodref_info类型常量,即此方法的方法符号引用。
  3. 读入00 01,这是invokespecial的参数,查常量池得0x0001对应的常量为实例构造器<init>方法的符号引用。
  4. 读入b1,查表得0xB1对应的指令为return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束。

通过javap -p -v MyClass.class可以看到

剩下的字节码是

1
2
3
000a
0000 0006 0001 0000 0001 000b 0000 000c
0001 0000 0005 000c 000d 0000

剩下的字节码表示attributes,这些attributes是什么呢?attributes一共有2个属性,LineNumberTable和LocalVariableTable。

其中000a 0000 0006 0001 0000 0001代表LineNumberTable_attribute这个结构,这个结构是

1
2
3
4
5
6
7
8
LineNumberTable_attribute {
u2 attribute_name_index; // 00 0a,常量池中的第10项
u4 attribute_length; // 0000 0006,属性长度是6个字节长
u2 line_number_table_length; // 0001,只有1个line_number_table
{ u2 start_pc; // 0000,字节码行号
u2 line_number; // 0001,Java源码行号
} line_number_table[line_number_table_length];
}

LineNumberTable的作用在于,如果选择不生成LineNumberTable属性,对程序运行产生的最主要的影响就是当抛出异常时,堆栈中将不会显示出错的行号,并且在调试程序的时候,也无法按照源码行来设置断点。

其中000b 0000 000c 0001 0000 0005 000c 000d 0000表示LocalVariableTable_attribute,这个结构是

1
2
3
4
5
6
7
8
9
10
11
LocalVariableTable_attribute {
u2 attribute_name_index; // 000b常量池中第11项
u4 attribute_length; // 000 000c,属性长度是12个字节
u2 local_variable_table_length; // 0001,只有1个local_variable_table
{ u2 start_pc; // 0000
u2 length; // 0005
u2 name_index; // 000c,常量池中的第12项,this
u2 descriptor_index; // 000d
u2 index; // 0000
} local_variable_table[local_variable_table_length];
}

LocalVariableTable属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。可以看到里边有个local_variable_table。它的含义如下。

start_pc和length 代表了这个局部变量的生命周期开始的字节码偏移量及其作用范围覆盖的长度,两者结合起来就是这个局部变量在字节码之中的作用域范围。

name_index和descriptor_index
指向常量池中CONSTANT_Utf8_info型常量的索引,分别代表了局部变量的名称以及这个局部变量的描述符

index
这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型时(double和long),它占用的Slot为index和index+1两个

第二个方法

我们再来分析第2个方法,它的字节码一共是

简单分析入下:
0001 000e 0008 0001表示的是

1
2
3
4
5
6
7
method_info {
u2 access_flags; // 0001, ACC_PUBLIC, 表示public方法
u2 name_index; // 000e, 常量池索引,表示第14项,inc
u2 descriptor_index; // 0008,常量池索引,表示第8项,()V
u2 attributes_count; // 0001,1个attribute_info
attribute_info attributes[attributes_count];
}

0009 0000 0039 表示的是

1
2
3
4
5
attribute_info {
u2 attribute_name_index; // 0009,常量索引,表示Code
u4 attribute_length; // 0000 0039, 十进制是57,表示info的长度
u1 info[attribute_length]; // 长度为57,该表示该java方法的字节块的剩余部分
}

再来分析info[57],是

字节码 名字 含义
0003 max_stack 操作数栈(Operand Stacks)深度的最大值
0001 max_locals max_locals代表了局部变量表所需的存储空间
0000 000b code_length 代表字节指令码的长度
2a59 b400 0204 60b5 0002 b1 code code指令码
00 00 exception_table_length 异常表长度
exception_table
00 02 attributes_count 属性数量
00 0a00 0000 0a00 0200 0000 0500 0a00 06 LineNumberTable
00 0b00 0000 0c00 0100 0000 0b00 0c00 0d00 00 LocalVariableTable

其中00 0a00 0000 0a00 0200 0000 0500 0a00 06对应的是

1
2
3
4
5
6
7
8
9
10
LineNumberTable_attribute {
u2 attribute_name_index; // 00 0a
u4 attribute_length; // 00 00 00 0a
u2 line_number_table_length; // 00 02

// 这里line_number_table数组长是2,分别是[ {00 00, 00 05},{00 0a, 00 06}]
{ u2 start_pc;
u2 line_number;
} line_number_table[line_number_table_length];
}

上边内容已经分析过它的含义,这里不再分析。

其中00 0b00 0000 0c00 0100 0000 0b00 0c00 0d00 00对应的是

1
2
3
4
5
6
7
8
9
10
11
12
13
LocalVariableTable_attribute {
u2 attribute_name_index; // 00 0b,常量池索引
u4 attribute_length; // 00 00 00 0c

// 这里应该也是在描述this这个变量
u2 local_variable_table_length; // 00 01
{ u2 start_pc; // 00 00
u2 length; // 00 0b 字节码中的作用域范围
u2 name_index; // 00 0c
u2 descriptor_index; // 00 0d
u2 index; // 00 00
} local_variable_table[local_variable_table_length];
}

注意,注意,其中关键的code码指令2a59 b400 0204 60b5 0002 b1,我们没有分析。

code码 指令 描述
2a aload_0 第0个Slot中为reference类型的本地变量(通常是this)推送到操作数栈顶
59 dup 复制栈顶。相当于把操作数栈顶元素pop出来,再把它push进去2次
b4 00 02 getfield 获取对象的字段,将其值压入栈顶
04 iconst_1 int型常量1进栈
60 iadd 加法
b5 00 02 putfield 给对象的字段赋值
b1 return 返回此方法,并且返回值为void

最后边的字节码

00 0100 0f00 0000 0200 10表示什么含义呢?

00 01 表示attribute count为1

00 0f00 0000 0200 10 表示attribute_info

1
2
3
4
5
attribute_info {
u2 attribute_name_index; // 00 0f,常量池中的SourceFile
u4 attribute_length; // 00 00 00 02,属性长度是2
u1 info[attribute_length]; // 00 10,常量池第16项的MyClass.java
}

这是ClassFile最后的1个属性。

至此,大概分析了一下。
感谢https://blog.csdn.net/qq_31156277/article/details/80108277,整个分析过程是靠这篇博客的讲解来的。