Class的二进制文件分析—方法解析
1.代码
public class Man {
private String name = "edwardy";
private Integer age = 28;
public void outPrint(){
System.out.println("name : "+name+" age: "+age);
}
public static void main(String[] args) {
Man man = new Man();
man.outPrint();
}
}
2.二进制文件(只截取了当前小节需要的部分)
3.javap verbose的常量池图(只截取了当前小节需要的部分)
public class Man
minor version: 0
major version: 51
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Class #2 // Man
#2 = Utf8 Man
#3 = Class #4 // java/lang/Object
#4 = Utf8 java/lang/Object
#5 = Utf8 name
#6 = Utf8 Ljava/lang/String;
#7 = Utf8 age
#8 = Utf8 Ljava/lang/Integer;
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
#12 = Methodref #3.#13 // java/lang/Object."<init>":()V
#13 = NameAndType #9:#10 // "<init>":()V
#14 = String #15 // edwardy
#15 = Utf8 edwardy
#16 = Fieldref #1.#17 // Man.name:Ljava/lang/String;
#17 = NameAndType #5:#6 // name:Ljava/lang/String;
#18 = Methodref #19.#21 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#19 = Class #20 // java/lang/Integer
#20 = Utf8 java/lang/Integer
#21 = NameAndType #22:#23 // valueOf:(I)Ljava/lang/Integer;
#22 = Utf8 valueOf
#23 = Utf8 (I)Ljava/lang/Integer;
#24 = Fieldref #1.#25 // Man.age:Ljava/lang/Integer;
#25 = NameAndType #7:#8 // age:Ljava/lang/Integer;
#26 = Utf8 LineNumberTable
#27 = Utf8 LocalVariableTable
#28 = Utf8 this
#29 = Utf8 LMan;
#30 = Utf8 outPrint
#31 = Fieldref #32.#34 // java/lang/System.out:Ljava/io/PrintStream;
#32 = Class #33 // java/lang/System
#33 = Utf8 java/lang/System
#34 = NameAndType #35:#36 // out:Ljava/io/PrintStream;
#35 = Utf8 out
#36 = Utf8 Ljava/io/PrintStream;
#37 = Class #38 // java/lang/StringBuilder
#38 = Utf8 java/lang/StringBuilder
#39 = String #40 // name :
#40 = Utf8 name :
#41 = Methodref #37.#42 // java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
#42 = NameAndType #9:#43 // "<init>":(Ljava/lang/String;)V
#43 = Utf8 (Ljava/lang/String;)V
#44 = Methodref #37.#45 // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#45 = NameAndType #46:#47 // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
#46 = Utf8 append
#47 = Utf8 (Ljava/lang/String;)Ljava/lang/StringBuilder;
#48 = String #49 // age:
#49 = Utf8 age:
#50 = Methodref #37.#51 // java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
#51 = NameAndType #46:#52 // append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
#52 = Utf8 (Ljava/lang/Object;)Ljava/lang/StringBuilder;
#53 = Methodref #37.#54 // java/lang/StringBuilder.toString:()Ljava/lang/String;
#54 = NameAndType #55:#56 // toString:()Ljava/lang/String;
#55 = Utf8 toString
#56 = Utf8 ()Ljava/lang/String;
#57 = Methodref #58.#60 // java/io/PrintStream.println:(Ljava/lang/String;)V
#58 = Class #59 // java/io/PrintStream
#59 = Utf8 java/io/PrintStream
#60 = NameAndType #61:#43 // println:(Ljava/lang/String;)V
#61 = Utf8 println
#62 = Utf8 main
#63 = Utf8 ([Ljava/lang/String;)V
#64 = Methodref #1.#13 // Man."<init>":()V
#65 = Methodref #1.#66 // Man.outPrint:()V
#66 = NameAndType #30:#10 // outPrint:()V
#67 = Utf8 args
#68 = Utf8 [Ljava/lang/String;
#69 = Utf8 man
#70 = Utf8 SourceFile
#71 = Utf8 Man.java
{
public Man();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #14 // String edwardy
7: putfield #16 // Field name:Ljava/lang/String;
10: aload_0
11: bipush 28
13: invokestatic #18 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
16: putfield #24 // Field age:Ljava/lang/Integer;
19: return
LineNumberTable:
line 2: 0
line 3: 4
line 4: 10
line 2: 19
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 this LMan;
public void outPrint();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=4, locals=1, args_size=1
0: getstatic #31 // Field java/lang/System.out:Ljava/io/PrintStream;
3: new #37 // class java/lang/StringBuilder
6: dup
7: ldc #39 // String name :
9: invokespecial #41 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V
12: aload_0
13: getfield #16 // Field name:Ljava/lang/String;
16: invokevirtual #44 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: ldc #48 // String age:
21: invokevirtual #44 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: aload_0
25: getfield #24 // Field age:Ljava/lang/Integer;
28: invokevirtual #50 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder;
31: invokevirtual #53 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
34: invokevirtual #57 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
37: return
LineNumberTable:
line 7: 0
line 8: 37
LocalVariableTable:
Start Length Slot Name Signature
0 38 0 this LMan;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #1 // class Man
3: dup
4: invokespecial #64 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokevirtual #65 // Method outPrint:()V
12: return
LineNumberTable:
line 11: 0
line 12: 8
line 13: 12
LocalVariableTable:
Start Length Slot Name Signature
0 13 0 args [Ljava/lang/String;
8 5 1 man LMan;
}
4.方法解析
在字段解析后,剩余的便是方法字节码。
4.1. methods_count
methods_count是U2的两个字节的长度,顾名思义它的值代表的含义是有几个方法。具体到本案例中,可以看到字节码是0x0003,所以表示有三个方法。
4.2. methods_info集合
method_info的长度是不同的,下面首先会给出method info 的结构图
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
U2 | access_flag | 1 | 方法的访问标识 |
U2 | name_index | 1 | 名称索引 |
U2 | descriptor_index | 1 | 方法信息描述索引 |
U2 | attributes_count | 1 | 方法属性数量 |
U2 | attributes | attributes_count | 方法属性(九大属性,由于篇幅精力有限,后面只介绍第一个方法用的属性,其它属性只列出) |
需要注意的是,access_flag又对应了一个值表,方法属性又包含9种,下面将分别列出。
4.2.1 access_flag图表
标志名称 | 二进制值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 方法是否是public |
ACC_PRIVATE | 0x0002 | 方法是否是private |
ACC_PROTECTED | 0x0004 | 方法是否是protected |
ACC_STATIC | 0x0008 | 方法是否是static |
ACC_FINAL | 0x0010 | 方法是否是final |
ACC_SYNCHRONIZED | 0x0020 | 方法是否是synchrionzed |
ACC_BRIDGE | 0x0040 | 方法是否有编译器产生的桥接方法 |
ACC_VARARGS | 0x0080 | 是否接受不定参数 |
ACC_NATIVE | 0x0100 | 字段是否是native |
ACC_ABSTRACT | 0x0400 | 字段是否是abstract |
ACC_STRICTFP | 0x0800 | 字段是否是strictfp |
ACC_SYNTHETIC | 0x1000 | 字段是否为编译器自动产生 |
4.2.2 九大属性之一:code属性
code属性即java程序中方法体重的代码经过编译之后处理的字节码,注意接口和抽象方法是不存在code属性。code属性的结构图如下
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
U2 | attribute_name_index | 1 | 属性名索引地址 |
U4 | attribute_length | 1 | 整个code属性的长度 |
U2 | max_stack | 1 | 操作数栈深度最大值 |
U2 | max_locals | 1 | 局部变量表的空间 |
U4 | code_length | 1 | code的长度 |
U1 | code | code_length | |
U2 | exception_table_length | 1 | 异常长度 |
U2 | exception_table | exception_table_length | 异常信息 |
U2 | attributes_count | 1 | 属性数 |
U2 | attributes | attributes_count |
(1)attribute_length 指的整个是code属性的长度
(2)max_locals 这个大小不是所有局部变量占用的Slot总和,而是局部变量的声明周期结束会分配给其它局部变量
(3)code和code_length可以确定出编译后的字节码指令,code中存储的是一些列字节流指令。
需要注意的是,每个指令都是一个U1的单字节,一个U1的单字节长度是8位,共可以组合成256种指令,足够支撑JVM虚拟机的200多个指令。所以在解析字节码的时候,只要分割正确
,即每个字节码的第一个字节肯定是指令,至于该指令后面是否需要操作数,便需要根据第一个字节的指令查询指令集合判断后面是否需要操作数。
java的指令集: https://blog.csdn.net/shi1122/article/details/8053605
4.2.3 九大属性之二:LineNumberTable属性
用于描述java源码和字节码对应的关系,可以再编译的时候通过参数进行设置或关闭该属性,该属性的作用是在堆栈抛异常的时候可以记录代码的行号或者调试程序时候记录代码行号,其结构如下
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
U2 | attribute_name_index | 1 | 属性名索引地址 |
U4 | attribute_length | 1 | 整个LineNumberTable属性的长度 |
U2 | line_number_table_length | 1 | 长度 |
line_number_info | line_number—_table | 1 | line_number_info类型的数组 |
line_number_info类型的结构如下
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
U2 | start_pc | 1 | 字节码行号 |
U2 | line_number | 1 | java源码行号 |
4.2.4 九大属性之三:LocalVariableTable属性
用于描述栈帧中局部变量表中的变量和源码中的变量之间的关系(非运行是必须,可以编译的时候指定不生成),该结构在IDE使用参数占位符的时候或调试获取上下文参数有用,其结构如下
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
U2 | attribute_name_index | 1 | 属性名索引地址 |
U4 | attribute_length | 1 | 整个LocalVariableTable属性的长度 |
U2 | local_variable_table_length | 1 | 长度 |
local_variable_info | local_variable_table | 1 | local_variable_info类型的数组 |
而local_variable_info的结构如下
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
U2 | start_pc | 1 | 局部变量的声明周期开始的字节码便宜量 |
U2 | length | 1 | 局部变量的作用范围覆盖的长度 |
U2 | name_index | 1 | 指向常量池的索引,即局部变量名称 |
U2 | descriptor_index | 1 | 指向常量池的索引,即局部变量描述名称索引 |
U2 | index | 1 | 局部变量在栈帧局部变量表中的slot位置 |
4.2.5 九大属性之四:ConstantValue属性
https://blog.csdn.net/honjane/article/details/51835636
4.2.6 九大属性之五:Exception属性
https://blog.csdn.net/u014629433/article/details/51626686
4.2.7 九大属性之六:InnerClass属性
https://blog.csdn.net/silentbalanceyh/article/details/42640739
4.2.7 九大属性之七:SourceFile属性
https://blog.csdn.net/qq_36051652/article/details/80716165
4.2.9 九大属性之八:Deprecated和Synthetic属性
https://blog.csdn.net/csdn1017355712/article/details/79788456
4.3. 第一个方法
4.3.1 access_flag
根据前面描述的规则可以知道,在methods_count后面的的U2字节码是00 01,代表的是第一个方法的访问标识,通过查询表可以知道是public。
4.3.2 name_index
标识的方法名在常量池中的索引号,具体到当前的案例的字节码是00 09,通过查看对应的常量池可以推断出字符串是<init>,
即第一个方法名称是<init>
#7 = Utf8 age
#8 = Utf8 Ljava/lang/Integer;
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
4.3.3 descriptor_index
也是指向常量池的描述信息索引,代表的含义是函数的入参和返回类型,结合当前的案例得字节码是00 0a,通过查询常量池可以知道,其值是()V,
()表示该函数的入参是空,V表示该函数的返回参数是void类型。
#7 = Utf8 age
#8 = Utf8 Ljava/lang/Integer;
#9 = Utf8 <init>
#10 = Utf8 ()V
#11 = Utf8 Code
4.3.4 attributes_count
字节码是00 01,表示该属性只有1个
4.3.5 attributes info
(1)attribute_name_index:U2长度,所以第一个方法的前两个字节是00 0b,即指向的是常量池为11的字符串,通过分析可知道代表的含义是Code
(2)attribute_length:U4长度,00 00 00 4a,即整个code属性的长度是74个字节
(3)max_stack:U2长度,00 02,即操作数栈的最大值2。
(4)max_locals:U2长度,00 01,即局部变量表所需要的存储空间为1
(5)code_length:U4长度,00 00 00 14,即代码的长度20
(6)code:长度不定,长度是code_length,所以这里的长度是20,即2a b7 00 0c 2a 12 0e b5 00 10 2a 10 1c b8 00 12 b5 00 18 b1。
2a是code字节指令中的第一个字节码,所以2a表示的是一个指令,即查询2a代表的指令aload_0,将第一个引用类型本地变量推送至栈顶。
0x2a aload_0 将第一个引用类型本地变量推送至栈顶
0xB7 invokespecial 调用超类构造方法、实例初始化方法、私有方法(跟一个U2的字节数据,指向常量池中的方法编号)
0x12 ldc 将int、float或String型常量值从常量池中推送至栈顶,后面跟一个U1字节数据
0xB5 putfield 为指定的类的实例域赋值,后面跟一个U2字节
0x10 bipush 将一个byte型常量值推送至栈顶,后面跟一个U1字节数据
0xB8 invokestatic 调用静态方法,后面跟U2字节
0xB1 return 当前方法返回void
b7 00 0c,invokespecial #12 调用 java/lang/Object."<init>"?)V进行初始化
2a,同上,将第一个引用类型本地变量推送至栈顶
12 0e,将字符串edwardy推送到栈顶
b5 00 12,对Ljava/lang/string类型的字段name进行赋值,其值是上面推送到栈顶得edwardy
2a,同上
10 1c,将byte类型的28推送到栈顶(注意这里是28值,前面没有#,所以不是常量池的索引)
b8 00 12,调用静态方法java.lang.Integer.valueOf(int i),将其转换为Integer类型
b5 00 18,对Ljava/lang/Integer类型的字段age进行赋值,将上一步转换成Integer的28进行赋值
b1。当前函数执行完,返回void
总结第一个函数<init>,实际上就是对类和变量进行初始化
(7)exception_table_length:exception_table_length长度为00 00,所以长度0
(8)exception_table:exception_table_length长度为0,所以没有该属性
(9)attributes_count:00 02,属性个数为2,表示该方法引用了其它两个属性表,所以要按照其它属性表进行分析
(10)attributes:
10.1 属性名是00 1a,即执行26的常量池,翻译过来即LineNumberTable属性,所以需要参考LineNumberTable属性结构进行分析。整个LineNumberTable的属性的字节码长度是00 00 00 12,即18个字节,而Line_number_info的个数有00 04即4个这种类型的数组。
分别是00 00 00 02,即第一组info的字节码行号是0,java源码行号是2,以此类推可以知道其他几个info分别是(4,3)(10,4)(19,2)
10.2 属性名是00 1b,即LocalVariableTable属性,其整个属性的长度是00 00 00 0c,即长度是12。而局部变量属性的个数只有1个,即字节码是5个U2类型数据,00 00 00 14 00 1c 00 1d 00 00,所以start_pc是0、覆盖范围length是20、局部变量名称是28对应的是this、类型描述符是LMan,index为0。换言之即该变量的名称是this 描述符是LMan,其作用域是范围0+20,且在Slot为0的地方存储。
4.3.5 javap verbose的方法图
public Man();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: invokespecial #12 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #14 // String edwardy
7: putfield #16 // Field name:Ljava/lang/String;
10: aload_0
11: bipush 28
13: invokestatic #18 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
16: putfield #24 // Field age:Ljava/lang/Integer;
19: return
LineNumberTable:
line 2: 0
line 3: 4
line 4: 10
line 2: 19
LocalVariableTable:
Start Length Slot Name Signature
0 20 0 this LMan;
可以看到,上述按照字节码一个个解析后的各个属性,最后都体现在上图。
4.4. 第二个方法是outPut的解析,和上述类似,略。
4.5. 第三个方法是main函数的解析,和上述类似,略。
5.总结
可以看出方法的解析是最为麻烦的部分,涉及到9种属性。但只要严格按照字节码标准分割,一定可以准确解析。