转载请附上原文地址哦
JVM系列1---字节码文件的解析
一.Java程序运行流程:由Java编译器将java文件编译成java字节码文件(.class文件),由Java虚拟机执行的就是字节码文件。那么也就意味着你手写的字节码只要符合jvm的规范,jvm也是可以执行的,这也就是为什么Kotlin编写的代码可以在JVM执行的原因。
二.字节码大小端的问题,假如由我们去写一个程序去解析字节码的话,一定要注意一个问题是,如果我们的机器是小端(x86体系的是小端的机器),那么在底层存储的时候高位的数据是存在低地址的(我的理解就是每读1个数据类型的偏移量,如果顺着地址读,那就是和正常的顺序是反过来的,计算机是以字节为基本单位,假如一个int类型如果是0x12345678,那读出来就是78 56 34 12,顺着读的时候),那么如果是自己写程序去解析字节码的话,要注意大小端的问题。
三.第一张图是借别人的,然后这是一个最简单的程序,逐步展开解析,第二张图是一个最简单的程序,第三张图是用sublime text打开之后的结果
0.对于字节码文件,解析的时候依靠的偏移量和当前解析的属性是第几个属性来决定的,对于变长的属性,一般在属性前面都会有一个长度属性,然后如果这个属性读出来为0,本来这个属性跟着的结构就不会出现在字节码文件中
1.首先二进制文件一开始的有个魔数,这个是规定的,是0xcafebabe,jvm如果一开始读到的不是这个,那么jvm则拒绝执行,上面的例子也就是在一开始的地方,jvm会先把他读掉。
2.接下来,读两次两个字节分别获得次版本号和主版本号,下面这些是10进制,从字节码直接读出来的是16进制,把他转成10进制之后,对照着表可以判断这个class应该在什么版本的java执行,上面的例子中minor version 是0000,而major version 0x0034,把34转化为10进制后也就是52,在下表中查询可知jdk版本为1.8
3.接下来读2个字节的长度,读出常量池的大小,常量池是一个变长的结构,我把他理解为一个有不同元素的数组,然后就会根据从读出的常量池的长度的下一个字节开始解析常量池,然后往下查询常量池中的一个元素的tag,首先判断这一个元素的类型,然后根据类型可以知道接下来这个元素的结构是什么,然后按照对应的结构读当前元素剩下属性的偏移量,在上面那个例子中读到常量池大小按个属性是0x0019,转化为10进制之后是25,然后根据jclasslib可以知道,常量池是从坐标1开始的,所以这里常量池有24个元素,然后接下来按照规则解析,tag占一个字节,0x0019下一个是0x0a,再表中对应的tag是tag为10的,可以知道这第一个常亮的类型为CONSTANT_Mehodref_info,那么可以知道这个元素还有两个长度为U2的字段,那接着就是从class文件中继续读两个字节读出来分别是0x0004 和 0x0015,这里两个元素只有两个字节的原因是他这里读出来的两个属性的值只是在常量池里面的偏移量,相当于记录了一个数组的下标,这里直接由于常量池太多了,所以直接用jclasslib查看,然后像CONSTANT_Utf8_info这种,就是字符串常量,除了被常量池本身元素的字段引用到,也可以被常量池之后的依赖。
如上面的图片
4.常量池解析完后就到了access_flag字段,在常量池后读两个字节,下面的值表明了当前类的情况,如果有哪个标志修饰了类,那这个字段的值加上去就行了,这个类似于linux里面文件的1 2 4的权限,用7可以表示 1 2 4的权限都含有。
5.然后接下来的两个字段类名和父类名,存的是常量池中的他们需要的字符串对应的偏移量
6.然后就是实现接口的数量,注意这里如果说实现接口的数量为0,那么接下来那个接口的结构也不会出现在class文件中
7.然后接着就是属性字段(filed)的数量了,属性的数量在这里不做具体的解析,但是如果需要去手动解析的话,用接下来的方法,首先如果属性字段的数量为0的话,之后那个filed_info[]也不会出现在.class文件中,在C中,我们可以把它把filed_info理解为一个结构体,然后filed_info[]是一个结构体数组,那么我们如果需要知道里面的内容,只要查询这个结构体的组成以及结构体里属性的占用空间,我们就可以继续解析.class文件了
就像这个结构一样,补充说明一下那个attribute_info是属性的属性,假如你属性前面有final之类的修饰,这里attributes的数量也会相应增加,然后那个attribute_info是另一个结构体,如果真的要手动解析的话,需要去查询他结构体的组成,然后如果一般有值的话
8.解析类的属性的话,和第七点说的解析属性的属性差不多,都是要找到相应的结构体,然后查他属性的偏移量
9.然后方法的解析虽然也类似,但是是最麻烦的也是最重要的,首先说一下方法的描述符,他的作用是可以生成一个包括属性和返回值的方法
描述符描述的方法,返回值在括号里面,用,为分隔符,如果是非引用类型非数组,方法的属性直接用商标中的字母表示(不包括L 和 [),如果是非引用类型的数组,那么在那个属性前面加[,每多一维就多一个[,举个例子,[I表示一维整数数组,[[I表示二维整数数组,如果是引用非数组类型那么首先写出所引用类的全限定名(包名+类型),然后在前面加一个L,如果说是引用数组,那也是在前面加[,每多一维度就多加一个[,然后类的全限定名的结尾要加;作为结束,然后方法参数列表的括号后面要跟方法的返回类型,这个的规则和括号里面的方法参数是差不多的
接下来来解析方法,首先是方法数量,上面那个例子中,这个字段的值为3,有3个方法<init>,<clinit>,main,其中前两个是由编译器生成的,clinit是在类中有静态方法或静态代码块时,编译生成的,init是默认构造方法,然后看一下method_info的结构
method_info{
u2 access_flags;
u2 name_index; //name_index是指向常量池的方法名的index,如果是构造方法,那这个index在常量池中的那个地方的值就是<init>,静态代码块则是<clinit>
u2 descriptor_index;//常量池的index,字符串类型,descriptor也是就是上面说的方法描述符
u2 attributes_count;//这里有值的时候,才有下面那个字段
attribute_info attributes[attributes_count];
}