class文件检验器

     class文件检验器实现的安全目标之一就是程序的健壮性。如果某个有漏洞的编译器,或是某个聪明的黑客,产生了一个class文件,而这个class文件中包含了一个方法,这个方法的字节码中含有一条跳转到方法之外的指令,那么,一但这个方法被调用,它将导致虚拟机的崩溃。所以,出于对健壮性的考虑.由虚拟机检验它装载的字节码的完整性是非常重要的。
      Java虚拟机的class文件检验器在字节码执行之前,必须完成大部分检验工作。它只在执行前而不是在执行中对字节码进行一次分析(并检验它的完整性),每一次遇到一个跳转指令时都进行检验。作为字节码确认工作的一部分,虚拟机将确认所有的跳转指令会到达另一条合法的指今,而且这条指令是在这个方法的字节码流中的。在大多数情况下,在执行前就对所有字节码进行一次检查,对于保证健壮性来说就足够了,而不必在它运行时每次都检验每一条字节码指令。
      class文件检验器要进行四趟独立的扫描来完成它的操作。第一趟扫描是在类被装载时进行的,在这次扫描中,class文件检验器检查这个class文件的内部结构,以保证它可以被安全地编译。第二和第三趟扫描是在连接过程中进行的,在这两次扫描中,class文件检验器确认类型数据遵从java编程语言的语义,包括检验它所包含的所有字节码的完整性。第四趟扫描是在进行动态连接的过程中解析符号引用时进行的,在这次扫描中,class文件检验器确认被引用的类、字段以及方法确实存在。
第一趟:class文件的结构检查
      在第一趟扫描中,对每一段将被当作类型导入的字节序列,class文件检验器都会确认它是否符合java class文件的基本结构。在这次扫描中,检验器将进行许多检查,例如每个class文件必须以四个同样的字节开始:魔数OxCAFEBABE。这个魔数的用处是让class文件分析器很容易分辨出某个文件有明显问题而加以拒绝,这个文件可能是被破坏了的class文件,或者是压根儿就不是class文件。这样,class文件检验器所做的第一件事极可能就是检查导入的文件是否是以OxCAFEBABE开头的。检验器还必须确认在class文件中声明的主版本号和次版本号,这个版本号必须在这个java虚拟机实现可以支持的范围之内。
      而且,在第一趟扫描中,class文件检验器必须确认这个class文件没有被删节,尾部也没有附带其他的字节。虽然不同的class文件有不同的长度,但是在class文件中包含的每一个组成部分都声明了它的长度和类型。检验器可以使用组成部分的类型和长度来确定整个class文件的正确的总长度。用这种方法,它就可以检查一个装入的文件,其长度是否和它里面的内容相一致
      第一趟扫描的主要目的就是保证这个字节序列正确地定义了一个新类型,它必须遵从java的class文件的固定格式,这样它才能被编译成在方法区中的〔基于实现的)内部数据结构。第二、第三和第四的扫描不是在符合class文件格式的二进制数据上进行的,而是在方法区中的、由实现决定的数据结构上进行的。
第二趟:类型数据的语义检查
      在第二趟扫描中,class文件检验器进行的检查不需要查看字节码,也不需要查看和装载任何其他类型。在这趟扫描中,检验器查看每个组成部分,确认它们是否是其所属类型的实例,它们的结构是否正确。例如,方法描述符〔它的返回类型,以及参数的类型和个数)在class文件中被存储为一个字符串.这个字符串必须符合特定的上下文无关文法。检验器对每个组成部分进行检查的目的之一是,为了确认某个方法描述符都是符合特定语法的、格式正确的字符串。
另外,class文件检验器检查这个类本身是否符合特定的条件,它们是由java编程语言规定的。例如,检验器强制规定除Object类以外的所有类,都必须有一个超类。在第二趟的扫描中,检验器还要检查final类(最终的)类没有被子类化,而且final方法没有被覆盖。还要检查常量池中的条目是合法的,而且常量池的所有索引必须指向正确类型的常量池条目。也就是说, class文件检验器在运行时检查了一些java语言应该在编译时遵守的强制规则。因为检验器并不能确定class文件是否是由一个善意的、没有漏洞的编译器产生的,所以它会检查每个class文件,以确保这些规则得到遵守。
第三趟:字节码验证
      class文件检验器成功地进行了两趟检查后,它将把注意力放在字节码上,这一趟扫描被称为“字节码验证”。在这趟扫描中,Java虚拟机对字节流进行数据流分析,这些字节流代表的是类的方法。为了理解字节码检验器,必须对字节码和栈帧有一定的了解。
      字节码流代表了Java的方法,它是由被称为操作码的单字节指令组成的序列,每一个操作码后都跟着一个或多个操作数。操作数用于在Java虚拟机执行操作码指令时捉供所需的额外的数据。执行字节码时,依次执行每个操作码,这就在Java虚拟内构成了执行的线程。每一个线程被授予自己的java栈,这个栈是由不同的栈帧构成的。每一个方法调用将获得一个自己的栈帧———栈帧其实就是一个内存片断,其中存储着局部变量和计算的中间结果。在栈帧中,用于存储方法的中间结果的部分被称为该方法的操作数栈。操作码和它的(可选的)操作数可能指存储在操作数栈中的数据,或存储在方法栈帧中的局部变量中的数据。这样,在执行一个操作码时,除了可以使用紧随其后的操作数,虚拟机还可以使用操作数栈中的数据,或局部变量中的数据,或是两者都用。
      字节码检验器要进行大量的检查,以确保采用任何路径在字节码流中都得到一个确定的操作码,确保操作数栈总是包含正确的数值以及正确的类型。它必须保证局部变量在赋予合适的值以前不能被访向,而且类的字段中必须总是被赋予正确类型的值,类的方法被调用时总是传递正确数值和类型的参数。字节码检验器还必须保证每一个操作码都是合法的,即每一个操作码都有合法的操作数,以及对每一个操作码,合适类型的数值位于局部变量中或是在操作数栈中。这些仅仅是字节码检验器所做的大量检验工作中的一小部分,在整个检验过程通过后,它就能保证这个字节码流可以被Java虚拟机安全地执行。
      字节码检验器并不试图检测出所有的安全问题。如果要这样做的话,它将会遇到‘停机问题‘。停机问题是计算机科学领域的一个著名论题:即不可能写出一个程序,用它来判断作为其输入而读入的某个程序在执行时是否停机。一个程序是否会停机被称为是程序的“不可判定“特性,因为不可能写出一个程序,让它100%地告诉你任何一个给定的程序是否含有这种特性。停机问题的不可判定性可以扩展成计算机程序的许多特性,如一个java字节码的的集合能否被虚拟机安全的执行。
      字节码检验器处理停机问题的方法是,不去试图精确地让每个安全的程序都通过检查。虽然不能写出一个程序来判定任何给定程序是否会停机,但是可以写出一个简单的程序,让它只是识别出某些一定会停机的程序。例如,如果一个程序的第一条指令就是停机,那么,这个程序一定可以停机。如果一个程序内没有循环,它也一定可以停机.如此等等。同样,虽然不可能写出一个能扫描检查所有字节码流是否能被虚拟机安全执行的检验器,但是可以写出一个能让其中一部分安全的字节码流通过的检验器。Java的字节码检验器恰恰就是这么做的。这个检验器检查确认读入的每一个字节码集合是否符合一个特定的规则集合。如果一个字节码集合能够遵从所有这些舰则,那么检验器就知道它可以被虚拟机安全地执行。如果不是,那么,这些字节码可能可以被虚拟机安全地执行,也可能不能安全地执行。这样,通过识别一些安全的字节码流,但不是全部,检验器就绕过了停机问题。由于字节码检验器强制检查的特性,只要定义好规则,任何程序只要可以用java程序语言书写,编译器就可以确保编译出来的字节码可以被检验器通过。有些程序虽然不能用java编程语言源代码表达出来,但仍可以通过检验器的位验。另外还有些程序(也不能用Java代码表示),它们虽然实际上也能被java虚拟机安全地执行,却不能通过检验器的检验。
      在第一、第二、第三趟扫描中,class文件检验器可以保证导人的class文件构成合理,内在一致,符合Java编程语言的限制条件,并且包含的字节码可以被java虚拟机安全地执行。如果class文件检验器发现其中任何一点不正确.它将会抛出一个错误,这个class文件将不会被程序使用。
第四趟:符号引用的加证
     在动态连接的过程中,如果包含在一个class文件中的符号引用被解析时,class文件检验器将进行第四趟检查。在这趟检查中,Java虚拟机将追踪那些引用一从被验证的class文件到被引用的class文件,以确保这个引用是正确的。因为第四趟扫描必须检查被检测的class文件以外的其他类,所以这次扫描可能需要装载新的类。大多数Java虚拟机的实现采用延迟装载类的策略.直到类真正地被程序使用时才装载。即使一个实现确实预先装载了这些类,这是为了加快装载过程的速度,那它还是会表现为延迟装载。例如,如果java虚拟机在预先装载中发现它不能找到某个特定的被引用类,它并不在当时抛出NoClassDefFoundError错误,而是直到(或者除非)这个被引用类首次被运行程序使用时才抛出。这样,如果Java虚拟机进行预先连接,第四趟扫描可以紧随第三趟扫描发生。但是如果java虚拟机在某个符号引用第一次使用时才进行解析,那么第四趟扫描将在第三趟扫描以后很久、当字节码被执行时才进行。
      class文件检验器的第四趟扫描仅仅是动态连接过程的一部分。当一个class文件被装载时,它包含了对其他类的符号引用以及它们的字段和方法。一个符号引用是一个字符串,它给出了名字,并且可能还包含了其他关于这个被引用项的信息—-这些信息必须足以惟一地识别一个类、字段或方法。这样,对于其他类的符号引用必须给出这个类的全名;对于其他类的字段的符号引用必须给出类名、字段名以及字段描述符;对于其他类中的方法的引用必须给出类名、方法名以及方法的描述符。
      动态连接是一个将符号引用解析为直接引用的过程。当java虚拟机执行字节码时,如果它遇到一个操作码,这个操作码第一次使用一个指向另一个类的符号引用,那么虚拟机就必须解析这个符号引用。在解析时,虚拟机执行两个基本任务:
          l)查找被引用的类(如果必要的话就装载它)。 
         2)将符号引用替换为直接引用,例如一个指向类,字段或方法的指针或偏移量。
      虚拟机必须记住这个直接引用,这样当它以后再次遇到相同的引用时,它就可以立即使用这个直接引用,而不必花时间再次解析这个符号引用了。
      当Java虚拟机解析一个符号引用时,class文件检验器的第四趟扫描确保了这个引用是合法的。当这个引用是个非法引用时---例如,这个类不能被装载,或这个类的确存在,但是不包含被引用的字段或方法--class文件检验器将抛出一个错误。
      再以Volcano类为例。如果Volcano类中的某个方法调用了名为Lava的类中的某个方法,这个Lava中的方法的全名和描述符将包含在Volcano的class文件的二进制数据中。当Volcano的方法在执行过程中第一次调用Lava的方法时,Java虚拟机必须确认Lava中存在这个方法,并且这个方法的名字和描述符与Volcano类中期待的相匹陪。如果这个符号引用(类名、方法名和描述符)是正确的,那么,虚拟机将把它替换为一个直接引用,例如一个指针,从那时开始将使用这个指针。但如果Volcano类中的符号引用不能匹配Lava类中的任何方法时,第四趟扫描验证失败,Java虚拟机将抛出一个NoSuchMethodError。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值