一、从源代码到本地代码。
我们都知道,机器是读不懂我们的高级语言的,从高级语言到机器语言,需要经历两个比较大的阶段(前端编译和后端编译),虽然我们今天的主题是前端编译,不过我觉得有必要系统的阐述一下。下面盗个图解释一下,毕竟画图太耗时间。这个是编译原理:
从源代码到机器码用java的话简单说,前端编译负责由.java编译成.class(javac);后端编译负责从.class生成机器码(解释器、JIT)。
前端编译:词法分析(将字符序列转换为标记序列)、语法分析(构造抽象语法树,用于描述程序语法结构是否正确)、语义分析(对结构上正确的源程序进行上下文有关性质的审查,进行类型审查)
通俗的来讲,可以理解为,你写了一篇作文,老师先把你的作文一个字一个字的读进去,然后分成字、词语、标点等等,这个是词法分析。然后呢,看看你分了几个段,段头又没有空格、符不符合主谓宾,有没有标题,有没有句号,括号是不是只有一边啥的,这是语法分析。最后通篇检查你写的符不符合逻辑,有没有前言不搭后语啥的,这是语义分析。
当然,这个类比并不严格,大体理解意思,知道每个阶段的关键作用就行了。
后端编译:主要分解释编译和即时编译。解释编译可以简单理解为一条一条的将字节码翻译成机器码。即时编译简单理解就是把一大段字节码翻译成机器码。这两个通俗的说法区别于有没有卡顿。解释执行一条一条没有卡顿但是执行效率低、即时编译一下一大段有卡顿,但是执行效率高。一般是结合使用的,一般逻辑解释执行、热点代码即时编译。有些对卡顿现象不敏感的场景,可以全部使用JIT。还有一种是AOT,就是直接从源代码到机器码,明显优点是保护代码,难反编译,特定场景下性能很好,缺点是对运行时支持不好,尤其是动态代理等场景。
再盗个图,毕竟画图能力捉鸡:
二、反编译字节码
字节码长啥样,我们先来一段简单的源代码:
一个简单的类我们来看看这段源代码产生的字节码长啥样:
student字节码长这样婶的。是不是有点懵,脑瓜子嗡嗡的。别急,咱们一点点的把这个字节码给反编译一下。通过这个过程,理解一下字节码的结构。
先科普一下基本知识,上面的图是16进制的,我们知道一个字节是8位,也就是2^8,2^4*2所以两个16进制表示一个字节。
要翻译,得找本字典,字典来了:
字典一:字节码结构图
class文件结构从这个结构上,我们发现,4个字节的魔数、2个字节次版本、2个字节主版本、2个字节常量池常量个数、然后跟着一个不定长的常量数组、2个字节访问标志、2个字节类本身信息、2个字节父类信息、2个字节接口数量、2个字节接口名数组、2个字节变量个数、然后跟着变量数组、2个字节方法个数、然后跟着方法数组、2个字节附加属性数量、然后是附加属性数组
4个字节魔数:
cafe babe每个类都是以这个魔数开头的,这是高司令定的,对于这个魔数,我是这么理解的,JAVA是一种咖啡,每个class都是这个cafe的babe,那为啥不是baby呢,因为16进制没有y。
2个字节主版本、2个字节次版本:
0000代表次版本是0 ;0034是52,52代表1.8;所以版本是1.8.0
2个字节是常量量个数:
0022说明有34个常量,我们看看反编译工具的结果:
常量池我们发现一共33个,不是34个,那0号呢?0号预留了,为异常处理留的。
后面紧跟着常量表的内容。我们先上一下常量池的字典再说
常量池元素类型表有人会问了,常量池是啥,干嘛的。笼统点的解释就是,后面的各结构里能用到的资源,包括一些能反复使用的字符串、还有一些类型信息啥的。咱们都写过脚本吧,有些执行脚本里,遇到一些常量啥的,我们都喜欢抽出来,然后脚本里面可以反复的引用。
这个常量池就好像一个资源预先准备的地方,之后用到的东西,很多都直接给各常量号就行了,不用再写一遍了。这样可以最大的节省空间。
我们一个常量一个常量的解析。
第1个常量:
首先1个字节是常量类型:
第一个常量的类型0a表示是10,查字典发现是
符号引用之前咱们讲过,类的初始化在连接阶段的解析阶段会将符号引用转变为直接引用,当时没有解释啥是符号引用、这个就是符号引用。因为编译阶段不知道具体的对象在哪儿,只能用类型加全名来表示引用,称为符号引用。
言归正传,后面有2个字节是类描述符的索引和2个字节名称和类型的索引
指向第5个常量和第29个常量。咱们还没解释出来这俩,先用工具看看是啥
第5个是这样婶的:
可以看到他又指向了第33个
第33是这样的,是个utf8的字符串。为啥第一个不直接指向33呢,因为类型不同。
第29的是这样的:
又指向了10 和 11
这俩组合起来就是"<init>":()V 最后组合起来就是:
表示java/lang/Object.的传参为空返回为空的构造方法。从第一个常量,我们可以看出规律来,最基本的是Utf8类型,相当于是基本的材料,组合这些材料,产出一个相对复杂的常量。产生的常量又可以作为之后程序中使用的材料。
第2个常量:
这是个类的成员变量的描述信息:
指向第4个常量和第30个常量:
表示student类里的int型名字为age的变量:
第3个常量:
指向第4个和第31个常量
结果为,表示student类类型为String名称为name的变量:
第4个常量:
指向第32个常量:
有字典对着可以一直翻译完。翻译完就是咱们执行 javap -verbose classpath/com/dabai/test/student.class的结果是一致的。这里提供各个阶段的字典。
Access_Flag对应表:
field结构:
method结构:
code:
其中method反编译比较麻烦,我们以setName为例子,讲解一下。
首先2个字节的access_flag:0001:ACC_PUBLIC 0013指向第19个常量:
0014指向第20个常量:
就是说,方法是public的、传入一个String类型的参数、返回时空的。
然后又2个字节的属性个数0002,有两个。先看第一个。2位属性名称索引:000C:
名称为 code 。4位属性长度:
属性长度:62个字节
2位最大栈深度:0002,2位内部变量:0002个,4位长度:00000006
代码:2a 对应代码:aload_0:加载第0个内部变量,
2b 对应代码:aload_1:加载1个内部变量
b5 对应代码:putfield
0003 对应常量池第3个常量即name:
b1:return
之后两个字节跟着是异常表长度:0000
然后是2个字节code属性个数:0002
第一个code属性:2个字节的属性名称:000d 对应第13个常量:
这个表示行号表,行号表结构如下:
后面跟着4字节的长度信息:0000000a,表示有长度为10
0002:代表两个行号对应
0000:字节码code的第0行,对应原代码的第000c 12行
0005:字节码code的第5行,对应原代码的第000d 13行
后面跟着的是本地变量表,就是内部变量000e:
4个字节表示长度:00000016:22
0002:代表变量个数,这里我们发现,命名只有一个变量,也就是传入的name,为什么是两个呢
我们继续反编译,0000:开始位置 0006:长度6 000f:第15个常量
后面是描述:0010:第16个常量,表示对象本身
0000:slot:0
然后是第二个变量:0000:开始为止 0006:长度 0006:第6个常量:
然后是描述:0007:第7个常量:
0001:slot:1
后面是方法的第二个属性,跟code属性平级。方法传入的参数。
0015:第21个常量,表示方法参数
00000005:长度为5
01:第一个参数,这里还可以回答我们一个问题,java的方法最多可以接受多少个参数,答案是ff即255个
0006:第6个常量:
0000:AcessFlag
最后,展示一下反编译之后的常量池和方法setName
方法setname:
这篇终于写完了,真是费劲。有些人可能觉得这么做一遍没啥意义,各花入各眼,仁者见仁吧。
网友评论