Java字节码文件解析
1.怎么查看字节码文件
- javap -v class文件名
- 通过jclasslib查看,idea里也有jclasslib插件
上面两种方式查看的其实都不是最原始的二进制字节码,是经过翻译整理过的。
- 将class文件以十六进制格式查看,notepad++就可以
对十六进制打开的字节码文件的疑惑:
1.为什么是两个十六进制数一对一对的书写的?
在十六进制表示中,每个字节用两个十六进制数字表示。例如,ca fe ba be
代表四个字节的数据。
为什么能用两个16进制数字表示一个字节?
一个字节(Byte)由 8 个二进制位(bit)组成,每个二进制位可以是 0 或 1。因此,一个字节可以表示的范围是:
二进制:00000000 到 11111111
十进制:0 到 255
十六进制:00 到 FF
所以,ca fe ba be等都是一个个字节的数据。
2.知道了上面后,可以看出一行是16个字节的数据,最左边一列的Address是内存地址,第一行首地址是00000000,第二行的首地址怎么跳到了00000010了?
00000010是十六进制数字,对应的十进制是16!用10进制来描述前两行的内存地址,就是第一行是0~15,第二行的起始地址就是16。
题外话:为什么内存地址基本上都用16进制来表示?
这里贴一下gpt的回答:
明白了上面两个疑惑,基本上就能阅读十六进制打开的字节码文件了。例如前四个字节是class文件的魔术:ca fe ba be,紧接着是副版本号和主版本号,例如上面的主版本号是003d,转换成十进制为61,主版本号61对应着JDK17的版本。
2.class文件里的数据类型
- 无符号数: 以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数
- 表:表是由多个无符号数或其他表构成的复合数据结构。所有的表都以“_info”结尾。 由于表没有固定长度,所以通常会在其前面加上个数说明。
在字节码中,所有的数据要么是一个无符号数字,要么是个表。
3.class文件结构
字节码中按照顺序依次存储的下面表格中的内容:
类型 | 名称 | 说明 | 长度 | 数量 |
---|---|---|---|---|
u4 | magic | 魔数,识别Class文件格式 | 4个字节 | 1 |
u2 | minor_version | 副版本号(小版本) | 2个字节 | 1 |
u2 | major_version | 主版本号(大版本) | 2个字节 | 1 |
u2 | constant_pool_count | 常量池计数器 | 2个字节 | 1 |
cp_info | constant_pool | 常量池表 | n个字节 | constant_pool_count-1(为什么是减1后面会说) |
u2 | access_flags | 访问标识 | 2个字节 | 1 |
u2 | this_class | 类索引 | 2个字节 | 1 |
u2 | super_class | 父类索引 | 2个字节 | 1 |
u2 | interfaces_count | 接口计数器 | 2个字节 | 1 |
u2 | interfaces | 接口索引集合 | 2个字节 | interfaces_count |
u2 | fields_count | 字段计数器 | 2个字节 | 1 |
field_info | fields | 字段表 | n个字节 | fields_count |
u2 | methods_count | 方法计数器 | 2个字节 | 1 |
method_info | methods | 方法表 | n个字节 | methods_count |
u2 | attributes_count | 属性计数器 | 2个字节 | 1 |
attribute_info | attributes | 属性表 | n个字节 | attributes_count |
4.class文件结构中的17项
class文件由哪些部分组成?
魔数、副主版本、常量池、访问标识、类索引、父类索引等
4.1魔术
class文件格式的特殊标志,事实上,每个文件格式都有自己的魔术,你以16进制格式打开几张png图片,你会发现,前一段内容都是一样的,这就可以看成是png文件的魔术。
4.2副主版本
编译器的版本,可以查出JDK的版本。
4.3常量池
常量池是class文件中最重要的部分。
要强调的是,该处是字节码文件中的常量池,本质上还是磁盘上的.class文件代码,和运行时的内存还没关系。字节码中的常量池部分会在jvm运行时加载到方法区的运行时常量池中,运行时常量池是方法区的一部分。并且从jdk1.7开始,字符串常量从方法区的运行时常量池中挪到了堆中,叫字符串常量池了。
常量池中存储着编译时期生成的各种字面量和符号引用,所以常量池的主要作用是class文件中字段和方法的解析,在解析字段和方法时,需要知道字段名、方法名、方法的形参、方法的返回类型等,这些信息都存储在常量池当中。
字节码中没有分隔符,所以需要指定常量池中有多少项也就是有多少个常量,所以字节码中紧跟着主版本的是常量池计数器,来指定常量池中有多少项,占两个字节。需要说明的是,如果常量池计数器大小为22,那么常量池中的常量个数为22-1=21,常量索引依次为1~21(从1开始,而不是0)。
通常代码中的计数都是从0开始例如数组,这里为什么是从1开始的?因为它把第0项空出来了,这是为了满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目”的含义。
常量池中主要存储着两大类:字面量(Literal)和符号引用(Symbolic Reference),包含了class文件结构及其子结构中引用的所有字符串常量、类或接口名、字段名和其它常量(比如final修饰的常量)。
对字面量和符号引用的解释
字面量:文本字符串和声明为final的常量
例如:
public class Test {final int A = 11;String s = "aaaaaaaaaaaaaaaaaa";public static void main(String[] args) {}
}
//"aaaaaaaaaaaaaaaaaa"就是文本字符串,A就是一个常量值为11
//在编译后的字节码中的常量池中能直接找到aaaaaaaaaaaaaaaaaa和11
符号引用:包括类和接口的全限定名、字段的名称和描述符、方法的名称和描述符。
全限定名:例如org/example/demo3/demos/demo/Example就是全限定名,就是把包名(org.example.demo3.demos.demo.Example)中的.换成了/
描述符:描述符是用来描述字段的数据类型、方法的参数列表、方法的返回类型。
描述符的规则:
标志符 | 含义 |
---|---|
B | 基本数据类型byte |
C | 基本数据类型char |
D | 基本数据类型double |
F | 基本数据类型float |
I | 基本数据类型int |
J | 基本数据类型long |
S | 基本数据类型short |
Z | 基本数据类型boolean |
V | 代表void类型 |
L | 对象类型,比如:Ljava/lang/Object; |
[ | 数组类型,代表一维数组。比如:double[][][] is [[[D |
例如:
这一项存储着a字段的符号引用,它的名称是a,描述符是I,也就是基本数据类型int
上图的额外解释:上图的名字和描述符其实指的分别是一个索引,分别是cp_info#11和cp_info#12。(cp_info表示常量池,#11表示常量池中索引为11的位置)。后面的<a>和<l>是jclasslib工具帮我们显示出来的。点到11索引和12索引看看:
用描述符的标志符也可以解释我们平时打印数组时的情况
public static void main(String[] args) {Object[] objects = new Object[10];System.out.println(objects);}
输出:
打印出来的[L是什么东西?
[表示的是数组类型,L表示的是对象类型,java.lang.Object是包名,分号是内存地址。
常量池中的每一项都是下面表格中的一种类型,都有相同的特征,第一个字节是类型标志,是一个十进制数字,用于确定该项的类型,这个字节称为tag byte(标记字节、标签字节)。
类型 | 标志(或标识) | 描述 |
---|---|---|
CONSTANT_utf8_info | 1 | UTF-8编码的字符串 |
CONSTANT_Integer_info | 3 | 整型字面量 |
CONSTANT_Float_info | 4 | 浮点型字面量 |
CONSTANT_Long_info | 5 | 长整型字面量 |
CONSTANT_Double_info | 6 | 双精度浮点型字面量 |
CONSTANT_Class_info | 7 | 类或接口的符号引用 |
CONSTANT_String_info | 8 | 字符串类型字面量 |
CONSTANT_Fieldref_info | 9 | 字段的符号引用 |
CONSTANT_Methodref_info | 10 | 类中方法的符号引用 |
CONSTANT_InterfaceMethodref_info | 11 | 接口中方法的符号引用 |
CONSTANT_NameAndType_info | 12 | 字段或方法的符号引用 |
CONSTANT_MethodHandle_info | 15 | 表示方法句柄 |
CONSTANT_MethodType_info | 16 | 标志方法类型 |
CONSTANT_InvokeDynamic_info | 18 | 表示一个动态方法调用点 |
(后三种是jdk1.7引入的)
常量类型的具体细节表:
(对CONSTANT_utf8_info的修正:length表示的字节数,bytes的u1表示的每个元素一个字节)
以一个字节码例子解释
0016是常量池计数器,转换为十进制为22,也就是常量池中有21个项。再往后面是常量池中的第一个项,每一项由若干部分组成,但第一部分都是tag,占一个字节(u1),该值是0a也就是10。
查表:
10表示的是CONSTANT_Methodref_info,也就是方法的符号引用。再往后两个字节是该方法的所在的类的描述符在常量池中的索引,也就是0004,转换为十进制为4,也就是常量池中索引为4的一项,也就是070015那一项。接着继续查表7
表示类或接口的符号引用,再往后两个字节0015表示指向全限定名常量项的索引,转换成十进制为21,找常量池中的第21项也就是最后一项,也就是0100106a61一直到74那一项,第一个字节01转成十进制1,1查表
表示的是U8编码的字符串,再往后两个字节表示该字符串的长度,也就是0010,转成十进制为16,也就是16个字符数,往后数16个字节,也就是从6a一直到74,6a转成十进制是106,106对应的ASC码是j,最终6a到74对应的字符串就是java/lang/Object,也就是对应的是Object这个类!!!
为什么该类class文件的常量池部分的第一项是Object的符号引用?
因为该类继承了Object,一个类没有显示继承,默认就会继承Object。当JVM加载一个类时,它需要知道该类的所有相关信息,包括其超类。所以会有Object的符号引用。
如果该类有了显示继承,那么第一项是其父类的符号引用,就没有了Object的符号引用。
4.4访问标识
紧跟着常量池的2个字节是访问标识,还以上面图为例,该字节码文件的访问标识就是0021
访问标识用于标识类或接口的访问信息,这个class是类还是接口,是不是public的,是否定义为abstract等等。各种访问标识如下表:
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 标志为public类型 |
ACC_FINAL | 0x0010 | 标志被声明为final,只有类可以设置 |
ACC_SUPER | 0x0020 | 标志允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真。(使用增强的方法调用父类方法) |
ACC_INTERFACE | 0x0200 | 标志这是一个接口 |
ACC_ABSTRACT | 0x0400 | 是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假 |
ACC_SYNTHETIC | 0x1000 | 标志此类并非由用户代码产生(即:由编译器产生的类,没有源码对应) |
ACC_ANNOTATION | 0x2000 | 标志这是一个注解 |
ACC_ENUM | 0x4000 | 标志这是一个枚举 |
上面的例子访问标识为0021,对应表就是ACC_PUBLIC和ACC_SUPER的和(注意:访问标识是相加的结果)。
补充说明:
-
ACC_SUPER,现代编译器都会设置并使用这个标记,可以认为是默认带的。
-
带有ACC_INTERFACE则是接口,不带则是类。
-
带有ACC_ANNOTATION的是注解类型。如果设置了ACC_ANNOTATION,则必须也得有ACC_INTERFACE。
在jclasslib中访问标识在这查看
4.5类索引、父类索引、接口计数器、接口索引集合
紧跟着访问标识的依次是类索引、父类索引、接口计数器、接口索引集合,都是占2个字节。
长度 | 含义 |
---|---|
u2 | this_class |
u2 | super_class |
u2 | interfaces_count |
u2 | interfaces[interfaces_count] |
在上面的例子中,类索引、父类索引、接口计数器依次是0003、0004、0000,由于接口计数器为0,所以也就是实现了0个接口,接口所以集合也就没有了。类索引、父类索引、接口索引集合,都是索引,常量池的索引,也都是在常量池中找。比如0003是常量池中索引为3的项:070014,查表,表示的是类或接口的符号引用,再用14(转换为十进制为20)定位查表,表示的01001663一直到6f,表示的是字符串,转换为ACS码就可以了。
4.6字段表
紧跟着接口索引集合的是字段表计算器和字段表。字段表用于描述接口或类中声明的字段(fields),字段包括类变量(static修饰的变量)、实例变量(非statci修饰的变量)以及final修饰的常量,但不包括方法内部的局部变量。
字段的名字、数据类型、访问修饰符都在字段表里,但也都是通过引用常量池中的项来描述的,字段表里也都是存储的常量池的索引。
注意:字段表集合中不会列出从父类或接口继承来的字段,但也可能列出原本Java代码中不存在的字段,例如在内部类中为了保持对外部类的访问性,会自动添加指向外部类的实例字段。
只要是个表就会先有个计数器,字段表也有字段计数器(fields_count)。用来描述字段表中字段的个数,使用两个字节来存储。字段表中的每个成员都是一个field_info结构。
field_info结构
类型 | 名称 | 含义 | 数量 |
---|---|---|---|
u2 | access_flags | 访问标志(具体见下表) | 1 |
u2 | name_index | 字段名索引 | 1 |
u2 | descriptor_index | 描述符索引 | 1 |
u2 | attributes_count | 属性计数器 | 1 |
attribute_info | attributes | 属性集合 | attributes_count |
字段表访问标志
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | 字段是否为public |
ACC_PRIVATE | 0x0002 | 字段是否为private |
ACC_PROTECTED | 0x0004 | 字段是否为protected |
ACC_STATIC | 0x0008 | 字段是否为static |
ACC_FINAL | 0x0010 | 字段是否为final |
ACC_VOLATILE | 0x0040 | 字段是否为volatile |
ACC_TRANSTENT | 0x0080 | 字段是否为transient |
ACC_SYNCHETIC | 0x1000 | 字段是否为由编译器自动产生 |
ACC_ENUM | 0x4000 | 字段是否为enum |
字段名索引和描述符索引也都是在常量池中找。(描述符忘了是什么看一下前面的介绍)
字段的属性用于存储更多的额外信息,比如初始化值、一下注释信息。以常量属性为例,结构为:
ConstantValue_attribute{
u2 attribute_name_index;
u4 attribute_length;
u2 constantvalue_index;
}
对于常量属性而言,attribute_length值恒为2。
在jclasslib中查看字段表:
*
4.7方法表
紧跟着字段表的是方法表。方法表和字段表的套路差不多,也包括计数器和具体的表,只不过描述符会有些区别。
方法表:指向常量池索引的一个集合,完整描述了每个方法的签名。
每一个method_info项都对应着类或接口中的方法信息,比如方法的访问修饰符、返回类型、参数信息。
和字段表一样,一方面,方法表也只描述当前类或接口中声明的方法,不包括从父类或接口继承来的方法;另一方面,方法表也可能会自动列出由编译器自动添加的方法,最典型的就是类初始化方法<clinit>()和实例化方法<init>(),init就是构造器方法,如果没有显示定义类构造器,编译器会自动添加一个无参构造器,这一点在方法表中就能直接看到。
方法表计数器也用一个u2类型两个字节表示,方法表的结构和字段表也是相似的:
类型 | 名称 | 含义 | 数量 |
---|---|---|---|
u2 | access_flags | 访问标志(具体见下表) | 1 |
u2 | name_index | 方法名索引 | 1 |
u2 | descriptor_index | 描述符索引 | 1 |
u2 | attributes_count | 属性计数器 | 1 |
attribute_info | attributes | 属性集合 | attributes_count |
方法表访问标识
标志名称 | 标志值 | 含义 |
---|---|---|
ACC_PUBLIC | 0x0001 | public,方法可以从包外访问 |
ACC_PRIVATE | 0x0002 | private,方法只能从本类中访问 |
ACC_PROTECTED | 0x0004 | protected,本类、子类、同包中可访问 |
ACC_STATIC | 0x000 | static |
ACC_FINAL | 0x0010 | 为final |
ACC_VOLATILE | 0x0040 | volatile |
ACC_TRANSTENT | 0x008 | transient |
ACC_SYNCHETIC | 0x1000 | 是否为由编译器自动产生 |
ACC_ENUM | 0x4000 | enum |
每个方法也可以有属性,例如init方法的Code属性
每个属性也是一个attribute_info结构,也是一个表,结构如下:
类型 | 名称 | 数量 | 含义 |
---|---|---|---|
u2 | attribute_name_index | 1 | 属性名索引 |
u4 | attribute_length | 1 | 属性长度 |
u1 | info | attribute_length | 属性表 |
具体Code属性也有自己的格式(再次套娃):
Code属性中含有字节码指令,但是字节码中都是2a等等这样的16进制数字,怎么和aload_0这样的字节码指令对应起来的?
事实上,你去点一下jclasslib中的字节码
点击’显示JVM规范’会直接跳到oracle官网,可以看到0x2a和具体的aload_0这样的字节码指令的对应关系!!
这样,16进制的字节码也就翻译成了aload_0这样的字节码指令了。
再看上面Code属性的结构,会发现Code属性也有自己的属性(再次套娃),再jclasslib中点开Code属性:
就是LineNumberTable和LocalVariableTable两个属性。
不再往下继续查表了。。。
4.8属性表
紧跟着方法表的是属性表,指的是class文件所带的辅助信息,比如class文件的源文件的名称,一般无须深入了解。
5.总结
翻译字节码的过程其实就是拿着字典查字典的过程,一些描述信息都是在常量池中存储着,常量池中的CONSTANT_utf8_info类型,转成十进制,再对应ASC码表,就是我们在Java源代码中定义的东西,如String str中的str。
最后附上一张字节码的完全解析: