月泉的博客

手把手带你分析Class字节码文件

字数统计: 2.2k阅读时长: 9 min
2018/04/19 Share

分析Class文件的源码如下:

1
2
3
4
5
6
7
8
9
package org.yuequan.klass;

public class Foo{
private int m;

public int inc(){
return m + 1;
}
}

笔者使用JDK1.8将源文件编译成class文件,为了更好的手动分析这个class文件,笔者将使用现有的Class分析工具去打开这个class文件。

打开后的Class文件:

Java虚拟机规范对Class文件结构做了严格的规定,其中的字节严格的按顺序紧密的排在一起,中间没有任何的分隔符和其它数据

分析之前先把Java虚拟机规范对Class文件结构的定义先列出来

根据Java虚拟机规范所描述的Class文件结构,我们大致的可以理解类型分别为u1、u2、u4、cp_info、field_info、method_info、attribute_info,其中u1、u2、u4分别为1、2、4无符号整型字节,_info后缀结尾的都是表结构类型
基本的介绍的差不多了,那就正式开始分析吧。


根据ClassFile文件结构来分析,前面4个字节是魔数,魔数为固定的0xCAFEBABE,魔数可以作为一个文件的特殊识别,很多类型的文件,都会在文件前面加几个字节填充魔数,利用魔数来识别文件类型,根据Java虚拟机规范也只有0xFAFEBABE魔数的文件才能被Java虚拟机所接受。


再紧跟着的是2个字节的次版本号


次版本号为0,次版本号从JDK1.2以后次版本号就没有再被使用而是一直使用着主版本号。


再紧跟着2个字节的主版本号


我的主版本号为0x34为52,正好是JDK1.8的版本号,为了方便观看,笔者将JDK的版本号整理了一个列表供参考


再接下来是2个字节的常量池数量


我的常量池数量是19个0x13 = 19,常量池元素索引是从1开始而不是0这点要注意,所以常量池可用数量为1 - count-1,第0项是为了某些指向常量池的索引值的数据在特殊情况下不需要引用任何一个常量池项目的时候就可以将索引置为0

接下来介绍ClassFile的cp_info也就是常量池,常量池中主要存储字面量和符号引用,字面量的意义比较接近于常量的概念,如字符串、final后的常量值等,这里的符号引用主要还是包括:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符,接下来再看下cp_info的结构定义,常量池中每一项都是一个表,到目前JDK8版本共有14个常量类型的结构,为了区分这些常量类型,常量数据的第一字节是tag分别对应着这14个常量类型


概念介绍到这里差不多了,那么开始正式的分析

第一个常量的tag是0x0A对应着10也就是Constant_Methodref_info

根据结构后面还有2个u2类型的class_indexname_and_type_index
class_index 是指常量池中索引为class_index的常量项,name_and_type_index同理,由于后面的常量项还没分析,无法得知index所指的具体值,所以先把值分析出来放在这里吧,不用担心,我在文章的该列出来的地方还会在列一遍,常量池第一项的数据

1
2
3
4
// index 1
tag = 10
class_index = 4
name_and_type_index = 15

接着我们来分析第二项

第二项tag为9对应着CONSTANT_Fieldref_info

1
2
3
4
// index 2
tag = 9
class_index = 3
name_and_type_index = 16

第三项

第三项tag为7对应着CONSTANT_Class_info

1
2
3
// index 3
tag = 7
name_index = 17

第四项

第四项tag为7对应着CONSTANT_Class_info

1
2
3
// index 4
tag = 7
name_index = 18

第五项

第五项的tag为1对应着CONSTANT_Utf8

长度为1

长度为1那bytes也就只有一个

1
2
3
4
// index  5
tag = 1
length = 1
bytes = ['m'] // bytes = [6D] 6D = 109 => UTF8 => m

第六项

嗯哼tag还是01

长度为1

1
2
3
4
// index 6
tag = 1
length = 1
bytes = ['I'] // bytes = ['49'] 49 => 73 => UTF8 => I

第七项

tag还是01

长度为6

1
2
3
4
5
6
7
8
9
// index 6
tag = 1
length = 6
// 23333 很长于是注释写这里
// [0x3C, 0x69, 0x6E, 0x69, 0x74, 0x3E]
// to UTF8
// ['<', 'i', 'n', 'i', 't', '>']
// bytes.toString() => "<init>"
bytes = ['<', 'i', 'n', 'i', 't', '>']

第八项

tag仍为01 ,😊2333333

长度为3

1
2
3
4
// index 8
tag = 1
length = 3
bytes = ['(', ')', 'V'] //不再重复转换步骤拉

第九项

tag还是01哦

长度是4

1
2
3
4
// index 9
tag = 1
length = 4
bytes = ['C', 'o', 'd', 'e']

第十项

tag还是01哦

长度是15

1
2
3
4
// index 10
tag = 1
length = 15
bytes = ['L', 'i', 'n', 'e', 'N', 'u', 'm', 'b', 'e', 'r', 'T', 'a', 'b', 'l', 'e']

