前两章我们已经提到,Java并没有严格对class的来源做出限定
这导致class的来源五花八门,而且存在安全隐患,试想一下,如果有人制作恶意class文件
jvm虚拟机照单全收,轻则导致电脑死机,重则直接造成财产损失,这显然是不被允许的
所以jvm自身的第一道防火墙,验证工作就开始了,一共四个步骤,文件格式验证,元数据验证,字节码验证,符号引用验证
文件格式很好理解,就比如我们买了一个拼装玩具,如果出现了一个拼装说明书上没有的零件,我们不得不怀疑是不是商家把零件搞混了,这种情况下,我们的第一反应肯定是去退货
jvm也差不多,他会检查一遍玩具的说明书看看是不是自己买的玩具的说明书(检查魔数是否为cafebabe)
看看说明书是不是属于同系列的新玩具(检查当前class文件的主副版本),最后看看零件是否齐全(检查常量池,方法表,字段表的各种标志位,然后结合上下文判断类型/数量是否异常,如,方法表标志位与方法数量不符,常量池是否有常量指向不存在的常量,标志位异常值)
如此,第一步就完成了,第二步和第一步差不多,不过检测的对象稍有不同
元数据就是类本身的数据,包含类的继承,实现的接口,jvm在此主要检测本类是否与父类冲突,例如,继承了父类find修饰的方法,非法的重载(方法参数一致,但是返回值不同)
第三步才是重头戏,字节码的校验
我在之前的文章中分析过一些简单的字节码,但是显然我们平时开发的项目并没有那么简单,可能涉及几十个类,几百个方法,各种调用层层叠叠
这显然是极端复杂的情况,而且这还涉及计算机中很著名的停机问题
什么是停机问题呢,用我的话来说,假设你有一段全知全能的代码,它可以判断所有代码是否可以完成(即停机),那么就有以下悖论
if(全知全能的代码)
while(true){}
else
return;
即,如果全知全能的代码认为程序可以停止,那么程序就不能停止,反之程序就可以停止,借由这个理论,可以得出永远无法用程序来判断程序是否正确
字节码的判断也是如此,对于元数据,和文件格式验证,我们可以判断他们是否符合某种格式,因为他们是固定不变的
但是对于可变(这里的可变是指可以随着条件的改变,可以有截然不同的结果)的字节码来说,显然想要判断出他们是否正确是一个伪命题,我们可以使用复杂的过程的将程序错误的几率降至最低
但是这显然会影响到程序执行的效率,比如jvm曾经采用的类型推导来检测
jvm最终采取了一个折中的办法,通过给方法表中有code属性的方法添加一个叫stackmaptable的属性,该属性记录了程序运行时的各种状态
相当于给程序拍一张照片,检查照片相比检查可以换衣服,带假发的真人显然要简单快速的多
、
jvm因此可以节省大量用来检测字节码的时间,但是有得必有失,理论上讲,黑客也可以拍张假照来糊弄jvm
所以我们需要注意即使经过验证,仍然有存在恶意class的可能性
最后一个步骤
符号引用的验证,就和他的名字一样,主要是验证一下符号引用,但是这里又有些不同
在前面文件格式的验证中,类自身的符号引用已经在文件格式验证中完成了
此处主要验证的是本类对其他类引用是否合法,如是否存在,是否可以被本类访问(如,引用的是private修饰的类型)