上一篇我们已经根据路径读取到了我们需要的字节码文件,就以java.lang.Object这个类为例,可以看到类似下面这种东西,那么这些数字是什么呢?
要了解这个,我们大概可以猜到这是十进制的,在线将十进制转为十六进制看看https://tool.oschina.net/hexconvert/,注意上图中已经用空格隔开了每个数,我们将最前面的变成十六进制看看效果,202对应CA,254对应FE,186对应BA,190对应BE,合起来就是CAFEBABE,有兴趣的可以查查这代表的时一种咖啡,所有的符合jvm规范的字节码文件都是以这个开头,专业称呼 "魔数";
不知道大家有没有发现,如果我们分析这个的时候要自己一个一个的转换,简直太坑爹了,但是有很多工具可以帮助我们更好的看十六进制的,比如vscode,editplus,winhex,jclasslib(这个看不到十六进制,但是可以看字节码文件的结构),实在不想下载的其他东西话用vim也可以看十六进制;这里强烈推荐一款工具叫做classpy,这个工具可以同时看十六进制和class字节码文件的结构,用起来很舒服;
链接:https://pan.baidu.com/s/1s_fqLxQjG0lVXMEB5z1mlg 提取码:gmyt ,使用这个classpy的时候,但是有一个前提,你计算机必须要有gradle环境!!!首先解压,然后需要进入classpy-master文件夹,命令行运行gradle uberjar,最后就是gradle run ,以后每次的话直接使用gradle run就行了!打开ui界面之后,把class手动丢进去就行了,如下图,左边是class文件的结构,右边的对应的十六进制;
1.简单说说class文件结构
首先说说class字节码文件的结构,看有哪几部分组成,其实在上图左边已经差不多说明了,下图更清楚:其中u2表示两个字节,u4表示四个字节,这之外的比如cp_info表示的是一张表,然后表中每一个字段又对应着一张表(这么说肯定不好理解,见过多维数组没,表就看作数组就好,只不多数组每个位置又对应这一个数组,这就叫多维数组);
至于下面这些代表什么意思,这里 就不多做赘述了,自己去看字节码文件的组成吧,不是我们的重点;
这里的结构有个很有意思的现象,就是在列出该项数据之前,会提前指明该数据有几个字节;比如constant_pool_count表示常量池中有n个表,占用2个字节;而紧接其后的constant_pool[constant_pool_count-1]存的就是各个表实际的数据,由于每个表第一个字节表示该表的类型,然后后面又会指定该表的大小,所以可以确定总共占用多少字节;access_flags表示访问权限,占两个字节,等等
接下来说说常量池中表的类型以及每个表的结构(每一种表都标识了自己占用的字节大小),如下所示,每一种表都有自己特有的结构,还要注意一点,下面这么多表中,某一个表中某一项可能会引用另外一张表的数据的;
常量池之外每个部分表示的什么,我随便找了一篇博客,参考这篇说的比较仔细的:https://www.jianshu.com/p/247e2475fc3a;这就不多说了,这也不是我们的重点;
2.读取class字节码文件
总的目录结构如下所示:
根据上面这个图我们将classfile中的文件分为几个部分理解一下,首先是class_reader.go这个文件里面是结构体,存了class文件的全部数据的字节切片,并且定义了一些方法一下子读取1字节,2字节,4字节和8字节等方法,方便于我们读取数据;
然后class_file.go文件中一个结构体,存了字节码中所有结构,就是魔数,版本号,常量池,访问修饰符等等,然后定义了一些获取这些部分的方法,可想而知这些方法需要使用前面说的class_reader.go文件中结构体读取数据;
再然后比较关键,就是class_file.go文件中定义的那些获取各个部分的方法,下图所示,其中最关键的就是读取常量池和属性表;
说道读取常量池数据,那么因为常量池中有很多不同类型的表,我们定义一个接口,所有的表都必须实现这个接口;至于总共有些什么类型的表,大致分为两种,一种是字符型,一种是引用型的;字符型的分为字符串和数字类的,分别是在上面的cp_utf8.go和cp_numberic.go中,其他的以cp开头的都是引用类型的表;
在读取常量池中的表的时候,我们首先要确定正在读取表的类型,在读取第一个字节的时候,该字节就是说明该表示什么类型,如下所示,然后每一种表都规定了字节的结构,前面已经说明白了;
const ( CONSTANT_Utf8 = 1 CONSTANT_Integer = 3 CONSTANT_Float = 4 CONSTANT_Long = 5 CONSTANT_Double = 6 CONSTANT_Class = 7 CONSTANT_String = 8 CONSTANT_Fieldref = 9 CONSTANT_Methodref = 10 CONSTANT_InterfaceMethodref = 11 CONSTANT_NameAndType = 12 CONSTANT_MethodHandle = 15 CONSTANT_MethodType = 16 CONSTANT_InvokeDynamic = 18 )
然后就是属性表,其实和常量池差不多定义了一个顶层接口,只不过属性表这里不是用这种数字来决定表的类型,而是用属性名(也就是字符串来区分),所以我们可以看到下面这种结构,通过读取属性表前面两个字节找到常量池的Constant_Utf8表的索引,然后取到字符串,再到下面这个switch中确定是什么类型的属性表;
属性表也有很多类型,我们这里只是列举其中的8种,至于每一种是什么意思,看看这个博客:https://www.cnblogs.com/lrh-xl/p/5351182.html,在上面的目录中attr_xxx开头的都是属性表,
3.各个文件
class_reader.go:用于帮助我们读取字节切片中的数据:
package classfile import "encoding/binary" //这个结构体从字节数组中读取数据 type ClassReader struct { data []byte } //读取一个字节,而且data数据也要将第一个字节干掉 func (this *ClassReader) readUint8() uint8 { //u1 val := this.data[0] this.data = this.data[1:] return val } //读取两个字节 func (this *ClassReader) readUint16() uint16 { //u2 val := binary.BigEndian.Uint16(this.data) this.data = this.data[2:] return val } //读取四个字节 func (this *ClassReader) readUint32() uint32 { //u4 val := binary.BigEndian.Uint32(this.data) this.data = this.data[4:] return val } //读取8个字节 func (this *ClassReader) readUint64() uint64 { val := binary.BigEndian.Uint64(this.data) this.data = this.data[8:] return val } //读取最前面的两个字节,表示数量 //根据这个数量继续往后面读取n个uint16的字节 func (this *ClassReader) readUint16s() []uint16 { n := this.readUint16() s := make([]uint16, n) for i := range s { s[i] = this.readUint16() } return s } //获取指定数量的字节 func (this *ClassReader) readBytes(length uint32) []byte { bytes := this.data[:length] this.data = this.data[length:] return bytes }