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

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

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

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的版本号。

 u2 minor_version;//次版本号
 u2 major_version;//主版本号
复制代码

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

0015

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 它的结构体是

CONSTANT_Methodref_info {
    u1 tag;
    u2 class_index;
    u2 name_and_type_index;
}
复制代码
items描述
tagCONSTANT_Methodref_info结构的tag值为10
class_indexCONSTANT_Methodref_info结构的class_index必须是一个类类型,而不是接口类型
name_and_type_indexname_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,它的结构是

CONSTANT_Class_info {
    u1 tag;
    u2 index;
}
复制代码

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

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

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项,它的结构是

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个字节表示的结构是:

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, 对应的结构是

method_info {
    u2             access_flags;        // 1
    u2             name_index;          // 7
    u2             descriptor_index;    // 8
    u2             attributes_count;    // 1
    attribute_info attributes[attributes_count];
}
复制代码

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

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可以看到

剩下的字节码是

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这个结构,这个结构是

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,这个结构是

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表示的是

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 表示的是

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

再来分析info[57],是

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

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

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对应的是

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

最后边的字节码

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

00 01 表示attribute count为1

00 0f00 0000 0200 10 表示attribute_info

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个属性。

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值