前言
对字节码垂涎已久,但由于较复杂,所以耽搁了很长时间,在周末两天参考的大量书籍,总结成这篇文章,由于网上很少有直接对字节码分析的,全贴一堆概念,让人很难懂,所以本章会结合实际的字节码来一一分析。
这个过程中会对一个class文件中所有字节挨个解析,推荐同样编译生成一个class,和这篇文章对应起来看。
编写Java文件
首先写个简单的Java类,然后编译生成class文件。
第一次分析的时候内容尽量少一点,不然字节码也会多,分析起来难。
public class Test {
private static final String name ="name";
private int age;
private static String addr;
public Test() {
}
private void print(){
System.out.println("print");
}
private static void printName(){
System.out.println(name);
}
public String getName(){
return name;
}
}
然后使用WinHex打开,或者其他十六进制文件查看器,这个编译好只有703字节,也不少。
结构预览
魔术和版本
大多数的文件开头的N个字节都是固定的,用来确保是不是一个合法的文件,比如把扩展名.jpg的文件改成.txt,通过图片查看器还是可以查看的,这是因为内部结构没有变,再比如png文件,它的开头是89504E470D0A1A0A,这是固定的,所有png格式的图片开头都是这个,但如果随便改一个,图片查看器就不能查看了,因为他确定不了这是一个png文件,即使后面的数据可以把图片的信息表示出来。
class文件也一样,开头的4个字节是CAFEBABE(这翻译过来是咖啡宝贝?),就是JVM用来校验是不是一个合法的字节码文件,其他软件也一样,大多数不会通过判断后缀名来确定一个文件的类型,而是通过文件的前N个字节。
紧跟后面的是版本,分别代表次版本和主版本,在不同版本的JDK上编译会是不同的,我的是JDK8,所以是00000034,如果是1.7,那么就是00000033。
常量池计数器、常量池
由于常量池内容不是固定得,是随着写得代码量而增加,所以需要2个字节来统计常量池的大小,所以跟着版本号后面的2个字节是常量池的大小,偏移地址是0x00000008。
常量池是占用Class文件最大得数据项之一,主要就是存放两大类数据,字面量和符号引用,字面量就是如字符串、被声明为final得常量值等,符号引用属于编译原理方面得东西。
从这里开始,就变得复杂了,会有一个叫表得东西出来,表就是由不同字节组成用来表示某个数据的东西,一共定义了11种不同的表结构,他们的共同点就是前1个字节都表示是哪种类型,后面就不一样。
在Test类中常量池大小是0x28,十进制是40,就是说他有39个常量,因为还有一个0项常量是空出来特殊考虑的,不做计算。
可以通过javap反编译查看一下,javap参数的-v和-verbose是相等的,但是不会输出private方法,还需要加-p参数,在后面会依赖这个输出进行对照。
上面1-39得数据都是一个表,每个表的第一个字节代表标志位(tag),取值1-12,代表当前属于哪种常量类型,(注意不存在标志位为2的数据类型),如下图所示。
由于常量内容太多,不能全部说完,所以只列举2个作为示例。
第一个常量解析
首先是第一个常量,值为0x0A,十进制为10,从上面图片中可以查出,这是个CONSTANT_Methodref_info(类中方法的符号引用),紧跟后面得2个字节给出了声明了被引用方法的类CONSTANT_Class_info入口索引,他的值是7,也就是说这个7指向的是常量池中第7个常量,后面还有2个字节是名称及类型描述符CONSTANT_NameAndType_info的索引值,他的值是1B,十进制是27。
在看第7个常量和第27是什么,也就是下面这个,这就是个互相引用的过程,引用来引用去,指向了一个CONSTANT_Utf8_info。
第二个常量解析
第二个是常量的值是9,代表CONSTANT_Fieldref_info,后2个字节指向声明字段的类或接口描述符CONSTANT_Class_info的索引项,在后面2个字节是指向字段描述符CONSTANT_NameAndType_info的索引项。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w4LG1qps-1601276480533)(https://upload-images.jianshu.io/upload_images/24613156-9d3e3a164df2fa0b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
这个指向来指向去,最终也指向了一个CONSTANT_Utf8_info字符。
后面的常量也是一个道理,一共有39个,在这39个常量结束之后,就是下面的访问标志了。
访问标志
紧接着常量池后面的就是2个字节是访问标志,那问题是,怎么定位到这两个字节的偏移位置呢?只有先把前面所占字节大小累加起来了。
最终定位到下图这个位置,值是21。
但是上面图片中0021又代表什么呢,其实这都是存在乌龟的屁股的,先看下面这张表。
[图片上传失败…(image-6bcea0-1601275736811)]
对应Test类看,首先他是public型的,所以存在值0001,还有符合ACC_SUPER,值是0020,最终运算方式是把他们进行或运算,也就是ACC_PUBLIC | ACC_SUPER=1 | 20=21。
如果把上面这个类改变abstract,那么他的值就会变成0421。
类索引、父类索引
紧接着访问标志后面的4个字节就是类索引(2字节)、父类索引(2字节),他们各自都会指向一个CONSTANT_Class_info 常量项,CONSTANT_Class_info中的name_index会指向常量池列表中类型为CONSTANT_Utf8_info常量项的索引,通过这个索引值就能获取到CONSTANT_Utf8_info常量项中的全限定名字符串。
比如下面的05,指向了CONSTANT_Class_info ,CONSTANT_Class_info 又指向了一个CONSTANT_Utf8_info,他得值为Test,同样父类也是一样,默认就是java/lang/Object。
接口计数器、接口表。
紧跟类索引、父类索引后面的2个字节是接口计数器,接口计数器用于表示当前类或者接口的直接超类接口数量,如果这个类没有实现任何接口,那么值就是0。为0的话后面的实现接口结构表也就不存在了。
接口表是一个数组集合,它包含了当前类或者接口在常量池列表中直接超类接口的索引集合,通过这个索引即可确定当前类或者接口的超类接口的全限定名。
但是现在接口计数器是0,后面接口的信息就不存在了,看不到了,为了验证变换,把上面的代码加点料,增加两个接口,然后再看变换。
可以看出,接口数变成了2,后面的0x0008、0x0009指向了常量池的类数据。
字段计数器、字段表
紧跟在接口计数器和接口表之后的2个字节就是字段计数器、后面是字段表,字段计数器用于记录字段表总数(和上面的接口计数器一样),也就是一个类中类变量(static)和实例变量的数量总和。
在Test类定义了3个字段,所以他的值是0x0003,他后面的n个字节会表示字段的信息,就是字段表,每个字段都会包含字段作用域(public、protected、private)、是类级别变量还是实例级别变量(static)、可变性(final)、并发可见性(volatile)、可序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)以及字段名称。
下面是字段的信息组成。
字段的访问标志。
字段name分析
首先第一个字段是 private static final String name =“name”;,所以前2个字节是0x001A,代表他的修饰,计算方式是0x0002|0x0008|0x0010,通过计算器可以算一下确实是1A。
在后面2个字节是0x0008,代表字段的名称,是对常量池的引用,然后再回到常量池中查看,这个字段名是name,和我们的相符。
在后面的2个字节是字段的描述符,也是对常量池的引用,描述符的作用是描述字段的数据类型,基本数据类型都是使用B(字节)、C(char)、D(double)、F(float)、I(int)、J(long)、S(short)、Z(boolean)、V(void)描述,而对象类型使用字符L加对象的全限定名组成,name这个字段是String类型,所以他的值就是Ljava/lang/String
在查看常量池中第9个是什么值,确实是Ljava/lang/String,也相符。
另外如果是数组的话,前面会加一个[来表示,如果是多维的话,那就多加几个[,是N维就加N个[
在后面2个字节是属性计数值,也就是最后的attributes项的大小,每个attributes都是由attribute_info表构成,在这个类中,他是1,所以他存在1个attribute_info的结构数据。
下面是attribute_info(属性表)的结构。
在接着分析,前2个字节是指向常量池中CONSTANT_Utf8_info的索引,代表当前属性的简单名称。
但是他的值是ConstantValue,这又是个啥意思?代表final关键字定义的常量值。
ConstantValue也是个结构,共有8个字节组成,
-
attribute_name_index:常量池的引用,值一定是"ConstantValue"。
-
attribute_length:他是固定的,占4个字节,值是2。
-
constantvalue_index :代表常量池中的一个字面量的引用,在这个类中他的值是6,所以回头在常量池中查找他,他的值肯定是字符串"name"。
下图是常量池第6个项数据。
字段age分析
然后在看第二个字段,在这个类中,第二个字段是 private int age,不妨先盲猜一波,首先是private的,对照上面的表,private的值是0x0002,这个字段无其他修饰,所以最终的前2个字节就是0x0002,接下来的2个字节是字段的名称,指向常量池中的索引,回到javap -verbose Test.class下查看,第11个 索引的值是age,十六进制就是B,所以,第二个字节就是0x000B,在后面两个字节是数据类型,在上面说过了,int类型的话就是I,在常量池中查找,第12个索引的值就是I,所以就能确定了,前6个字节就是0x0002000B000C。
在看接下来的数据,和我们猜想的一样。
在后面的两个字节是0,所以后面不存在和这字段相关的数据了,然后开始最后一个字段。
字段addr分析
最后一个字段的定义是 private static String addr;,在来盲猜一波,首先访问标志是private,值是0x0002,加了static,值是0x0008,然后进行或运算得出A,所以前2个字节就是0x000A,后面2个字节是字段名,指向常量池中得索引,在这个常量池中,第13个得值就是addr,十六进制就是D,后面得2个字节是数据类型,String会被表示成Ljava/lang/String;,在常量池中他是第9个,所以可得出,前6个字节就是000A 000B 0009。
在后面的两个字节是0,所以后面不存在和这字段相关的数据了,下面就到了方法信息了,是比较难的一块,因为设计到指令了。
方法计数器、方法表
字段分析完了,就剩下方法了,同样的逻辑,存有方法计数器、方法表,在上面结构结束之后紧跟的2个字节就是方法计数器,如果为0,就代表没有方法,方法表也不存在,但是在这个类中,方法有4个,也就是后面会有4个方法表。
方法表中表示方法的完整信息,比如方法修饰符、返回值、方法参数等信息。
方法表得结构和字段表得结构一样,如下图所示。
但是修饰标志会不一样,方法得修饰标志如下。
构造方法分析
在这个类中,第一个方法就是构造方法,前2个字节就是0001,后面2个字节是方法得名称,指向常量池中得索引,构造方法就是
在后面2个字节是返回值。构造方法得返回值就是()V。
在后面2个字节就是属性计数器,如果为0,就代表没有属性表,但是在这里他有一个,所以,后面会存在1个属性表,属性表得结构在上面说了,前两个字节是属性名,指向常量池中得索引,在这里,他是0010,常量池中得属性名是Code。
这个Code就是Java编译成得字节码指令,这里更复杂了。
然后属性表中得attribute_length(4字节)代表后面info项得具体大小,在这里他是33,十进制51,所以后面51个字节就是与构造方法有关的代码。
从attribute_length往后数51个字节后就是下一个方法的开始。
就是下面圈住的地方,这里一共51个字节。
所以这里还要看Code属性表,这个表比以往的更复杂。
前两个字节不说了,从第三个开始:
max_stack:操作数栈的深度最大值
max_locals:局部变量所需要的存储空间
code_length:存储Java字节码指令的长度
code:字节码指令,大小是code_length
exption_table_length:后面exception_table的大小
exception_table:异常表项,大小是exption_table_length
attributes_count:后面attributes的大小
attributes:attributes_info结构,大小是attributes_count。
如果要查看对应十六进制代表什么指令,那就需要查看doc了,官网:docs.oracle.com/javase/spec…
再来实战分析一下,从中先定位到指令的开始:
aload_0:将第一个引用类型本地变量推送到栈顶。
invokespecial :调用超类构造方法,实例初始化方法。
后面的0001是invokespecial的参数,查看常量池是java/lang/Object.😦)V的引用。
最后的B1是指令return的意思,这条指令结束后,方法结束。
在后面exception_table_length是00000,所以exception_info表就不存在,在后面2个字节是属性表的总数,Test类中他的值是2,所以存在2个attribute_info表。
这里第一个属性表是LineNumberTable,LineNumberTable就是用来记录java文件中代码行号和字节码中字节码行号之间都对应关系。
还有一个LocalVariableTable表,用于描述局部变量中局部变量和Java代码中定义的变量之间的对应关系。
这两个表先不做研究了。
print方法分析
然后是第二方法print(),他的修饰符是private,无其他修饰,所以前2个字节是0002,然后是方法名、方法返回类型、属性总数、属性表。
再来看他的指令:B2 0002 12 03 B6 0004 B1
分别代表:
B2:指令是getstatic,获取指定类的静态域,并将其压入栈顶,后面的0002 是常量池中数据,值是java/lang/System.out:Ljava/io/PrintStream;
12: 指令是ldc,意思是将int、float、String型常量值从常量池推送到栈顶,这里推送的03就是常量池中的print,这个print就是我们写在System.out.println方法中要打印的常量。
B6:指令是invokevirtual,调用实例的方法,参数是0004,在常量池中是java/io/PrintStream.println:(Ljava/lang/String;)V的方法引用。
B1:return
第三第四个方法同样也是一样,就不说了。
属性计数器、属性表
在方法计数器、方法表后面就是属性计数器、属性表,这个结构在上面已经提到过了,如在Test类中最后的是0001001900000002001A,代表有一个属性表(前2个字节),后面的就是属性表的数据了,
0x0019的的十进制是25,在常量池中是SourceFile。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c3bTtMYc-1601276480587)(https://upload-images.jianshu.io/upload_images/24613156-f01fa150ccbe2787.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]
SourceFile也是个表结构,代表源文件名。共有8个字节组成,也就是最后8位。
最后2个字节是在常量池中的引用,在这里他引用的26,值是Test.java
到这里这个文件就分析完了,但还有很多表结构没有说,如LineNumberTable 、InnerClasses。
这些会在后续慢慢说把。
作者:小安的技术梦 链接:https://juejin.im/post/6877135279111536654 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。