专注虚拟机与编译器研究

第7.2篇-类的连接之验证

在介绍字节码连接之前,有必要介绍一下字节码验证。HotSpot虚拟机其实会遵守《Java虚拟机规范》,对Class文件中包含的信息进行合法性验证,以保证虚拟机的安全。从整体上来看,验证阶段大致上会进行如下4方面的验证:

  • 文件格式验证:包括魔数,版本号等;
  • 元数据验证:对程序进行语义分析,如是否有父类,是否继承了不被继承的类,不是抽象类,是否实现了父类或者接口中的所有要求被实现的方法;
  • 字节码验证:指令级别的语义验证,如跳转指令不会跳转到方法体以外的代码上;
  • 符号引用验证:符号引用转化为直接引用的时候,可以看作是对类自身以外的信息进行匹配验证,如通过全限定名,是否能找到对应的类。

文件格式的验证大部分都会在解析类文件的parseClassFile()函数中进行,例如对魔数,版本号的验证,如下:

cfs->guarantee_more(8, CHECK_(nullHandle));  // magic, major, minor
u4 magic = cfs->get_u4_fast();
guarantee_property(magic == JAVA_CLASSFILE_MAGIC,"Incompatible magic value %u in class file %s",magic, CHECK_(nullHandle));

// Version numbers
u2 minor_version = cfs->get_u2_fast();
u2 major_version = cfs->get_u2_fast();

// Check version numbers - we check this even with verifier off
if (!is_supported_version(major_version, minor_version)) {
    if (name == NULL) {
      Exceptions::fthrow(
        THREAD_AND_LOCATION,
        vmSymbols::java_lang_UnsupportedClassVersionError(),
        "Unsupported major.minor version %u.%u",
        major_version,
        minor_version);
    } else {
      ResourceMark rm(THREAD);
      Exceptions::fthrow(
        THREAD_AND_LOCATION,
        vmSymbols::java_lang_UnsupportedClassVersionError(),
        "%s : Unsupported major.minor version %u.%u",
        name->as_C_string(),
        major_version,
        minor_version);
    }
    return nullHandle;
}

魔数验证了值,而版本号能够让当前虚拟机判断是否支持运行这个Class版本的文件。通常在读取文件中相关字节码时,都会调用guarantee_more()方法,以保证文件中有足够的字节信息用来读取。

元数据验证的逻辑也大部分都在类解析阶段完成,例如在parseClassFile()中对父类的验证逻辑如下:

if (super_klass.not_null()) {
  if (super_klass->is_interface()) {
	ResourceMark rm(THREAD);
	Exceptions::fthrow(
	  THREAD_AND_LOCATION,
	  vmSymbols::java_lang_IncompatibleClassChangeError(),
	  "class %s has interface %s as super class",
	  class_name->as_klass_external_name(),
	  super_klass->external_name()
	);
	return nullHandle;
  }
  // Make sure super class is not final
  if (super_klass->is_final()) {
	THROW_MSG_(vmSymbols::java_lang_VerifyError(), "Cannot inherit from final class", nullHandle);
  }
}

验证父类不能为接口或有final修饰的类,否则将出现异常。 

在classFileParse.cpp文件中定义了一系列verify_xxx()或check_xxx()方法,都是对元数据进行验证的,有兴趣的读者可自行阅读。 

在InstanceKlass::link_class_impl()方法中调用verify_code()方法进行字节码验证,方法的实现如下:

bool InstanceKlass::verify_code(instanceKlassHandle this_oop, bool throw_verifyerror, TRAPS) {
  // 1) Verify the bytecodes
  Verifier::Mode mode = throw_verifyerror ? Verifier::ThrowException : Verifier::NoException;
  return Verifier::verify(this_oop, mode, this_oop->should_verify_class(), CHECK_false);
}

Verifier::verify()方法的实现如下: 

bool Verifier::verify(instanceKlassHandle klass, Verifier::Mode mode, bool should_verify_class, TRAPS) {
  HandleMark hm;
  ResourceMark rm(THREAD);

  Symbol*        exception_name = NULL;
  const size_t   message_buffer_len = klass->name()->utf8_length() + 1024;
  char*          message_buffer = NEW_RESOURCE_ARRAY(char, message_buffer_len);
  char*          exception_message = message_buffer;

  const char* klassName = klass->external_name();
  // 失败回退,新的使用StackMapTable属性进行验证的叫类型检查,而之前的叫类型推导验证
  bool can_failover = FailOverToOldVerifier &&
                      klass->major_version() < NOFAILOVER_MAJOR_VERSION;

  // If the class should be verified, first see if we can use the split
  // verifier.  If not, or if verification fails and FailOverToOldVerifier
  // is set, then call the inference verifier.
  if (is_eligible_for_verification(klass, should_verify_class)) {
    // STACKMAP_ATTRIBUTE_MAJOR_VERSION的值为50
    if (klass->major_version() >= STACKMAP_ATTRIBUTE_MAJOR_VERSION) {
      ClassVerifier  split_verifier(klass, THREAD);
      split_verifier.verify_class(THREAD);
      exception_name = split_verifier.result();
      if (
    	    can_failover &&
            !HAS_PENDING_EXCEPTION &&
            (
               exception_name == vmSymbols::java_lang_VerifyError() ||
               exception_name == vmSymbols::java_lang_ClassFormatError()
            )
	  ) {
          if (TraceClassInitialization || VerboseVerification) {
              tty->print_cr("Fail over class verification to old verifier for: %s", klassName);
          }
          // 如果失败,则回退到类型推导验证
          exception_name = inference_verify(klass, message_buffer, message_buffer_len, THREAD);
      }
      if (exception_name != NULL) {
    	  exception_message = split_verifier.exception_message();
      }
    } else {
        // 推导验证
        exception_name = inference_verify(klass, message_buffer, message_buffer_len, THREAD);
    }
  }

  // ...
}

由于数据流验证的高复杂性,虚拟机设计团队为了避免过多的时间消耗在字节码验证阶 段,在JDK 1.6之后的Javac编译器和Java虚拟机中进行了一项优化,给方法体的Code属性的 属性表中增加了一项名为“StackMapTable”的属性,这项属性描述了方法体中所有的基本块 (Basic Block,按照控制流拆分的代码块)开始时本地变量表和操作栈应有的状态,在字节 码验证期间,就不需要根据程序推导这些状态的合法性,只需要检查StackMapTable属性中 的记录是否合法即可。这样将字节码验证的类型推导转变为类型检查从而节省一些时间。

在JDK 1.6的HotSpot虚拟机中提供了-XX:-UseSplitVerifier选项来关闭这项优化,或者使用参数-XX:+FailOverToOldVerifier要求在类型校验失败的时候退回到旧的类型推导方式进 行校验。而在JDK 1.7之后,对于主版本号大于50的Class文件,使用类型检查来完成数据流 分析校验则是唯一的选择,不允许再退回到类型推导的校验方式。  

验证阶段不是必须的,如果代码运行已经稳定了之后,可以通过设置参数-Xverify:none参数来关闭类验证,减少虚拟机的类加载时间,提高运行效率。

 

 

  

 

posted on 2020-08-11 09:12  鸠摩(马智)  阅读(529)  评论(0编辑  收藏  举报

导航