首先准备材料:
1 Winhex工具下载:http://www.x-ways.net/winhex/
Winihex用来查看class文件
2 JclassLib工具下载:https://github.com/ingokegel/jclasslib
用于结构化展现字节码文件
3 java命令:javap -C 类文件名/java -verbose 类文件名
D:\zhunode_prac\demo\target\classes\com\zhunode\bytecode>javap -verbose MyTest01.class
Classfile /D:/zhunode_prac/demo/target/classes/com/zhunode/bytecode/MyTest01.class
Last modified 2019-6-8; size 481 bytes
MD5 checksum dc8c25bf7aa8c02cf95d1dc625ae5ee8
Compiled from "MyTest01.java"
public class com.zhunode.bytecode.MyTest01
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#20 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#21 // com/zhunode/bytecode/MyTest01.a:I
#3 = Class #22 // com/zhunode/bytecode/MyTest01
#4 = Class #23 // java/lang/Object
#5 = Utf8 a
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 LocalVariableTable
#12 = Utf8 this
#13 = Utf8 Lcom/zhunode/bytecode/MyTest01;
#14 = Utf8 getA
#15 = Utf8 ()I
#16 = Utf8 setA
#17 = Utf8 (I)V
#18 = Utf8 SourceFile
#19 = Utf8 MyTest01.java
#20 = NameAndType #7:#8 // "<init>":()V
#21 = NameAndType #5:#6 // a:I
#22 = Utf8 com/zhunode/bytecode/MyTest01
#23 = Utf8 java/lang/Object
{
public com.zhunode.bytecode.MyTest01();
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
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zhunode/bytecode/MyTest01;
public int getA();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zhunode/bytecode/MyTest01;
public void setA(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #2 // Field a:I
5: return
LineNumberTable:
line 9: 0
line 10: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/zhunode/bytecode/MyTest01;
0 6 1 a I
}
SourceFile: "MyTest01.java"
4 准备java文件:com.zhunode.MyTest01.java文件
package com.zhunode.bytecode;
public class MyTest01 {
private int a;
public int getA() {
return this.a;
}
public void setA(int a) {
this.a = a;
}
}
5 常量表数据结构描述符
关于常量表数据结构:可以查看《Java虚拟机规范》,本章是来自Java虚拟机8,来自第四章内容。当然,关于JVM虚拟机规范也可以查看官方文档。
第二部分:开始查看文件:
利用Winhex工具打开class文件,内容如图:
现在来逐步认识这份字节码结构:
首先该文件是一个16进制的文件
对于第一行的内容来说,CA FE BA BE 是一个固定值,CAFE,JAVA标志,BABE,baby。这个就是魔数,用于标识该文件是Java的二进制文件,JVM在加载class文件时,会通过魔数来检查该文件是否为java class文件。
跟着CA FE BA BE 后面的 00 00 00 34 表示小版本号和大版本号,如00 就与下图的minor version对应,00 34就与major version对应
00 00转为10进制=0
00 34转为10进制=3*16^1 + 4*16^0 = 52
在说常量池之前,需要注意的是,常量池不是用于存储常量的,它是一个数据资源库,用于为后面的方法表等部分的内容提供资源内容。
后面的00 18 表示常量池的个数:转为10进制的数值为:24。表示常量池中常量的个数为24,不过因为常量池中有一个常量为保留常量,即JVM内部定义的一个常量,所以除去这个常量,常量池中的常量的个数为24 - 1 = 23。下图就是通过javap -verbose得到的常量池
常量池的个数已经知道了,那么接下来的就是常量池的内容了。根据 材料准备 的 常量池数据结构信息可知,tag项用于表示
常量类型,一个用1bit表示。那么紧接着 00 18的 0A表示tag值为10 ,那么对应的常量类型为:Constant FieldRef info,这部分由三部分内容组成,1bit的index,2bit的用于指向Constant_Class_info类型的索引,2bit用于指向Constant_NameAndType类型的索引。那么就可以确定第一个常量池中的常量值内容了:0A 00 00 04 00 14该部分就用于描述常量池的第一个常量。
与第一个常量类似:接下来就直接画出所有的常量的二进制标志了。
序号 | 二进制表示 | 释义 | 对应的常量 |
---|---|---|---|
1 | 0A 00 04 00 14 | 00 004 表示第一个index,00 14表示第二个index | 0A对应常量类型:CONSTANT_Methodref_info,详情见常量信息。 |
2 | 09 00 03 00 15 | 00 03表示第一个index,指向声明的字段的类或者是Constant_Class_info的索引项,即索引项为3;00 15表示第二个index,指向字段的描述符的Constant_TypeAndName_info的索引项,索引项为21 | 09对应的常量类型:Constant FieldRef info |
3 | 07 00 16 | 00 16表示索引项,指的是指向常量限定名索引,索引项为22 | 07对应的常量类型:Constant_Class_Info |
4 | 07 00 17 | 00 17表示索引项,指的是指向常量限定名索引,索引项为23 | 07对应的常量类型:Constant_Class_Info |
5 | 01 00 01 61 | 00 01指的是UTF8编码的字符串占用的字符数,61 指长度为length的UTF-8编码的字符串,转为10进制数值为97 ,表示的数值为a。 | 01对应的常量类型:Constant_UTF8_Info |
6 | 01 00 01 49 | 00 01指的是UTF8编码的字符串占有的字符数,只有一个字符串,49 表示的字符数值为I。 | 01对应的常量类型:Constant_UTF8_Info |
7 | 01 00 06 3C 69 6E 69 74 3E | 00 06指的是UTF8编码的字符串占有的字符数为6,有6个字符,3C 69 6E 69 74 3E 表示的字符数值为<init>。 | 01对应的常量类型:Constant_UTF8_Info |
8 | 01 00 03 28 29 56 | 00 03指的是UTF8编码的字符串占有的字符数为3,有3个字符,28 29 56表示的字符数值为()V。 | 01对应的常量类型:Constant_UTF8_Info |
9 | 01 00 04 43 6F 64 65 | 00 04指的是UTF8编码的字符串占有的字符数为4,有4个字符,43 6F 64 65表示的字符数值为Code。 | 01对应的常量类型:Constant_UTF8_Info |
10 | 01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65 | 00 0F指的是UTF8编码的字符串占有的字符数为16,有16个字符,4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65表示的字符数值LineNumberTable。 | 01对应的常量类型:Constant_UTF8_Info |
11 | 01 00 12 4C 4F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65 | 00 12指的是UTF8编码的字符串占有的字符数为18,有18个字符,4C 4F 63 61 6C 56 61 72 69 61 62 6C 65 54 61 62 6C 65表示的字符数值LocalVariableTable | 01对应的常量类型:Constant_UTF8_Info |
12 | 01 00 04 74 68 69 73 | 00 04指的是UTF8编码的字符串占有的字符数为4,有4个字符,74 68 69 73表示的字符数值this | 01对应的常量类型:Constant_UTF8_Info |
13 | 01 00 1F 4C 63 6F 6D 2F 7A 68 75 6E 6F 64 65 2F 62 79 74 65 63 6F 64 65 2F 4D 79 54 65 73 74 30 31 3B | 00 1F指的是UTF8编码的字符串占有的字符数为32,有32个字符,4C 63 6F 6D 2F 7A 68 75 6E 6F 64 65 2F 62 79 74 65 63 6F 64 65 2F 4D 79 54 65 73 74 30 31 3B表示的字符数值Lcom/zhunode/bytecode/MyTest01; | 01对应的常量类型:Constant_UTF8_Info |
14 | 01 00 04 67 65 74 41 | 00 04指的是UTF8编码的字符串占有的字符数为4,有4个字符,表示的字符数值为getA | 01对应的常量类型:Constant_UTF8_Info |
15 | 01 00 03 28 29 49 | 00 03指的是UTF8编码的字符串占有的字符数为3,有3个字符,表示的字符数值为()I | 01对应的常量类型:Constant_UTF8_Info |
16 | 01 00 04 73 65 74 41 | 00 04指的是UTF8编码的字符串占有的字符数为4,有4个字符,表示的字符数值为setA | 01对应的常量类型:Constant_UTF8_Info |
17 | 01 00 04 28 49 29 56 | 00 04指的是UTF8编码的字符串占有的字符数为4,有4个字符,表示的字符数值为(I)A | 01对应的常量类型:Constant_UTF8_Info |
18 | 01 00 0A 53 6F 75 72 63 65 46 69 6C 65 | 00 0A指的是UTF8编码的字符串占有的字符数为10,有10个字符,表示的字符数值为SourceFile | 01对应的常量类型:Constant_UTF8_Info |
19 | 01 00 0D 4D 79 54 65 73 74 30 31 2E 6A 61 76 61 | 00 0D指的是UTF8编码的字符串占有的字符数为13,有13个字符,表示的字符数值为MyTest01.java | 01对应的常量类型:Constant_UTF8_Info |
20 | 0C 00 07 00 08 | NameAndTypeInfo常量类型由三部分组成,第二项指向全限定名常量项索引,即第7项索引;第三项指向字符串字面量的索引,即第8八项索引 | 0C对应的常量类型:Constant_NameAndTypeField ,具体信息查看结构类型 |
21 | 0C 00 05 00 06 | NameAndTypeInfo常量类型由三部分组成,第二项指向全限定名常量项索引,即第5项索引;第三项指向字符串字面量的索引,即第6项索引 | 0C对应的常量类型:Constant_NameAndTypeField ,具体信息查看结构类型 |
22 | 01 00 1D 63 6F 6D 2F 7A 68 75 6E 6f 6r 65 2F 62 79 74 65 63 6F 64 65 2F 4D 79 54 65 73 74 30 31 | 对应的UTF8类型, 63 6F 6d 2f 7a 68 75 6e 6f 6r 65 2f 62 79 74 65 63 6f 64 65 2f 4d 79 54 65 73 74 30 31表示的数值为com/zhunode/bytecode/MyTest01 | 01对应的常量类型:Constant_UTF8_INFO ,具体信息查看结构类型 |
23 | 01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74 | 对应的UTF8类型, 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74表示的数值为java/lang/Object | 01对应的常量类型:Constant_UTF8_INFO ,具体信息查看结构类型 |
到此为止,常量表的字节码就分析完成了,现在就开始看方法表的字节码。
字节码Access_Flag访问标志
对常量池有初步了解之后,我们可以来看看访问标志信息了,如下:
访问标志信息包括该Class文件是类还是接口,是否被定义成public,是否是abastract,如果是类,是否被声明为final。而我们定义的源代码就知道文件是类而且是public的:
类文件的访问标志对应的字节码信息如下:
对上面的类访问标志有了初步认识就继续往下分析,常量池之后的两个字节表示访问标志,则数两个字节。
0x0021
对应上面的类访问修饰符可知,并没有找到对应的修饰符,这是由于Java的修饰符是可以互相组合的,这里并没有穷举每一种组合,而是为每一个基本的访问修饰符定义了一个值,那如果一个类即是public,又是final时候,此时就会取这两个修饰符的并集:
0x0011 | 0x0010 = 0x0011
// 所以在二进制字节码中就会记录“0x0011”,同样的,回到之前的"0x0021",可以发现是
0x0001 | 0x0020 = 0x0021
所以0x0021:是0x0020和0x0001的并集,表示ACC_PUBLIC与ACC_SUPER。表示该类是public的并能调用父类的。所以在javap -verbose中能看到:
紧接着访问标志之后两个字节则是当前类的类名,如下:
所以往后数两个字节:
注意:这是一个索引,指向常量池,所以从常量池中进行查找。
紧接着再往后两个字节则是父类类名:
同样也在常量池中去找寻:
紧接着父类名称之后,则是接口信息了:
进一步展开如下:
也就是先数两个字节来看一下接口的数量:
表示当前类没有实现接口,既然接口数为0,则接口名则不会出现了,如下:
字段表
好,往下则是字段表信息,如下:
字段表用于描述类和接口中声明的变量。这里包含了类级别变量以及实例变量,但是不包括方法内部声明的局部变量。
接下来需要了解一下“字段表集合”,这是一个比较复杂的概念,虽说展开字段的信息是这样:
但是变量表是有自己结构的,就像常量池:
字段表的结构如下:
其中access_flags是访问标志符,占两个字符;name_index代表字段的名称索引,占两个字符;descriptor_index代表描述符的索引,占2个字节;这三个信息就可以完整的描述一个字段的信息;attributes_count:属性个数,可有可无;
用结构体形式描述为:
field_info{
u2 access_flags; // 0002
u2 name_index; // 0005
u2 descriptor_index; // 0006
u2 attributes_count; // 0000
attribute_info attributes[attributes_count];
}
好,回到二进制文件中来分析一下字段信息,先数2个字节来看一下字段的个数:
表示类中有一个字段,接下来得要根据字段表来看一下该字段的具体信息,首先数2个字节表示修饰符信息,如下:
所以往后数两个字节呗:
那它代表什么修饰符呢,上这张表去查一下:
呃~~什么鬼,木有是0x0002的修饰符嘛,其实它是ACC_PRIVATE,只是未列在此表中,对应源代码确实是嘛:
接下来两个字节表示字段名称索引,如下:
继续数数:
name_index,索引名,去常量池找:
再往后两个字节则是描述符的索引:
数两个字节:
继续在常量池上找
接下来两个字节则是属性数量,如下:
说明该字段木有属性信息,所以之后的attributes也不可能出现在二进制文件当中了,至此,整个字段信息就分析完了!
方法表结构
这一小节来分析方法表的内容
接着来看一下方法表相关的信息:
所以往下找两个字节:
0x0003表明有三个方法,这是因为有一个为默认构造方法,可以通过Bytecode viewer作为辅助查看:
接着往下则是方法表相关的信息:
先来看一下表结构:
也就是说每个方法所对应的方法表结构如上,所以接下来看一下第一个方法的信息,往下数2个字节则是access_flags,如下:
这是就需要查看对应的方法访问修饰符了:
表示是public的方法,接下来四个字节为方法名字和索引和描述符的索引,一起看:
其对应的常量池信息如下:
也就是第一个方法是构造方法,接着往下则是方法的属性相关的东东,注意:这个属性跟方法的变量是两码事,如下:
所以往下数两个字节,看一下方法属性的个数:
说明有一个方法属性,而往后两个字节对是属性名称索引,如下:
对应常量池:
Code属性,对于每一个方法都有一个Code属性,如下:
{
public com.zhunode.jvm.MyTest01();
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 a:I
9: return
LineNumberTable:
line 8: 0
line 10: 4
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/zhunode/jvm/MyTest01;
public int getA();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field a:I
4: ireturn
LineNumberTable:
line 13: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/zhunode/jvm/MyTest01;
public void setA(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: iload_1
2: putfield #2 // Field a:I
5: return
LineNumberTable:
line 17: 0
line 18: 5
LocalVariableTable:
Start Length Slot Name Signature
0 6 0 this Lcom/zhunode/jvm/MyTest01;
0 6 1 a I
}
SourceFile: "MyTest01.java"
其实就是描述了整个方法的Code信息,Code attribute的作用是保存该方法的结构,如所对应的字节码:
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[exception_table_length];
u2 attribute_count;
attribute_info attributes[attribute_count];
}
其中attribute_name_index则是属性名的索引,也就是Code,接下来还有一些信息下面来解释一下:
- attribute_length表示attribute所包含的字节数,不包含attribute_name_index和attribute_length字段。
说的是什么意思呢?比如attribute_length=50个字节,则是指往后的50个字节则为整个Code属性的信息,所以往后先数4个字节咱们来看一看长度:
0x0038转为10进制后的值为56,即属性的长度是56,所以往后56个字节为具体code属性信息,如下:
那咱们来jclasslib中来确认一下是否也是显示的56个字节,如下:
- max_stack表示这个方法运行的任何时刻所能达到的操作数栈的最大深度。
也就是在上面56个字节从头数2个字节,则是这个操作数栈的最大深度,咱们来看一下:
也就是最大的深度为2。如javap -verbose所示:
-
max_locals表示方法执行期间创建的局部变量的数目,包含用来表示传入的参数的局部变量。
再继续读2个字节:
也就是局部变量的数目为1,如javap -verbose所示:
-
code_length表示该方法所包含的字节码的字节数以及具体的指令码。具体指令码既是该方法被调用时,虚拟机所执行的字节码。
也就是占10个字节,所以往后数10个字节:
其实这10个字节是对应在jclasslib中的代码助记符的信息,如下:
所以问题就来了,aload_0不是助记符信息么,怎么就能够跟字节码文件中的字节对应上呢?所谓助记符其实也就是帮忙我们去记忆的符合,在底层其实也是对应的一个个十六进制的数字的,其它aload_0对应的就是2A这个十六进制数字,凭什么这么说?因为有jclasslib这么好的工具能帮我们对应上,将鼠标放在助记符上发现是一个可以点的链接,点一下“aload_0”发现居然链到了oracle的官网上的说明上去了,如下:
所以第一个字节已经分析完了,确实是跟助记符对应上了,接着来分析第二个字节:
而在jsclasslib中对应的第二个助记符是“invokespecial”,点击链到官网看一下:
而它的作用可以理解成就是调用父类的方法,很明显对于咱们自定义的子类肯定会去调用父类的构造方法,而这个助记符是有参数的:
其实也就是往后的两个字节就是该助记符所对应的参数,如下:
对应常量池为:
也就是构造方法嘛,如jsclasslib所示:
接下来继续往下走一个字节:
对应的code结构中的aload_0,如下:
接下来再往后看一个字节:
对应的就是Code结构中的下一个属性:
为啥如此任性呢?因为点击查看以下说明就晓了:
为啥要push一个1呢?实际就是给咋们定义的成员变量a赋值,如下:
可见,该变量的赋值是再默认构造函数中进行的,而不是直接进行赋值的,这也就是分析字节码文件的好处,可以更加真实的发现底层细节。
接下来再数一个字节:
它对应的方法中Code结构的注记符是:putfield。
点击看一下官网说明:
接下来的注记符是带有参数的,所以再往后数两个字节:
对应的常量池:
也就说给MyTest01的成员变量a赋值为1。
接下来再数一个字节:
对应的注记符如下:
点击链接:
至此整个构造方法就执行完成了。发现通过分析字节码也能获得不少新知识嘛,仅通过这个构造函数的执行流程就能知道了对于我们定义的成员变量原来是再构造方法中进行赋值而非直接赋值的,还是挺有价值的。
好,方法的code分析完成之后,则就得往下进行分析了,先来查看一下结构类型:
也就是再数两个字节,看一下:
说明该方法木有异常信息,所以接下来的异常表就不会显示再字节码文件当中了:
其中关于异常还需要解释一下:
- exception_table,这里存放的是处理异常的信息。
- 每个exception_table表项有start_pc、end_pc、handler_pc、catch_type组成。
- start_pc和end_pc表示再code数组中的start_pc到end_pc处(包含start_pc,不包含end_pc)的指令抛出异常会由这个表项来处理。
- handler_pc表示处理异常的代码的开始处。catch_type表示会处理异常类型,它指向常量池的异常类。当catch_type为0时,表示处理所有的异常。
接下来就到属性相关的东东了,如下:
所以往下数2个字节:
说明该方法有两个属性,往下数两个字节则是第一个属性的名字索引,如下:
也就是对应第10的常量池,为:
该属性用来表示code数组中的字节码和Java代码行数之间的关系。这个属性可以用来在调试的时候定位代码执行的行数。而该属性的结构为:
其中attribute_name_index就是常量索引10,接下来4个字节则时属性的长度attribute_length,如下:
也就是属性的长度为10,也是就接下来10个字节则为LineNumberTable的属性信息,如下:
看一下jclasslib:
下面具体来分析一下这10个字节,根据结构体来看:
先2个字节表示属性表有几对映射,如下:
说明有两对映射,然后再回到结构体中,每对映射的内容为:
每对占4个字节,先看第一对映射:
也就是start_pc=0;line_number=8,对应于jclasslib:
由于咱们源代码木有构造方法,所以字节码对应源代码就在第8行,如下:
接下来看第二对映射:
也就是start_pc=4,line_number=8,对应于jclasslib:
因为成员变量的赋值是在构造方法中完成的,所以对应第10行代码:
好,方法的第一个属性已经完了,接下来以同样的顺序来查看方法的第二个属性信息了,走2个字节来看属性名称索引,如下:
对应第11个常量池索引,如下:
它的结构跟LineNumberTable差不多的,往后数4个字节则是局部变量表所占的长度:
长度为12,如jclasslib所示:
然后往后数12个字节则是局部变量的具体信息,首先两个字节则为局部变量的个数:如下:
额,构造方法哪来的局部变量呢?好奇快,先不管,先来把其他字节分析完成,再往后四个字节表示start_pc和length,如下:
如jclasslib所示:
接下来则为局部变量的索引为0,也就是第一个局部变量:
再往后两个字节则是局部变量对应常量池的索引,如下:
再接下来两个字节则是对局部变量的一个描述常量索引,如下:
所以对应jclasslib中可以看到:
那思考一下为啥构造方法中会有一个this的局部变量呢?我们知道在所有方法中我们都能使用this关键字来访问当前的对象,而从字节层面来讲其实this是作为方法的第一个参数传进来的,也就是说对于java的一个实例方法而言,最少会有一个this的局部变量存在。
还剩最后两个字节则为stackmaptable信息,JDK1.6加入的,主要做校验检查的,因为0嘛,所以后面肯定木有相关的信息了,这里就直接忽略,如下:
这里就已经将编译器生成的默认构造方法的字节相关的分析完成了接下来继续分析自定义的方法啦,按照顺序来讲的话,应该是分析getA();
还是按照方法的结构来分析,先回忆一下方法的结构:
method_info{
u2 access_flag;
u2 name_index;
u2 descriptor_index;
u2 attribute_count;
attribute_info attributes[attribute_count];
}
前两个字节表示访问修饰符,所以跟着上次分析的位置数两个字节:
根据方法表修饰符可知,0x0001的修饰符是ACC_PUBLIC,
接着4个字节分别表示方法名称的索引记忆方法描述符的索引,如下:
所以数4个字节:
对应常量池14、15,如下:
确实如咱们的猜想,第二个方法就是getA(),好,继续往下分析:
表示方法的属性个数,所以往下数两个字节:
所以说明只有一个属性,接着再数2个字节则表示属性的名称索引,如下:
对应常量池:
如之前所述:每个方法都有一个对应的Code属性, 所以接下来就得来看一下Code属性结构体了,如下:
接着就是attribute_length了,往下数4个字节
说明整个Code长度为47个字节,对比一下jclasslib所示:
继续往下:往下两个字节为:
所以往下4个字节为:
- max_statck表示这个方法运行的任何时刻都能达到的操作数栈的最大深度为1;
- max_locals表示方法执行期间创建的局部变量的个数为1,而getA()方法压根就没有定义局部变量,根据之前分析的默认构造方法可以得知此局部变量为编译器生成的this变量。
继续往下:
长度为5,表示往后5个字节则为对应在jclasslib中代码注记符的信息,也就是方法的执行体,如下:
第一个字节为:
刚好对应aload_0,如下:
接着第二个字节
对应注记符为:
看一下官网对它的解释:
而且它是有参数的,所以往后两个字节:
对应常量为:
所以如jclasslib所看到的:
接下来看最后一个Code的字节,如下:
对应的注记符为:
查看官网说明:
好,接下来则是异常表的信息,如下:
往后数两个字节:
因为该方法木有异常,所以为0,那异常信息就可以忽略了,如下:
接下来则到属性相关的东东了,如下:
往后数两个字节:
说明有两个属性,往下数两个字节则是第一个属性的名字索引,如下:
查看常量池:
表示的是行号表,所以此时就要看一下行号表的结构体了:
接下来4个字节则是属性的长度attribute_length,如下:
所以接下来的6个字符则为该行号表的信息,如下:
看一下jclasslib:
下面具体来分析一下这10个字节,根据结构体来看:
先2个字节表示属性表有几对映射,如下:
说明有一对映射,然后再回到结构体中,每对映射的内容为:
每对映射占4个字节,所以:
也就是start_pc=0;line_number=7,对应于jclasslib:
对应的源代码为:
好,方法的第一个属性已经完了,接下来以同样的顺序来查看方法的第二个属性信息了,走2个字节来看属性名称索引,如下:
对应的常量表:
它的结构跟LineNumberTable差不多的,往后数四个字节则是局部变量表所占的长度:
长度为12,如jclasslib所示:
然后往后数12个字节则是局部变量的具体信息,如下:
首先两个字节则为局部变量的个数,如下:
说明有一个局部变量,再往后四个字节表示start_pc和length,如下:
如jclasslib所示:
同时,局部变量的索引为0,也就是第一个局部变量,如下:
再往后两个个字节则是局部变量对应常量池的索引,如下:
再接下来两个字节则是对该局部变量的一个描述常量索引,如下:
即this的索引为第13行
所以对应jclasslib中可以看到:
从这个分析又能证明,对于实例方法,都会有一个this局部变量存在的。
还剩最后两个字节则为stackmaptable信息,JDK1.6加入的,主要做校验检查的,因为0嘛所以后面肯定木有相关的信息了,这里就直接忽略,如下:
至此getA()方法就已经完全分析完了,接着就是第二个方法setA()了:
所以还得来依据方法表来进行分析:
前两个字节表示访问修饰符:
根据方法表修饰符可知,0x0001对应地修饰符为:ACC_PUBLIC。
接着4个字节分别表示方法名称地索引以及方法描述符地索引,如下:
对应常量池地16,17,如下:
正如我们所看到的,继续往下分析:
表示方法的属性个数,所以往下数两个字节:
所以说明只有一个属性,接着再数2个字节则表示属性的名称索引,如下:
对应常量池:
是不是对它异常的亲切了,每个方法必然会有这个属性,所以接下来就得来看一下Code属性结构体了,如下:
接着则是attribute_length了,往下数四个字节:
说明整个Code的长度为62个字节,对比一下jclasslib所示:
继续往下:往下两个字节为:
所以往后数4个字节:
max_stack表示这个方法运行的任何时刻所能达到的操作数栈的最大深度为2;max_locals表示方法执行期间创建的局部变量的数目为2,为啥这次变为2个局部变量了呢?因为第一个是隐式的this,第二个则为方法的int参数。
继续往下:
表示该方法所包含的字节码的字节数以及具体的指令码,所以往后数4个字节:
长度为6,所以往后再数6个字节则为对应在jclasslib中的代码助记符的信息,也就是方法的执行体,如下:
第一个字节为:
刚好对应aload_0,如下:
接着第二个字节:
对应助记符为:
看一下官网对它的解释:
第三个字节:
对应的注记符为:
看一下官网对他的解释:
而且它是有参数的,所以往后数两个字节:
对应常量池为:
所以如jclasslib所看到的:
接下来最后一个Code的字节,如下:
对应助记符为:
至此setA()方法的执行体就已经分析完了。
好,接下来则是异常表的信息,如下:
往后数2个字节:
因为该方法木有异常,所以为0,那异常信息就可以忽略了,如下:
接下来则到属性相关的东东了,如下:
往后数两个字节:
说明该方法有两个属性,往下数两个字节则是第一个属性的名字索引,如下:
也就是对应常量池10,为:
该属性用来表示code数组中的字节码和Java代码行数之间的关系。这个属性可以用来在调试的时候定位代码执行的行数。而该属性的结构为:
其中attribute_name_index就是常量索引10,接下来数4个字节则是属性的长度attribute_length,如下:
也就是属性的长度为10,也就是接下来10个字节则为LineNumberTable的属性信息,如下:
看一下jclasslib:
下面具体来分析一下这10个字节,根据结构体来看:
先2个字节表示属性表有几对映射,如下:
说明有两对映射,然后再回到结构体中,每对映射的内容为:
每对占4个字节,先看第一对映射:
也就是start_pc=0;line_number=17,对应于jclasslib:
对应源代码:
接下来看第二对映射:
也就是start_pc=5;line_number=18,对应于jclasslib:
对应源代码:
好,方法的第一个属性已经完了,接下来以同样的顺序来查看方法的第二个属性信息了,走2个字节来看属性名称索引,如下:
对应第11个常量池索引,如下:
它的结构跟LineNumberTable差不多的,往后数四个字节则是局部变量表所占的长度:
长度为22,如jclasslib所示:
然后往后数12个字节则是局部变量的具体信息,首先两个字节则为局部变量的个数,如下:
先分析第一个局部变量,往后四个字节表示start_pc和length,如下:
如jclasslib所示:
接下则为局部变量的索引为0,也就是第一个局部变量,如下
再往后两个字节则是局部变量对应常量池的索引,如下:
再接下来两个字节则是对该局部变量的一个描述常量索引,如下:
所以对应jclasslib中可以看到:
还剩最后两个字节则为stackmaptable信息,JDK1.6加入的,主要做校验检查的,因为0嘛所以后面肯定木有相关的信息了,这里就直接忽略,如下:
再来分析第二个局部变量,往后四个字节表示start_pc和length,如下:
接下来则为局部变量的索引为1,也就是第二个局部变量:
再接下来两个字节则是对该局部变量的一个描述常量索引,如下:
如jclasslib:
还剩最后两个字节则为stackmaptable信息,JDK1.6加入的,主要做校验检查的,这里就直接忽略,如下:
至此!!所有类中的方法相关的字节码就全部分析完了,确实够麻烦,最后则是类的属性信息了如下:
往后两个字节则表明字节码属性的长度:
有一个文件属性,往后两个字节则为属性名称的常量池索引,如下:
对应常量池:
再往后四个字节则为属性所占字节的长度:
说明attribute_length占2个字节,也就是最后剩的两个字节,如下:
对应常量:
如jclasslib所示:
至此,MyTest01字节码文件就分析完毕,好繁琐呀,好无聊呀。。。
文章参考:https://www.cnblogs.com/webor2006/
书本参考:《Java虚拟机规范 Java SE 8版.pdf》