文件类的验证
尽管 Java 编译器只需生成满足前文所述所有静态和结构约束的类文件,但 Java 虚拟机无法保证它所要求加载的任何文件都是由该编译器生成的,或者其格式是正确的。例如,像网络浏览器这样的应用程序不会下载源代码然后进行编译;这些应用程序会下载已经编译好的类文件。浏览器需要确定该类文件是出自可靠的编译器,还是出自企图利用 Java 虚拟机的攻击者。
编译时检查的另一个问题是版本差异。用户可能已经成功将一个类(比如“PurchaseStockOptions”)编译为“TradingClass”的子类。但“TradingClass”的定义可能自该类被编译以来发生了变化,且这种变化与现有的二进制文件不兼容。方法 可能已被删除,或者其返回类型或修饰符已被更改。字段可能已改变类型,或者从实例变量变为类变量。方法或变量的访问修饰符可能已从公共变为私有。有关这些问题的讨论,请参阅《Java 语言规范》第 13 章“二进制兼容性”,Java SE 8 版本。
由于这些潜在问题,Java 虚拟机需要自行验证它试图整合的类文件是否满足所需的约束条件。Java 虚拟机实现会在链接时验证每个类文件是否满足必要的约束条件(§5.4)。
链接时的验证提高了运行时解释器的性能。原本在运行时必须为每个解释指令执行的用于验证约束条件的昂贵检查可以被省略。Java 虚拟机 4.10 类文件的验证 类文件格式168 机器可以假定这些检查已经完成。例如, Java 虚拟机将会预先知晓以下内容:
- 操作数栈不会出现溢出或下溢的情况。 • 所有局部变量的使用和存储都是有效的。
- 所有 Java 虚拟机指令的参数都是有效类型的。
Java 虚拟机实现可以采用两种验证策略:
- 通过类型检查进行验证必须用于版本号大于或等于 50.0 的类文件。
- 通过类型推断进行验证必须被所有 Java 虚拟机实现所支持,但那些符合 Java ME CLDC 和 Java Card 配置文件的实现除外,以验证版本号小于 50.0 的类文件。 支持 Java ME CLDC 和 Java Card 配置文件的 Java 虚拟机实现的验证受其各自规范的约束。 在这两种策略中,验证主要涉及对代码属性的 Code 数组(§4.7.3)中的静态和结构约束进行强制执行(§4.9)。
然而,在验证过程中,还有三个超出代码属性范围的额外检查必须执行:
- 确保最终类不会被子类化。
- 确保最终方法不会被重写(第 5.4.5 节)。
- 检查每个类(除了 Object 类之外)是否都有直接的父类。
通过类型检查进行验证
版本号为 50.0 或更高的类文件(§4.1)必须按照本节给出的类型检查规则进行验证。 只有当类文件的版本号等于 50.0 时,如果类型检查失败,Java 虚拟机实现方可选择通过类型推断来尝试进行验证(§4.10.2)。
这是一种实用的调整措施,旨在简化向新验证规范的过渡。许多操作类文件的工具可能会以某种方式修改方法的字节码,这需要对方法的栈映射帧进行调整。如果工具没有对栈映射帧进行必要的调整,即使字节码原则上是有效的(并且因此在旧的类型推断方案下也能验证),类型检查也可能失败。为了给实现者留出时间来适应他们的工具,Java 虚拟机:类文件格式 验证类文件 4.10169 实施过程可能会回退到较旧的验证方法,但这种做法仅限于特定情况下使用。时间。 在类型检查失败但调用了类型推断并成功的情况下,预计会存在一定的性能损失。这种损失是不可避免的。它还应当向工具供应商发出信号,表明他们的输出需要进行调整,并为供应商提供进一步的动力来做出这些调整。
总之,通过类型推断进行验证的支持了两种情况:一是逐步在 Java SE 平台上添加栈映射帧(如果版本 50.0 类文件中没有这些帧,则允许进行转换),二是逐步从 Java SE 平台移除 jsr 和 jsr_w 指令(如果版本 50.0 类文件中存在这些指令,则允许进行转换)。
如果 Java 虚拟机实现尝试对版本 50.0 类文件进行类型推断验证,那么在所有类型检查失败的情况下都必须进行此类操作。
这意味着 Java 虚拟机实现不能在某些情况下选择使用类型推断,而在其他情况下不使用。它必须要么拒绝无法通过类型检查验证的类文件,要么在类型检查失败时始终进行类型推断验证转换。类型检查器会依据通过 Prolog 语句所指定的类型规则来进行检查。
classIsTypeSafe(Class) :-
classClassName(Class, Name),
classDefiningLoader(Class, L),
superclassChain(Name, L, Chain),
Chain \= [],
classSuperClassName(Class, SuperclassName),
loadedClass(SuperclassName, L, Superclass),
classIsNotFinal(Superclass),
classMethods(Class, Methods),
checklist(methodIsTypeSafe(Class), Methods).
classIsTypeSafe(Class) :-
classClassName(Class, 'java/lang/Object'),
classDefiningLoader(Class, L),
isBootstrapLoader(L),
classMethods(Class, Methods),
checklist(methodIsTypeSafe(Class), Methods).
英语文本以一种非正式的方式描述了这些类型规则,而 Prolog 语句则提供了正式的规范。 类型检查器要求每个具有“Code”属性的方法都提供一个栈映射帧列表(§4.7.3)。栈映射帧列表由“Code”属性的“StackMapTable”属性给出(§4.7.4)。其目的是确保在方法中的每个基本块的开头都必须出现一个栈映射帧。栈映射帧会指定每个操作数栈条目和每个局部变量在每个基本块开始时的验证类型。类型检查器会读取具有“Code”属性的方法的栈映射帧列表,并使用这些映射来生成“代码”属性中指令的类型安全性的证明。 如果一个类的所有方法都是类型安全的,并且它没有子类化一个最终类,则该类就是类型安全的。班级。
Prolog 中的谓词 classIsTypeSafe 假定 Class 是一个 Prolog 术语,该术语代表一个已成功解析并加载的二元类。这 该规范并未强制规定此术语的具体结构,但确实要求对某些谓词进行定义。
例如,我们假设有一个谓词类方法(Class, Methods),其第一个参数为上述描述的表示类的术语,该谓词会将第二个参数绑定到一个包含该类所有方法的列表中,这些方法以一种便于理解的形式呈现。后来。 如果谓词类“isTypeSafe”不为真,类型检查器就必须抛出“验证错误”异常,以表明类文件存在格式错误。否则,该类文件已通过类型检查且字节码验证已完成。成功地。 本节的其余部分将详细解释类型检查的过程:
- 首先,我们为核心 Java 虚拟机的构件(如类和方法)提供 Prolog 语句(§4.10.1.1)。
- 其次,我们指定类型检查器所知的类型系统(§4.10.1.2)。
- 第三,我们指定指令和栈映射帧的 Prolog 表示形式(§4.10.1.3、§4.10.1.4)。
- 第四,我们说明如何对无代码的方法进行类型检查(§4.10.1.5)以及对有代码的方法进行类型检查(§4.10.1.6)。 類別檔案格式 程式碼檔案驗證 4.10171 • 第五,我们探讨了所有加载和存储指令所共有的类型检查问题(§4.10.1.7),以及对受保护成员的访问问题(§4.10.1.8)。 • 最后,我们明确了对每条指令进行类型检查的规则(§4.10.1.9)。
Java 虚拟机构件的访问器
我们规定存在 28 个 Prolog 语句式谓词(“访问器”),它们具有特定的预期行为,但其正式定义并未在本规范中给出。
classClassName(Class, ClassName) 提取类 Class 的名称,即 ClassName。
classIsInterface(Class) 若类 Class 是接口,则返回真。
classIsNotFinal(Class) 若类 Class 不是最终类,则返回真。
classSuperClassName(Class, SuperClassName) 提取类 Class 的超类名称,即 SuperClassName。
classInterfaces(Class, Interfaces) 提取类 Class 的直接超接口列表,即 Interfaces。
classMethods(Class, Methods) 提取类 Class 中声明的方法列表,即 Methods。
classAttributes(Class, Attributes) 提取类 Class 的属性列表,即 Attributes。 每个属性都以形式为“attribute(AttributeName, AttributeContents)”的函数应用来表示,其中 AttributeName 是属性的名称。属性内容的格式未作规定。
classDefiningLoader(Class, Loader) 提取类 Class 的定义类加载器,即 Loader。
**isBootstrapLoader(Loader)**当类加载器 Loader 为启动类加载器时,该条件为真。
loadedClass(Name, InitiatingLoader, ClassDefinition) 当存在一个名为名称的类,且该类(按照本规范的要求)在由加载器启动加载器加载时的表示形式为类定义时,该条件为真。
methodName(Method, Name) 提取方法 Method 的名称(Name)。
methodAccessFlags(Method, AccessFlags) 提取方法 Method 的访问标志(AccessFlags)。
methodDescriptor(Method, Descriptor) 提取方法 Method 的描述符(Descriptor)。
methodAttributes(Method, Attributes) 提取方法 Method 的属性列表(Attributes)。
isInit(Method) 若方法(不论所属类)为 则返回 True。
isNotInit(Method) 若方法(不论所属类)非为 则返回 True。
isNotFinal(Method, Class) 若类 Class 中的方法 Method 为非最终则返回 True。
isStatic(Method, Class) 若类 Class 中的方法 Method 为静态则返回 True。
isNotStatic(Method, Class) 若类 Class 中的方法 Method 为非静态则返回 True。
isPrivate(Method, Class) 若类 Class 中的方法 Method 为私有则返回 True。
isNotPrivate(Method, Class) 若类 Class 中的方法 Method 为非私有则返回 True。
isProtected(MemberClass, MemberName, MemberDescriptor) 若在类 MemberClass 中存在名为 MemberName 且描述符为 MemberDescriptor 的受保护成员则返回 True。
isNotProtected(MemberClass, MemberName, MemberDescriptor) 若不存在名为 MemberName 且描述符为 MemberDescriptor 的受保护成员则返回 True。在类“MemberClass”中有一个名为“MemberDescriptor”的成员,且该成员未设为受保护状态。 parseFieldDescriptor(Descriptor, Type) 将字段描述符(Descriptor)转换为相应的验证类型(Type)。
parseMethodDescriptor(Descriptor, ArgTypeList, ReturnType) 将方法描述符 Descriptor 转换为与方法参数类型相对应的验证类型列表 ArgTypeList,以及与返回类型相对应的验证类型 ReturnType。
parseCodeAttribute(Class, Method, FrameSize, MaxStack, ParsedCode, Handlers, StackMap) 提取 Class 中 Method 方法的指令流 ParsedCode,以及最大操作数栈大小 MaxStack、最大局部变量数量 FrameSize、异常处理程序 Handlers 和栈映射 StackMap。 指令流和栈映射属性的表示必须符合 §4.10.1.3 和 §4.10.1.4 中的规定。
samePackageName(Class1, Class2) 当且仅当 Class1 和 Class2 的包名相同时返回 True。
differentPackageName(Class1, Class2) 当且仅当 Class1 和 Class2 的包名不同时返回 True。 在检查方法体类型时,访问有关方法的信息很方便。为此,我们定义了一个环境,这是一个由以下元素组成的六元组:
- 一个类
- 一个方法
- 方法的声明返回类型
- 方法中的指令
- 操作数栈的最大大小
- 异常处理程序列表 我们指定了用于从环境中提取信息的访问器
allInstructions(Environment, Instructions) :-
Environment = environment(_Class, _Method, _ReturnType,
Instructions, _, _).
exceptionHandlers(Environment, Handlers) :-
Environment = environment(_Class, _Method, _ReturnType,
_Instructions, _, Handlers).
maxOperandStackLength(Environment, MaxStack) :-
Environment = environment(_Class, _Method, _ReturnType,
_Instructions, MaxStack, _Handlers).
thisClass(Environment, class(ClassName, L)) :-
Environment = environment(Class, _Method, _ReturnType,
_Instructions, _, _),
classDefiningLoader(Class, L),
classClassName(Class, ClassName).
thisMethodReturnType(Environment, ReturnType) :-
Environment = environment(_Class, _Method, ReturnType,
_Instructions, _,
我们指定了一些额外的谓词,以便从数据中提取更高级别的信息。环境。
offsetStackFrame(Environment, Offset, StackFrame) :-
allInstructions(Environment, Instructions),
member(stackMap(Offset, StackFrame), Instructions).
currentClassLoader(Environment, Loader) :-
thisClass(Environment, class(_, Loader)).
最后,我们定义了一个在整个类型规则中通用的谓词:
notMember(_, []).
notMember(X, [A | More]) :- X \= A, notMember(X, More).
决定哪些访问器是规定好的而哪些是完全明确的所遵循的原则是:我们不想过度详细地规定类文件的表示方式。 为“类”或“方法”项提供具体的访问器会迫使我们完全明确地规定表示类文件的 Prolog 项的格式。
验证类型系统
类型检查器依据一种基于验证类型层次结构的类型系统进行校验,该结构示意图如下所示。
大多数验证类型与表 4.3-A 中字段描述符所表示的基本类型和引用类型之间存在直接对应关系:
- 基本类型 double、float、int 和 long(字段描述符 D、F、I、J)各自对应同名的验证类型。
- 基本类型 byte、char、short 和 boolean(字段描述符 B、C、S、Z)都对应验证类型 int。
- 类型和接口类型对应使用函数对象的验证类型。班级。验证类型类(N,L)表示由加载器 L 加载的具有二进制名称为 N 的类。请注意,L 是由类(N,L)所代表的类所启动的加载器(§5.3),并且它可能是该类的定义加载器,也可能不是。 例如,类类型 Object 将被表示为 class('java/lang/Object',BL),其中 BL 是引导加载器。
- 数组类型与使用“arrayOf”函数的验证类型相对应。最终的译文:这个 验证类型 arrayOf(T) 表示其元素类型为验证类型 T 的数组类型。 例如,整型数组 int[] 和对象数组 Object[] 将分别通过 arrayOf(int) 和 arrayOf(class('java/lang/Object', BL)) 来表示。 验证类型 uninitialized(Offset) 通过将函数对象 uninitialized 应用于表示数值的参数来表示。偏移。 其他验证类型在 Prolog 中表示为原子,其名称表示所涉及的验证类型。 验证类型的子类型规则如下。 子类型关系是自反的。
isAssignable(X, X)。 在 Java 编程语言中,不是引用类型的验证类型具有以下形式的子类型规则:
isAssignable(v, X) :- isAssignable(the_direct_supertype_of_v, X)。 也就是说,如果 v 的直接超类型是 X 的子类型,那么 v 就是 X 的子类型。规则如下:
这些子类型规则未必就是子类型关系的最直观表述方式。“有” 在 Java 编程语言中,对于引用类型的子类型规则与对于其余验证类型的规则之间存在着明显的区分。 这种区分使我们能够阐述 Java 编程语言中的引用类型与其他验证类型之间的一般子类型关系。 这些关系不受 Java 引用类型在类型层次结构中的位置影响,并有助于防止 Java 虚拟机实现出现不必要的类加载。对于 例如,我们不会因为接收到“class(foo, L) <: twoWord”这样的查询而开始遍历 Java 类的超类层次结构。
我们还有一条规则,即子类型关系是自反的,因此这些规则共同涵盖了 Java 编程语言中大多数非引用类型的验证类型。
对于 Java 编程语言中的引用类型,其子类型规则是通过 isJavaAssignable 递归指定的。
isAssignable(class(X, Lx), class(Y, Ly)) :-
isJavaAssignable(class(X, Lx), class(Y, Ly)).
isAssignable(arrayOf(X), class(Y, L)) :-
isJavaAssignable(arrayOf(X), class(Y, L)).
isAssignable(arrayOf(X), arrayOf(Y)) :-
isJavaAssignable(arrayOf(X), arrayOf(Y)).
对于任务而言,接口的处理方式与“对象”相同。
isJavaAssignable(class(_, _), class(To, L)) :-
loadedClass(To, L, ToClass),
classIsInterface(ToClass).
isJavaAssignable(From, To) :-
isJavaSubclassOf(From, To).
数组类型是对象类型的子类型。其目的还在于,数组类型也是可克隆类和 java.io.Serializable 接口的子类型。
isJavaAssignable(arrayOf(_), class('java/lang/Object', BL)) :-
isBootstrapLoader(BL).
isJavaAssignable(arrayOf(_), X) :-
isArrayInterface(X).
isArrayInterface(class('java/lang/Cloneable', BL)) :-
isBootstrapLoader(BL).
isArrayInterface(class('java/io/Serializable', BL)) :-
isBootstrapLoader(B
对于基本类型数组之间的类型细分,其关系即为等同关系。
JavaAssignable(arrayOf(X), arrayOf(Y)) :-
atom(X),
atom(Y),
X = Y.
引用类型数组之间的类型细分是协变的
(X), arrayOf(Y)) :- compound(X), compound(Y), isJavaAssignable(X, Y).
子类化是自反的
isJavaSubclassOf(class(SubclassName, L), class(SubclassName, L)).
isJavaSubclassOf(class(SubclassName, LSub), class(SuperclassName, LSuper)) :-
superclassChain(SubclassName, LSub, Chain),
member(class(SuperclassName, L), Chain),
loadedClass(SuperclassName, L, Sup),
loadedClass(SuperclassName, LSuper, Sup).
superclassChain(ClassName, L, [class(SuperclassName, Ls) | Rest]) :-
loadedClass(ClassName, L, Class),
classSuperClassName(Class, SuperclassName),
classDefiningLoader(Class, Ls),
superclassChain(SuperclassName, Ls, Rest).
superclassChain('java/lang/Object', L, []) :-
loadedClass('java/lang/Object', L, Class),
classDefiningLoader(Class, BL),
isBootstrapLoader(BL).
指令表示
在 Prolog 中,单个字节码指令被表示为术语,其函数名即为指令的名称,而其参数则是对其解析后的操作数。
例如,一个 aload 指令被表示为术语“aload(N)”,其中包含指令操作数 N。 整个指令被表示为一系列形式为“instruction(Offset, AnInstruction)”的术语列表。
例如,“instruction(21, aload(1))”。 此列表中的指令顺序必须与类文件中的顺序相同。 一些指令的操作数是常量池中的项,这些项代表字段、方法和动态调用点。在常量池中,字段由 CONSTANT_Fieldref_info 结构表示,方法由 CONSTANT_InterfaceMethodref_info 结构(用于接口的方法)或 CONSTANT_Methodref_info 结构(用于类的方法)表示,动态调用点由 CONSTANT_InvokeDynamic_info 结构表示。 这些结构以以下形式表示为函数应用:
- 对于字段,有 CONSTANT_Fieldref_info 结构中的 class_index 项所引用的类名、字段名和字段描述符,其中 FieldClassName 表示该引用的类的名称,而 FieldName 和 FieldDescriptor 分别对应于 CONSTANT_Fieldref_info 结构中 name_and_type_index 项所引用的字段名和字段描述符。
- 对于接口的方法,有 imethod 结构(MethodIntfName、MethodName 和 MethodDescriptor),其中 MethodIntfName 表示由 CONSTANT_InterfaceMethodref_info 结构中的 class_index 项所引用的接口的名称,而 MethodName 和方法描述符与 CONSTANT_InterfaceMethodref_info 结构中的 name_and_type_index 项所引用的名称和方法描述符相对应;
- 对于类的方法,有(类名,方法名,方法描述符)这样的形式,其中类名是 CONSTANT_Methodref_info 结构中的 class_index 项所引用的类的名称,而方法名和方法描述符则对应于 CONSTANT_Methodref_info 结构的 name_and_type_index 项所引用的名称和方法描述符;并且
- 对于动态调用站点而言,使用“dmethod(CallSiteName, MethodDescriptor)”函数, 其中 CallSiteName 和 MethodDescriptor 分别对应于 CONSTANT_InvokeDynamic_info 结构中“name_and_type_index”项所引用的名称和方法描述符。
为了便于理解,我们假设字段和方法描述符(第 4.3.2 节、第 4.3.3 节)已被映射为更易读的名称:类名的开头和结尾的“L”和“;”会被删除,而用于基本类型的基类型字符会被映射为相应类型的名称。
例如,如果指令是“getfield”,其操作数是引用常量池中类型为 F 的类 Bar 中字段 foo 的索引,则其表示形式为“getfield(field('Bar', 'foo', 'F'))”。
常量池中引用常量值的条目,例如 CONSTANT_String、CONSTANT_Integer、CONSTANT_Float、CONSTANT_Long、CONSTANT_Double 和 CONSTANT_Class,都是通过具有相应名称的函数对象进行编码的,这些名称分别为 string、int、float、long、double 和 classConstant。
例如,用于加载整数 91 的 ldc 指令会被编码为 ldc(int(91))。
堆栈映射帧表示
在 Prolog 中,栈映射帧的表示形式为一个由以下形式的术语组成的列表: stackMap(偏移量, 类型状态地点:
- 偏移量是一个整数,表示栈图帧生效的字节码偏移量(§4.7.4)。 此列表中字节码偏移量的顺序必须与类文件中的顺序一致。
- 类型状态是位于偏移量处指令的预期输入类型状态。类型状态是对方法的操作数栈和局部变量中的位置与验证类型之间的映射。其形式为: frame(局部变量位置、操作数栈、标志)地点:
- “locals” 是一个验证类型列表,其中列表中的第 i 个元素(采用基于 0 的索引方式)代表第 i 个局部变量的类型。
- “operandStack” 是一个验证类型列表,列表中的第一个元素代表操作数栈顶部的类型,而位于顶部以下的栈元素的类型则按适当顺序依次排列在列表中。 大小为 2 的类型(长整型和双精度型)由两个栈元素表示,其中第一个元素为顶部,第二个元素为类型本身。 例如,一个包含双精度值、整数值和长整数值的栈在类型状态中表示为包含五个元素的栈:顶部和双精度值的元素、整数值的元素、顶部和长整数值的元素。 因此,“operandStack” 是列表 [顶部、双精度、整数、顶部、长整数]。
- “flags” 是一个列表,其可以为空或者只有一个元素,即 flagThisUninit。 類型文件格式 类型验证 4.10181 如果“局部变量”中任何变量的类型为“未初始化This”,那么“Flags”就会有一个单一元素“flagThisUninit”;否则,“Flags”就是一个空列表。 “flagThisUninit”在构造函数中被使用,用于标记尚未完成此类型初始化的状态。在这样的状态中,从该方法中返回是非法的。 验证类型的子类型化是按点式扩展到类型状态的。 方法的局部变量数组通过构造具有固定的长度(参见第 4.10.1.6 节中的“methodInitialStackFrame”);而操作数栈则会增长。收缩。因此,我们需要对期望可赋值的运算符栈的长度进行明确检查。
frameIsAssignable(frame(Locals1, StackMap1, Flags1),
frame(Locals2, StackMap2, Flags2)) :-
length(StackMap1, StackMapLength),
length(StackMap2, StackMapLength),
maplist(isAssignable, Locals1, Locals2),
maplist(isAssignable, StackMap1, StackMap2),
subset(Flags1, Flags2)。
运算符栈的长度不能超过声明的最大栈长度。
operandStackHasLegalLength(Environment, OperandStack) :-
length(OperandStack, Length),
maxOperandStackLength(Environment, MaxStack),
Length =< MaxStack。
某些数组指令(§aaload、§arraylength、§baload、§bastore)会查看运算符栈上的值的类型,以检查它们是否为数组类型。最终的译文:这位男士说道。 该子句会从类型状态中获取操作数栈中的第 i 个元素。
nth1OperandStackIs(i, frame(_Locals, OperandStack, _Flags), Element) :-
nth1(i, OperandStack, Element)。
加载和存储指令对操作数栈的操作(第 4.10.1.7 节)由于某些类型在栈中占用两个位置而变得复杂。 下面给出的谓词已将此情况考虑在内,从而使规范的其余部分能够忽略这一问题。 从栈中弹出一系列类型。
可以执行以下操作: 将(局部变量、操作栈、标志)帧转换为(局部变量、已弹出的操作栈、标志)帧; 调用“popMatchingList”函数,该函数接收操作栈、类型列表以及已弹出的操作栈,并返回新的操作栈。
popMatchingList 函数的实现如下: 如果操作栈为空,则将操作栈复制到新操作栈中; 否则,从操作栈中弹出类型 P,并将其存储在临时操作栈中,然后递归调用 popMatchingList 函数处理剩余的类型列表。
该函数的作用是从操作栈中弹出指定类型的元素。更确切地说,如果操作栈的逻辑顶部是指定类型的子类型,则执行弹出操作。如果某个类型占用两个操作栈位置,则操作栈的逻辑顶部实际上是顶部下方的类型,而操作栈的顶部是不可使用的类型顶部。 popMatchingType 函数的实现如下:
如果操作栈中包含类型 ActualType 和其他元素,则将 ActualType 与指定类型 Type 进行比较,并将结果存储在操作栈中; 如果操作栈中只有顶部元素 Type,则将顶部元素 Type 与指定类型 Type 进行比较,并将结果存储在操作栈中。
sizeOf 函数的实现如下: 如果类型 X 的大小为 2,则将其视为可分配类型; 如果类型 X 的大小为 1,则将其视为不可分配类型; 如果顶部元素为 top,则将大小设为 1。
将逻辑类型推送到操作栈中。具体行为取决于类型的大小。如果要入栈的类型大小为 1,我们就直接将其压入栈中。如果要入栈的类型大小为 2,我们就先将其压入栈中,然后再压入栈顶元素。
pushOperandStack(OperandStack, 'void', OperandStack)。 如果要入栈的类型大小为 2,则执行以下操作:pushOperandStack(OperandStack, 类型,[类型 | 操作数栈])。
如果要入栈的类型大小为 1,则执行以下操作:pushOperandStack(OperandStack, 类型,[栈顶,类型 | 操作数栈])。 如果存在空间,则将一系列类型压入栈中。
类型检查 抽象和本地方法
如果抽象方法和原生方法未重写任何最终方法,那么它们就被认为是类型安全的。
methodIsTypeSafe(Class, Method) :-
doesNotOverrideFinalMethod(Class, Method),
methodAccessFlags(Method, AccessFlags),
member(abstract, AccessFlags).
methodIsTypeSafe(Class, Method) :-
doesNotOverrideFinalMethod(Class, Method),
methodAccessFlags(Method, AccessFlags),
member(native, AccessFlags)
私有方法和静态方法与动态方法的调用方式是相互独立的, 因此它们从不会覆盖其他方法。
doesNotOverrideFinalMethod(class('java/lang/Object', L), Method) :- isBootstrapLoader(L). doesNotOverrideFinalMethod(Class, Method) :- isPrivate(Method, Class). doesNotOverrideFinalMethod(Class, Method) :- isStatic(Method, Class). doesNotOverrideFinalMethod(Class, Method) :- isNotPrivate(Method, Class), isNotStatic(Method, Class), doesNotOverrideFinalMethodOfSuperclass(Class, Method). doesNotOverrideFinalMethodOfSuperclass(Class, Method) :- classSuperClassName(Class, SuperclassName), classDefiningLoader(Class, L), loadedClass(SuperclassName, L, Superclass), classMethods(Superclass, SuperMethodList), finalMethodNotOverridden(Method, Superclass, SuperMethodList).
那些私有且/或静态的方法是较为罕见的,因为私有方法和静态方法本身是不能被重写的。因此,如果发现有最终的私有方法或最终的静态方法,那么从逻辑上讲,它们不可能被其他方法所重写。
finalMethodNotOverridden(Method, Superclass, SuperMethodList) :-
methodName(Method, Name),
methodDescriptor(Method, Descriptor),
member(method(_, Name, Descriptor), SuperMethodList),
isFinal(Method, Superclass),
isPrivate(Method, Superclass).
finalMethodNotOverridden(Method, Superclass, SuperMethodList) :-
methodName(Method, Name),
methodDescriptor(Method, Descriptor),
member(method(_, Name, Descriptor), SuperMethodList),
isFinal(Method, Superclass),
isStatic(Method, Superclass).
如果发现的是非最终的私有方法或非最终的静态方法,则跳过它,因为这类方法与重写无关。
finalMethodNotOverridden(Method, Superclass, SuperMethodList) :-
methodName(Method, Name),
methodDescriptor(Method, Descriptor),
member(method(_, Name, Descriptor), SuperMethodList),
isNotFinal(Method, Superclass),
isPrivate(Method, Superclass),
doesNotOverrideFinalMethodOfSuperclass(Superclass, Method).
finalMethodNotOverridden(Method, Superclass, SuperMethodList) :-
methodName(Method, Name),
methodDescriptor(Method, Descriptor),
member(method(_, Name, Descriptor), SuperMethodList),
isNotFinal(Method, Superclass),
isStatic(Method, Superclass),
doesNotOverrideFinalMethodOfSuperclass(Superclass, Method).
如果发现了一个非终结、非私有、非静态的方法,那么就意味着该方法确实没有被最终方法覆盖。否则,就向上递归处理。
finalMethodNotOverridden(Method, Superclass, SuperMethodList) :-
methodName(Method, Name),
methodDescriptor(Method, Descriptor),
member(method(_, Name, Descriptor), SuperMethodList),
isNotFinal(Method, Superclass),
isNotStatic(Method, Superclass),
isNotPrivate(Method, Superclass).
finalMethodNotOverridden(Method, Superclass, SuperMethodList) :-
methodName(Method, Name),
methodDescriptor(Method, Descriptor),
notMember(method(_, Name, Descriptor), SuperMethodList),
doesNotOverrideFinalMethodOfSuperclass(Superclass, Method).
类型检查方法与代码
非抽象、非原生的方法如果具有代码并且该代码是类型正确的,那么它们就是类型正确的。
methodIsTypeSafe(Class, Method) :-
doesNotOverrideFinalMethod(Class, Method),
methodAccessFlags(Method, AccessFlags),
methodAttributes(Method, Attributes),
notMember(native, AccessFlags),
notMember(abstract, AccessFlags),
member(attribute('Code', _), Attributes),
methodWithCodeIsTypeSafe(Class, Method).
如果一种带有代码的方法能够将代码和栈映射帧合并为一个单一的流,使得每个栈映射帧都位于其对应的指令之前,并且合并后的流是类型正确的,那么这种方法就是类型安全的。此外,如果该方法有异常处理程序,那么这些异常处理程序也必须是合法的。
methodWithCodeIsTypeSafe(Class, Method) :-
parseCodeAttribute(Class, Method, FrameSize, MaxStack,
ParsedCode, Handlers, StackMap),
mergeStackMapAndCode(StackMap, ParsedCode, MergedCode),
methodInitialStackFrame(Class, Method, FrameSize, StackFrame, ReturnType),
Environment = environment(Class, Method, ReturnType, MergedCode,
MaxStack, Handlers),
handlersAreLegal(Environment),
mergedCodeIsTypeSafe(Environment, MergedCode, StackFrame).
首先让我们来探讨异常处理程序。
异常处理程序由以下形式的函数应用来表示: handler(Start, End, Target, ClassName) 其中的参数分别是该处理程序所涵盖的指令范围的起始和结束位置、处理程序代码的第一条指令以及该处理程序旨在处理的异常类的名称。
如果异常处理程序的起始位置(Start)小于结束位置(End),并且存在一条指令其偏移量等于起始位置(Start),还存在一条指令其偏移量等于结束位置(End),并且该处理程序的异常类可赋值为 Throwable 类,则该异常处理程序是合法的。如果处理程序的类表项为 0,则该处理程序的异常类为 Throwable 类;否则,其异常类为处理程序中指定的类名。
如果处理程序位于 方法内部,且该处理程序所涵盖的指令中有一条是 方法的 invokespecial 指令,则还存在一个附加要求。在 在这种情况下,如果处理程序正在运行,就意味着正在构建的对象很可能存在问题,因此处理程序不能直接忽略该异常并让包含在 方法中的外部方法正常返回给调用者。因此,处理程序必须要么立即抛出异常给包含 方法的外部方法的调用者,要么无限循环。
handlersAreLegal(Environment) :-
exceptionHandlers(Environment, Handlers),
checklist(handlerIsLegal(Environment), Handlers).
handlerIsLegal(Environment, Handler) :-
Handler = handler(Start, End, Target, _),
Start < End,
allInstructions(Environment, Instructions),
member(instruction(Start, _), Instructions),
offsetStackFrame(Environment, Target, _),
instructionsIncludeEnd(Instructions, End),
currentClassLoader(Environment, CurrentLoader),
handlerExceptionClass(Handler, ExceptionClass, CurrentLoader),
isBootstrapLoader(BL),
isAssignable(ExceptionClass, class('java/lang/Throwable', BL)),
initHandlerIsLegal(Environment, Handler).
instructionsIncludeEnd(Instructions, End) :-
member(instruction(End, _), Instructions).
instructionsIncludeEnd(Instructions, End) :-
member(endOfCode(End), Instructions).
handlerExceptionClass(handler(_, _, _, 0),
class('java/lang/Throwable', BL), _) :-
isBootstrapLoader(BL).
handlerExceptionClass(handler(_, _, _, Name),
class(Name, L), L) :-
Name \= 0.
initHandlerIsLegal(Environment, Handler) :-
notInitHandler(Environment, Handler).
notInitHandler(Environment, Handler) :-
Environment = environment(_Class, Method, _, Instructions, _, _),
isNotInit(Method).
notInitHandler(Environment, Handler) :-
Environment = environment(_Class, Method, _, Instructions, _, _),
isInit(Method),
member(instruction(_, invokespecial(CP)), Instructions),
CP = method(MethodClassName, MethodName, Descriptor),
MethodName \= '<init>'.
initHandlerIsLegal(Environment, Handler) :-
isInitHandler(Environment, Handler),
sublist(isApplicableInstruction(Target), Instructions,
HandlerInstructions),
noAttemptToReturnNormally(HandlerInstructions).
isInitHandler(Environment, Handler) :-
Environment = environment(_Class, Method, _, Instructions, _, _),
isInit(Method).
member(instruction(_, invokespecial(CP)), Instructions),
CP = method(MethodClassName, '<init>', Descriptor).
isApplicableInstruction(HandlerStart, instruction(Offset, _)) :-
Offset >= HandlerStart.
noAttemptToReturnNormally(Instructions) :-
notMember(instruction(_, return), Instructions).
noAttemptToReturnNormally(Instructions) :-
member(instruction(_, athrow), Instructions).
现在让我们来探讨指令流和栈映射帧。 将指令和栈映射帧合并为一个单一的流涉及四种情况:
- 将一个空的栈映射帧与一系列指令合并,会得到原始的指令列表。说明。
mergeStackMapAndCode([], CodeList, CodeList).
- 若给定一个以指令在偏移量 Offset 处的类型状态开头的栈映射帧列表,以及从偏移量 Offset 开始的指令列表,则合并后的列表为栈映射帧列表的头部,接着是指令列表的头部,然后是两个列表尾部的合并。
mergeStackMapAndCode([stackMap(Offset, Map) | RestMap],
[instruction(Offset, Parse) | RestCode],
[stackMap(Offset, Map),instruction(Offset, Parse) | RestMerge]) :-
mergeStackMapAndCode(RestMap, RestCode, RestMerge)。
- 否则,若给定一个以指令在偏移量 OffsetM 处的类型状态开头的栈映射帧列表,以及从偏移量 OffsetP 开始的指令列表,则如果 OffsetP < OffsetM,合并后的列表由指令列表的头部组成,接着是栈映射帧列表与指令列表尾部的合并。清单。
mergeStackMapAndCode([stackMap(Offset, Map) | RestMap],
[instruction(Offset, Parse) | RestCode],
[stackMap(Offset, Map), instruction(Offset, Parse) | RestMerge]) :-
mergeStackMapAndCode(RestMap, RestCode, RestMerge).
- 否则,两个列表的合并是未定义的。由于指令列表的偏移量是单调递增的,所以两个列表的合并只有在每个栈映射帧的偏移量都有对应的指令偏移量,并且栈映射帧是单调递增的顺序的情况下才定义。
为了确定一个方法的合并流是否类型正确,我们首先推断该方法的初始类型状态。 方法的初始类型状态包括一个空的操作数栈以及从该方法和参数的类型推导出的局部变量类型,以及根据该方法是否为 方法而确定的相应标志。
methodInitialStackFrame(Class, Method, FrameSize, frame(Locals, [], Flags), ReturnType):
- methodDescriptor(Method, Descriptor)
- parseMethodDescriptor(Descriptor, RawArgs, ReturnType)
- expandTypeList(RawArgs, Args)
- methodInitialThisType(Class, Method, ThisList)
- flags(ThisList, Flags)
- append(ThisList, Args, ThisArgs)
- expandToLength(ThisArgs, FrameSize, top, Locals)
给定一个类型列表,以下语句块会生成一个列表,其中每个大小为 2 的类型都被替换为两个条目:一个用于自身,一个作为顶部条目。结果 然后对应于在 Java 虚拟机中将列表表示为 32 位字节流的方式。
expandTypeList([], []).
expandTypeList([Item | List], [Item | Result]) :-
sizeOf(Item, 1),
expandTypeList(List, Result).
expandTypeList([Item | List], [Item, top | Result]) :-
sizeOf(Item, 2),
expandTypeList(List, Result).
flags([uninitializedThis], [flagThisUninit]).
flags(X, []) :- X \= [uninitializedThis].
expandToLength(List, Size, _Filler, List) :-
length(List, Size).
expandToLength(List, Size, Filler, Result) :-
length(List, ListLength),
ListLength < Size,
Delta is Size - ListLength,
length(Extra, Delta),
checklist(=(Filler), Extra),
append(List, Extra, Result).
对于实例方法的初始类型状态,我们计算“this”的类型,并将其放入一个列表中。在 Object 类的 方法中,“this”的类型是 Object;“in”翻译成中文为“在”。 其他 <初始化> 方法中,此类型的初始状态为未初始化的 This;否则,实例方法中的此类型状态为类(N,L),其中 N 是包含该方法的类的名称,L 是其定义的类加载器。 对于静态方法的初始类型状态,这无关紧要,因此列表为空。
methodInitialThisType(_Class, Method, []) :-
methodAccessFlags(Method, AccessFlags),
member(static, AccessFlags),
methodName(Method, MethodName),
MethodName \= '<init>'.
methodInitialThisType(Class, Method, [This]) :-
methodAccessFlags(Method, AccessFlags),
notMember(static, AccessFlags),
instanceMethodInitialThisType(Class, Method, This).
instanceMethodInitialThisType(Class, Method, class('java/lang/Object', L)) :-
methodName(Method, '<init>'),
classDefiningLoader(Class, L),
isBootstrapLoader(L),
classClassName(Class, 'java/lang/Object').
instanceMethodInitialThisType(Class, Method, uninitializedThis) :-
methodName(Method, '<init>'),
classClassName(Class, ClassName),
classDefiningLoader(Class, CurrentLoader),
superclassChain(ClassName, CurrentLoader, Chain),
Chain \= [].
instanceMethodInitialThisType(Class, Method, class(ClassName, L)) :-
methodName(Method, MethodName),
MethodName \= '<init>',
classDefiningLoader(Class, L),
classClassName(Class, ClassName).
我们现在会根据方法的初始类型状态来判断合并后的代码流是否类型正确:
- 如果存在栈映射帧和传入的类型状态,那么该类型状态必须能够赋值给栈映射帧中的类型状态。然后,我们可以使用栈映射帧中给出的类型状态来对代码流的其余部分进行类型检查。
mergedCodeIsTypeSafe(Environment, [stackMap(Offset, MapFrame) | MoreCode],
frame(Locals, OperandStack, Flags)) :-
frameIsAssignable(frame(Locals, OperandStack, Flags), MapFrame),
mergedCodeIsTypeSafe(Envir
- 如果合并后的代码流相对于传入的类型状态 T 是类型安全的,那么它必须以相对于 T 类型安全的指令 I 开头,并且 I 必须满足其异常处理程序(见下文),并且流的尾部在执行 I 后所跟随的类型状态下是类型安全的。 NextStackFrame 表示会向下传递到后续指令的内容。对于 一条无条件分支指令,在执行“Goto”操作后会具有特殊值。 ExceptionStackFrame 表示传递给异常处理程序的内容。
mergedCodeIsTypeSafe(Environment, [instruction(Offset, Parse) | MoreCode],
frame(Locals, OperandStack, Flags)) :-
instructionIsTypeSafe(Parse, Environment, Offset,
frame(Locals, OperandStack, Flags),
NextStackFrame, ExceptionStackFrame),
instructionSatisfiesHandlers(Environment, Offset, ExceptionStackFrame),
mergedCodeIsTypeSafe(Environment, MoreCode, NextStackFrame).
- 在无条件分支(由“afterGoto”类型的输入状态指示)之后,如果有一个提供后续指令类型状态的栈映射帧,我们就可以继续使用该栈映射帧提供的类型状态来类型检查这些指令。
mergedCodeIsTypeSafe(Environment, [stackMap(Offset, MapFrame) | MoreCode],
afterGoto) :-
mergedCodeIsTypeSafe(Environment, MoreCode, MapFrame).
- 在无条件分支之后如果没有为它提供栈映射帧,则是非法的。
mergedCodeIsTypeSafe(_Environment, [instruction(_, _) | _MoreCode],
afterGoto) :-
write_ln('No stack frame after unconditional branch'),
fail.
- 如果代码末尾存在一个无条件分支,则停止执行。
mergedCodeIsTypeSafe(_Environment, [endOfCode(Offset)], afterGoto).
如果目标对象具有关联的栈帧“Frame”,而当前栈帧“StackFrame”能够被赋值给“Frame”,那么从目标对象分支出去的操作就是类型安全的。
etIsTypeSafe(Environment, StackFrame, Target) :- offsetStackFrame(Environment, Target, Frame), frameIsAssignable(StackFrame, Frame).
如果一条指令满足了适用于该指令的所有异常处理程序,那么这条指令就满足了其异常处理程序的要求。
instructionSatisfiesHandlers(Environment, Offset, ExceptionStackFrame) :-
exceptionHandlers(Environment, Handlers),
sublist(isApplicableHandler(Offset), Handlers, ApplicableHandlers),
checklist(instructionSatisfiesHandler(Environment, ExceptionStackFrame),
ApplicableHandlers).
如果指令的偏移量大于或等于异常处理程序范围的起始位置,并且小于该处理程序范围的结束位置,那么该异常处理程序就适用于该指令。
isApplicableHandler(Offset, handler(Start, End, _Target, _ClassName)) :-
Offset >= Start,
Offset < End
如果指令的传出类型状态为“异常栈帧”,并且该处理程序的目标(即处理程序代码的初始指令)在假设输入类型状态为 T 的情况下是类型安全的,那么该指令就满足该处理程序的要求。类型状态 T 是通过将操作数栈替换为仅包含处理程序异常类的栈而从“异常栈帧”中推导出来的。
instructionSatisfiesHandler(Environment, ExcStackFrame, Handler) :-
Handler = handler(_, _, Target, _),
currentClassLoader(Environment, CurrentLoader),
handlerExceptionClass(Handler, ExceptionClass, CurrentLoader),
/* The stack consists of just the exception. */
ExcStackFrame = frame(Locals, _, Flags),
TrueExcStackFrame = frame(Locals, [ ExceptionClass ], Flags),
operandStackHasLegalLength(Environment, TrueExcStackFrame),
targetIsTypeSafe(Environment, TrueExcStackFrame, Target).
##类型检查加载和存储指令
所有的加载指令都遵循一种通用模式,即改变指令所加载值的类型。
从局部变量索引处加载类型为 Type 的值是类型安全的,前提是该局部变量的类型为 ActualType,ActualType 能够赋值给 Type,并且将 ActualType 推入传入的操作数栈中是一种有效的类型转换,从而产生新的类型状态 NextStackFrame。执行加载指令后,类型状态将变为 NextStackFrame。
loadIsTypeSafe(Environment, Index, Type, StackFrame, NextStackFrame) :-
StackFrame = frame(Locals, _OperandStack, _Flags),
nth0(Index, Locals, ActualType),
isAssignable(ActualType, Type),
validTypeTransition(Environment, [], ActualType, StackFrame,
NextStackFrame).
所有的存储指令都遵循一种通用模式,即改变指令所存储值的类型。
一般来说,如果存储指令所引用的局部变量的类型是 Type 的超类型,并且操作数栈的顶部是 Type 的子类型(其中 Type 是该指令旨在存储的类型),那么该存储指令是类型安全的。更确切地说,如果可以弹出一个类型为 ActualType 的值(其中 ActualType 是可以赋值给 Type 的类型),那么该存储操作就是类型安全的。从操作数栈中取出“matches”类型(即为“Type”的子类型), 然后合法地将该类型赋值给局部变量 LIndex。
storeIsTypeSafe(_Environment, Index, Type,
frame(Locals, OperandStack, Flags),
frame(NextLocals, NextOperandStack, Flags)) :-
popMatchingType(OperandStack, Type, NextOperandStack, ActualType),
modifyLocalVariable(Index
给定局部变量 Locals,将 Index 修改为类型 Type 会导致局部变量列表变为 NewLocals。这些修改过程有些复杂,因为某些值(及其对应的类型)会占据两个局部变量。因此,修改 LN 可能需要修改 LN+1(因为类型会占据 N 和 N+1 的位置)或 LN-1(因为本地 N 曾是从本地 N-1 开始的两个字值/类型中的上半部分,所以本地 N-1 必须被无效化),或者两者兼而有之。这 以下将对此进行进一步描述。我们从 L0 开始并进行递增计数。
modifyLocalVariable(Index, Type, Locals, NewLocals) :- modifyLocalVariable(0, Index, Type, Locals, NewLocals).
给定局部变量列表的起始索引 I 的后缀 LocalsRest,将局部变量列表中索引为 I 的变量的类型修改为 Type,会导致局部变量列表的后缀变为 NextLocalsRest。
如果 I < Index - 1,则直接将输入内容复制到输出,并向前递归。如果 I = Index - 1,局部变量 I 的类型可能会改变。这可能发生在 LI 的类型大小为 2 的情况下。 一旦我们将 LI+1 设置为新的类型(并相应地赋值),LI 的类型/值将失效,因为其上半部分将被破坏。然后我们向前递归。
modifyLocalVariable(I, Index, Type,
[Locals1 | LocalsRest],
[Locals1 | NextLocalsRest] ) :-
I < Index - 1,
I1 is I + 1,
modifyLocalVariable(I1, Index, Type, LocalsRest, NextLocalsRest).
modifyLocalVariable(I, Index, Type,
[Locals1 | LocalsRest],
[NextLocals1 | NextLocalsRest] ) :-
I =:= Index - 1,
modifyPreIndexVariable(Locals1, NextLocals1),
modifyLocalVariable(Index, Index, Type, LocalsRest, NextLocalsRest).
当我们找到一个变量,并且它只占用一个单词时,我们就将其改为“Type”,这样就完成了。当我们找到一个变量,且它占用两个单词时,我们就将其类型改为“Type”,然后将下一个单词改为“top”。
modifyLocalVariable(Index, Index, Type,
[_ | LocalsRest], [Type | LocalsRest]) :-
sizeOf(Type, 1).
modifyLocalVariable(Index, Index, Type,
[_, _ | LocalsRest], [Type, top | LocalsRest]) :-
sizeOf(Type, 2).
我们将类型将要被修改的局部变量之前紧邻出现的那个局部变量称为“前置索引变量”。对于类型为“InputType”的前置索引变量,其未来的类型为“Result”。如果前置索引局部变量的类型“Type”的大小为 1,则其类型不会改变。如果前置索引局部变量的类型“Type”为 2,则需要将其两个字值中的较低半部分标记为不可用,通过将其类型设置为“top”来实现。
modifyPreIndexVariable(Type, Type) :- sizeOf(Type, 1).
modifyPreIndexVariable(Type, top) :- sizeOf(Type, 2).
对受保护成员的类型检查
所有访问成员的指令都必须遵循有关保护成员的规则。本节描述了与 JLS 第 6.6.2.1 节相对应的保护检查。 保护检查仅适用于当前类的超类中的保护成员。其他类中的保护成员将在解析阶段进行访问检查时被捕捉到。有四种情况:
- 如果一个类的名称不是任何超类的名称,那么它就不能是超类,因此可以安全地忽略它。
通过类型推断进行验证
如果一个类文件不包含“栈映射表”属性(该属性的版本号必须为 49.0 或更低版本),则必须通过类型推断来进行验证。
通过类型推断进行的验证过程
在链接过程中,验证器会针对类文件中的每个方法,通过对每个方法进行数据流分析来检查“代码”属性的代码数组。最终的译文:这个 验证器会确保在程序的任何特定点上,无论采用何种代码路径到达该点,以下所有条件均成立:
- 操作数栈的大小始终相同,并且包含相同类型的值。
- 除非已知局部变量包含适当类型的值,否则不得对其进行访问。
- 方法的调用使用适当的参数。
- 字段仅使用适当类型的值进行赋值。
- 所有操作码在操作数栈和局部变量数组中都有适当类型的参数。 出于效率考虑,某些原本可以通过验证器进行的测试会推迟到方法的代码首次实际调用时才进行。这样,验证器就能避免加载类文件,除非确实需要。 例如,如果一个方法调用另一个返回类 A 实例的方法,并且该实例仅被赋值给相同类型的字段,那么验证器不会去检查类 A 是否实际存在。然而,如果它被赋值给类型为 B 的字段,则必须加载 A 和 B 的定义以确保 A 是 B 的子类。
字节码验证器
每个方法的代码都会独立进行验证。首先,构成代码的字节被分解成一系列指令,并且每个指令起始位置在代码数组中的索引被存入一个数组中。然后,验证器会再次遍历代码并解析这些指令。在这次遍历过程中,会构建一个数据结构来保存方法中每个 Java 虚拟机指令的相关信息。对于每个指令的操作数(如果有)都会进行检查,以确保其有效。例如:
- 分支必须位于代码数组的范围内,且必须在方法内。
- 所有控制流指令的目标都是每条指令的起始位置。 对于宽指令而言,宽操作码被视为指令的起始部分,而由该宽指令修改的操作码则不被视为指令的起始部分。禁止在指令中间进行分支。
- 任何指令都不能访问或修改其方法所指定分配的本地变量的索引值大于或等于该方法所指定的本地变量数量。
- 所有对常量池的引用都必须指向适当类型的条目。 (例如,指令 getfield 必须引用一个字段。)
- 代码不能在指令中间结束。
- 执行不能超出代码的末尾。
- 对于每个异常处理程序,由该处理程序保护的代码的起始和结束点必须位于指令的开头,或者在结束点的情况下,必须紧接在代码的末尾之后。起始点必须在结束点之前。异常处理程序代码必须从有效的指令开始。并且它不能始于由宽指令修改的操作码处。
对于方法中的每一条指令,验证器都会记录该指令执行前操作数栈和局部变量数组的内容。对于操作数栈,需要知道栈的高度以及栈上每个值的类型。对于每个局部变量,需要知道该局部变量内容的类型,或者该局部变量包含不可用或未知的值(可能是未初始化的)。字节码验证器在确定操作数栈上的值类型时,无需区分整数类型(例如字节、短整型、字符)。
接下来,会初始化一个数据流分析器。对于方法的第一条指令,表示参数的局部变量最初包含由方法类型描述符所指示的类型值;操作数栈为空。所有其他的 局部变量包含了一个非法值。对于那些尚未检查的其他指令,关于操作数栈或局部变量的信息均不可获取。 最后,运行数据流分析器。对于每一条指令,一个“已更改”位表示该指令是否需要进行检查。最初,只有第一条指令的“已更改”位被设置。数据流分析器执行以下循环:
-
选择一个“已更改”位被设置的 Java 虚拟机指令。如果不再有指令的“已更改”位处于设置状态,则表示该方法已成功通过验证。否则,将所选指令的“已更改”位设置为未被设置状态。
-
通过以下步骤来模拟指令对操作数栈和局部变量数组的影响: 如果指令使用了操作数栈中的值,则要确保栈中有足够的值,并且栈顶的值类型要恰当。否则,验证将失败。
- 如果指令使用了局部变量,则要确保指定的局部变量包含适当类型的值。否则,验证将失败。
- 如果指令将值推送到操作数栈中,则要确保操作数栈中有足够的空间容纳新的值。将指定的类型添加到模型化的操作数栈的顶部。
- 如果指令修改了局部变量,则记录该局部变量现在包含新的类型。3. 确定能够遵循当前指令的后续指令。继任者 指令可以是以下其中一种:
- 如果当前指令不是无条件控制转移指令(例如,goto、return 或 throw),则为下一条指令。若存在“脱离”方法末尾指令的可能性,则验证将失败。
- 条件或无条件分支或切换的目标。
- 该指令的任何异常处理程序。
- 在当前指令执行完毕后,将操作数栈的状态和局部变量数组合并到每个后续指令中。 在控制转移至异常处理程序的特殊情况中,操作数栈被设置为包含由异常处理程序信息所指示的异常类型的单个对象。必须为这个单个值留出足够的空间在操作数栈上,就好像有一条指令已经将其压入栈中一样。
- 如果这是首次访问后续指令,则记录在步骤 2 和 3 中计算出的操作数栈和局部变量值是执行后续指令之前操作数栈和局部变量数组的状态。为后续指令设置“已更改”位。
- 如果已经见过该后续指令,则将步骤 2 和 3 中计算出的操作数栈和局部变量值合并到已经存在的值中。那里。如果值有任何修改,则设置“已更改”标志。
- 继续执行步骤 1。 要合并两个操作数栈,每个栈上的值数量必须相同。 然后,对两个栈上的对应值进行比较,并在合并的栈中计算出相应的值,具体如下:
- 如果一个值是基本类型,则对应的值必须是相同的基本类型。合并后的值就是该基本类型。
- 如果一个值是非数组引用类型,则对应的值必须是引用类型(数组或非数组)。合并后的值是一个指向两个引用类型共同的超类实例的引用。(由于类型 Object 是所有类、接口和数组类型的超类,所以总是存在这样的引用类型。) 例如,Object 和 String 可以合并;结果是 Object。同样地, 对象和字符串数组可以合并;合并后的结果仍然是对象。例如,对象数组和字符串数组可以合并,结果是对象数组。可拷贝数组和字符串数组可以合并,或者 java.io.Serializable 数组和字符串数组也可以合并;结果分别是可拷贝数组和 java.io.Serializable 数组。
- 如果对应的值都是数组引用类型,那么会检查它们的维度。如果数组类型具有相同的维度,那么合并后的值是一个指向具有两个数组类型共同超类的数组类型的实例的引用。(如果任何一个或两个数组类型具有基本元素类型,则使用 Object 作为元素类型。)如果数组类型的维度不同,那么合并后的值是一个指向具有两个数组类型较小维度的数组类型的实例的引用;如果较小的数组类型是可拷贝或 java.io.Serializable,则元素类型为 Cloneable 或 java.io.Serializable,否则为 Object。
例如,对象数组和字符串数组可以合并;结果是对象数组。可拷贝数组和字符串数组可以合并,或者 java.io.Serializable 数组和字符串数组也可以合并;结果分别是可拷贝数组和 java.io.Serializable 数组。甚至 int 数组和字符串数组也可以合并;结果是对象数组,因为在计算第一个共同超类时使用的是 Object 而不是 int。由于数组类型可以具有不同的维度,所以 Object[] 和 String[][] 可以合并,或者 Object[][] 和 String[];在这两种情况下,结果都是 Object[]。Cloneable[] 和 String[][] 也可以合并;结果是 Cloneable[]。最后, 可克隆数组[][] 和字符串[] 可以合并;合并后的结果为对象[]。
如果操作数栈无法合并,则方法验证会失败。 要合并两个局部变量数组的状态,需要对相应的局部变量进行比较。 合并后的局部变量的值是使用上述规则计算得出的,但允许相应的值为不同的基本类型。在这种情况下,验证器会记录合并后的局部变量包含一个不可用的值。
如果数据流分析器在某个方法上运行时并未报告验证失败,那么该方法已由类文件验证器成功通过验证。
某些指令和数据类型会增加数据流分析器的复杂性。现在我们将更详细地探讨这些内容。
类型“long”和“double”的值
长整型和双整型的值在验证过程中会得到特殊处理流程。
每当将类型为 long 或 double 的值移动到索引为 n 的局部变量中时,索引 n + 1 会被特别标记,以表明它已被索引为 n 的值所占用,不得再用作局部变量的索引。之前位于索引 n + 1 处的任何值都将无法再使用。
每当将值移动到索引为 n 的局部变量中时,都会检查索引 n - 1 是否为类型为 long 或 double 的值的索引。如果是,则将索引为 n - 1 的局部变量更改为表明它现在包含一个不可用的值。由于 索引为 n 的局部变量已被覆盖,索引为 n - 1 的局部变量无法表示长整型或双精度型的值。
在操作数栈上处理长整型或双精度型的值会更简单;“the” 验证器将它们视为栈中的单个值。例如,对于 dadd 操作码(用于将两个双精度值相加)的验证代码会检查栈的顶部两个元素都是双精度类型。在计算操作数栈长度时,长整型和双精度型的值长度为两。
操作数栈操作的无类型指令必须将长整型和双精度型的值视为原子(不可分割的)。例如,如果栈顶值是双精度型,并且遇到诸如 pop 或 dup 这样的指令,验证器会报告失败。应使用 pop2 或 dup2 这样的指令代替。
实例初始化方法与新创建的对象
... new myClass(i, j, k); ...
可以通过以下方式实现:
...
new #1 // Allocate uninitialized space for myClass
dup // Duplicate object on the operand stack
iload_1 // Push i
iload_2 // Push j
iload_3 // Push k
invokespecial #5 // Invoke myClass.<init>
...
此指令序列将新创建并初始化的对象留在操作数栈顶。(有关编译为 Java 虚拟机指令集的更多示例,请参见第 3 节(为 Java 虚拟机编译)。
对于类 myClass 的实例初始化方法(第 2.9 节),新未初始化的对象作为其 this 参数出现在局部变量 0 中。在该方法调用 myClass 或其直接超类的另一个实例初始化方法之前,此方法对 this 唯一可执行的操作是为 myClass 内声明的字段赋值。
在对实例方法进行数据流分析时,验证器将局部变量 0 初始化为包含当前类的对象,或者对于实例初始化方法,局部变量 0 包含一个特殊类型,表示未初始化的对象。在对 this 对象调用适当的实例初始化方法(来自当前类或其直接超类)之后,验证器模型中的操作数栈和局部变量数组中所有此特殊类型的出现都将被当前类类型替换。验证器拒绝使用 new 操作符创建未初始化对象的代码。在对象初始化之前对其进行访问或对其进行多次初始化。此外,它还确保方法的每次正常返回都调用了该方法所在类或直接超类中的实例初始化方法。同样,由于 Java 虚拟机指令 new 的执行结果,会在验证器的操作数栈模型中创建并压入一种特殊类型。这种特殊类型表明创建类实例的指令以及所创建的未初始化类实例的类型。当在未初始化类实例上调用该类中声明的实例初始化方法时,所有出现的特殊类型都将被替换为该类实例的预期类型。这种类型的更改可能会随着数据流分析的进行传播到后续指令。
由于在操作数栈上可能同时存在多个尚未初始化的类实例,因此需要将指令编号存储为特殊类型的一部分。例如,实现以下代码的 Java 虚拟机指令序列:
new InputStream(new Foo(), new InputStream("foo"))在操作数栈上可能同时有两个未初始化的 InputStream 实例。
当在类实例上调用实例初始化方法时,只有操作数栈或局部变量数组中与该类实例为同一对象的特殊类型的那些出现会被替换。
异常和finally
为了实现 try-finally 结构,生成版本号为 50.0 或更低的类文件的 Java 编程语言编译器可以使用异常处理设施以及两条特殊指令:jsr(“跳转到子程序”)和 ret(“从子程序返回”)。finally 子句在 Java 虚拟机代码中被编译为其方法内的一个子程序,这与异常处理程序的代码非常相似。当执行调用该子程序的 jsr 指令时,它会将返回地址(即正在执行的 jsr 指令之后的指令地址)作为 returnAddress 类型的值压入操作数栈。子程序的代码将返回地址存储在一个局部变量中。在子程序的末尾,ret 指令从局部变量中获取返回地址,并将控制权转移到返回地址处的指令。
控制可以以几种不同的方式转移到 finally 子句(可以调用 finally 子程序)。如果 try 子句正常完成,则在计算下一个表达式之前通过 jsr 指令调用 finally 子程序。在 try 子句内部的 break 或 continue 语句若将控制权转移到 try 子句之外,则会先执行跳转到 finally 子句代码的 jsr 操作。如果 try 子句执行了 return,编译后的代码会按如下步骤操作:1. 将返回值(如果有)保存到一个局部变量中。2. 执行一个 jsr 指令跳转到 finally 子句的代码。3. 从 finally 子句返回时,返回保存在局部变量中的值。
编译器会设置一个特殊的异常处理程序,用于捕获 try 子句中抛出的任何异常。如果在 try 子句中抛出异常,此异常处理程序将执行以下操作:1. 将异常保存到一个局部变量中。2. 执行到 finally 子句的 jsr 操作。3. 从 finally 子句返回时,重新抛出异常。 有关 try-finally 结构实现的更多信息,请参阅第 3.13 节。
finally 子句的代码给验证器带来了一个特殊问题。通常,如果特定指令可以通过多条路径到达,并且特定局部变量通过这些多条路径包含不兼容的值,那么局部变量就存在验证问题。 变量变得不可用。然而,finally 子句可能会从几个不同的地方被调用,从而产生几种不同的情况:
- 从异常处理程序调用时,可能会有一个局部变量包含异常。
- 为实现返回而调用时,可能会有一个局部变量包含返回值。
- 从 try 子句底部调用时,该局部变量可能包含不确定的值。
finally 子句本身的代码可能通过验证,但在完成对 ret 指令所有后续指令的更新后,验证器会注意到异常处理程序期望包含异常的局部变量,或者返回代码期望包含返回值的局部变量,现在包含的是不确定的值。 包含 finally 子句的代码验证较为复杂。基本思路如下:
- 每条指令都跟踪到达该指令所需的 jsr 目标列表。对于大多数代码,此列表为空。对于 finally 子句代码内的指令,列表长度为 1。对于多重嵌套的 finally 代码(极为罕见!),列表长度可能超过 1。• 对于每条指令以及到达该指令所需的每个 jsr 指令,都会维护一个位向量,其中包含自 jsr 指令执行以来访问或修改的所有局部变量。
- 在执行实现从子程序返回的 ret 指令时,只能有一个可能的子程序可以返回到该指令。两个不同的子程序不能“合并”到单个 ret 指令的执行中。
- 为了对 ret 指令执行数据流分析,会使用一个特殊的过程。由于验证器知道该指令必须从哪个子程序返回,因此它可以找到调用该子程序的所有 jsr 指令,并将 ret 指令执行时的操作数栈和局部变量数组的状态合并到 jsr 指令之后的指令的操作数栈和局部变量数组中。
合并使用局部变量的特殊值集:
- 对于位向量(如上构建)指示已被子程序访问或修改的任何局部变量,在 ret 时使用该局部变量的类型。
- 对于其他局部变量,在 jsr 指令之前使用该局部变量的类型。
Java虚拟机的限制
以下 Java 虚拟机的局限性隐含于类文件格式之中:
- 每个类或接口的常量池(constant pool)的条目数量被 ClassFile 结构中的 16 位 constant_pool_count 字段所限制,最多为 65535 个(§4.1)。这实际上对单个类或接口的总体复杂性设定了一个内部限制。
- 类或接口可声明的字段数量被 ClassFile 结构中的 fields_count 项的大小所限制,最多为 65535 个(§4.1)。 请注意,ClassFile 结构中 fields_count 项的值不包括从超类或超接口继承的字段。
- 类或接口可声明的方法数量被 ClassFile 结构中的 methods_count 项的大小所限制,最多为 65535 个(§4.1)。 请注意,ClassFile 结构中 methods_count 项的值不包括从超类或超接口继承的方法。
- 类或接口的直接超接口数量被 ClassFile 结构中的 interfaces_count 项的大小所限制,最多为 65535 个(§4.1)。
- 框架的局部变量数组中的最大局部变量数量 当调用一个方法时创建的局部变量数量(第 2.6 节)受到代码属性(第 4.7.3 节)中“代码方法”的“max_locals”项大小的限制,该值为 65535。同时,还受到 Java 虚拟机指令集的 16 位局部变量索引的限制。 请注意,长整型和双精度型的值各自被视为预留两个局部变量,并为“max_locals”值贡献两个单位,因此使用这些类型的局部变量会进一步降低此限制。
- 在一个帧中的操作数栈大小(第 2.6 节)受到代码属性(第 4.7.3 节)中“max_stack”字段的限制,该值为 65535 个值。 请注意,长整型和双精度型的值各自被视为为“max_stack”值贡献两个单位,因此在操作数栈上使用这些类型的值会进一步降低此限制。
- 方法参数的数量由方法描述符(第 4.3.3 节)的定义所限制,其中在实例或接口方法调用的情况下,此限制包括一个单位用于此。 请注意,方法描述符是根据方法参数长度的概念来定义的,在这种情况下,长整型或双精度型的参数贡献两个单位到该限制中。长度,因此这些类型的参数进一步降低了该限制。
- 字段和方法名称、字段和方法描述符以及其他常量字符串值(包括由 ConstantValue(§4.7.2)属性引用的那些)的长度被 CONSTANT_Utf8_info 结构中的 16 位无符号长度项(§4.4.7)限制为 65535 个字符。 请注意,该限制是针对编码中的字节数,而非编码字符的数量。UTF-8 对某些字符使用两字节或三字节进行编码。因此,包含多字节字符的字符串受到进一步的限制。
- 数组的维度数量被 multianewarray 指令的维度大小操作码以及对 multianewarray、newarray 和 anewarray 指令所施加的约束(§4.9.1、§4.9.2)限制为 255。