第十一项

长度为3

1
2
3
4
// index 11
tag = 1
length = 3
bytes = ['i', 'n', 'c']

第十二项

tag任然是1
长度为3

1
2
3
4
// index 12
tag = 1
length = 3
bytes = ['(', ')', 'I']

第十三项

长度为10

1
2
3
4
// index 13
tag = 1
length = 10
bytes = ['S', 'o', 'u', 'r', 'c', 'e', 'F', 'i', 'l', 'e'] //SourceFile

第十四项

长度为8

1
2
3
4
// index 14
tag = 1
length = 8
bytes = ['F', 'o', 'o', '.', 'j', 'a', 'v', 'a'] // Foo.java

第十五项

tag为12,12对应着CONSTANT_NameAndType

1
2
3
4
// index 15
tag = 12
name_index = 7
descriptor_index = 8

第十六项

1
2
3
4
// index 16
tag = 12
name_index = 5
descriptor_index = 6

第十七项

长度为21

1
2
3
4
// index 17
tag = 1
length = 21
bytes = ['o', 'r', 'g', '/', 'y', 'u', 'e', 'q', 'u', 'a', 'n', '/', 'k', 'l', 'a', 's', 's', '/', 'F', 'o', 'o']

哇,最后一项,由此可见常量池占了我们字节码文件的大部分内容
第十八项

长度为16

1
2
3
4
// index 18
tag = 1
length = 16
bytes = ['j', 'a', 'v', 'a', '/', 'l', 'a', 'n', 'g', '/', 'O', 'b', 'j', 'e', 'c', 't']

至此,常量池已经分析完了,是时候整理一波了。


大致分析出来的内容用思维导图整理了一下,最终整合一波

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// index 1
// class_index = 4 name_and_type = 15
Methodref java/lang/Object.<init>

// index 2
// class_index = 3 name_and_type = 16
Fieldref org/yuequan/klass/Foo.m

// index 3
// name_index = 17
Class org/yuequan/klass/Foo

// index 4
// name_index = 18
Class java/lang/Object

// index 5
Utf8 m

// index 6
Utf8 I

// index 7
Utf8 <init>

// index 8
Utf8 ()V

// index 9
Utf8 Code

// index 10
Utf8 LineNumberTable

// index 11
Utf8 inc

// index 12
Utf8 ()I

// index 13
Utf8 SourceFile

// index 14
Utf8 Foo.java

// index 15
// name_index = 7 descriptor_index = 8
NameAndType <init>&()V

// index 16
// name_index = 5 descriptor_index = 6
NameAndType m&I

// index 17
Utf8 org/yuequan/klass/Foo

// index 18
Utf8 java/lang/Object

终极整理已完成~,常量池分析就先到这里,由于文章篇幅好像有点长了23333,还有些许知识点我放在下一篇《升级版》中去探讨


接下来,该分析啥,掏出ClassFile结构来看看

常量池结束后,紧跟着的是2个字节的访问标识符

我们来看看对应着啥?

一眼看上去感觉没有对的上的? 但使用了bitmask的思路所以对应的应该是

1
access_flag = ACC_PUBLIC, ACC_SUPER

在接着往下分析

访问标识符完了后,根据结构是2个字节的this_class 这个值也是指向的常量池的索引

对应着常量池第三个索引(从1开始数)也就是org/yuequan/klass/Foo


接下来是父类

java.lang.Object以外所有类都有一个默认父类,那便是java.lang.Object,可以理解为父要么为0要么就是指向常量池中存在的Class,而为0的根据目前来看应该只有java.lang.Object如果还有其它的欢迎在Blog的Github上提issue大家一起分享。

常量池索引为4的是java.lang.Object,所以该类的父类应是java.lang.Object



接下来是接口

此类没有实现接口数量为0


接下来是fields_count,它的数量为1

接着是field_info

先把结构掏出来看下

根据结构先是一个u2的访问标识符

0x0002对应着ACC_PRIVATE

接着是一个u2类型的name_index0x0005对应着常量池中cp_info[5]的m

接着是u2类型的descriptor_index0x0006对应着常量池中cp_info[6]的I(int的描述形式)

接着也是一个u2类型的attributes_count,但数量为0


接下来的方法的数量

数量表示有2个方法


接着是method_info,还是先把结构拿出来

先分析第一个access_flags

0x0001对应着ACC_PUBLIC

第二个是name_index,指向着常量池的一个索引

指向常量池第7个索引<init>
接着是descriptor_index

指向常量池第8个索引()V,由于文章篇幅太长了,剩下的attribute改天分解或者说不会再读分解,我感觉已经够了,如果大家想要我写完的话,那我就写完吧

原文作者:yuequan

原文链接:http://www.lunaspring.com/2018/04/19/class_analysis/

发表日期:April 19th 2018, 5:53:42 pm

更新日期:June 20th 2019, 4:07:04 pm

版权声明:© 月泉 - 邓亮泉 版权所有

CATALOG