- 需要说明的是,这是一个关于字节码验证器的规范的问题,特别是在处理不是由javac生成的字节码时。*
操作数堆栈可以以非确定性的方式修改吗?换句话说,是否有可能让条件代码根据仅在运行时才知道的信息,将堆栈转换为不同的状态(大小,堆栈顶部的类型......)?
可能不验证的方法示例(伪代码)
float f(boolean b) {
if (b) {
push 1.0 // float
} else {
push 2L // long
}
dup // generic instructions that work for both floats and longs
pop
if (b) {
return // return the float
} else {
l2f // convert the long to float and return it
return
}
}
个字符
关于第二个例子,我知道操作数堆栈是有界的,所以拥有任意大的堆栈本身就已经是个问题了。
虽然这两种方法在语义上都是正确的,但我认为在非平凡的情况下,在恒定的时间内检查它们的正确性是不可能的。我闻到了一点停机问题/赖斯定理的味道。
当Java运行时不能证明字节码的正确性时,它也可以回退到解释器,但据我所知,在Java中,字节码总是被主动检查(在链接时)。
1条答案
按热度按时间fumotvh31#
简而言之,操作数堆栈必须是确定性的。
规范中有两个地方解决了您的大部分问题:
§2.6.2.操作数堆栈:
来自操作数堆栈的值必须以适合其类型的方式进行操作。例如,不可能推送两个
int
值并随后将其视为long
,或者推送两个float
值并随后使用 iadd 指令将其相加。少量的Java虚拟机指令(dup 指令(§dup)和 swap(§swap))作为原始值在运行时数据区域上操作,而不考虑它们的特定类型;这些指令以这样的方式定义,即它们不能用于修改或分解各个值。这些对操作数堆栈操作的限制是通过class
文件验证(第4.10节)强制执行的。在任何时间点,操作数堆栈都具有相关联的深度,其中
long
或double
类型的值向深度贡献两个单位,并且任何其他类型的值贡献一个单位。注意句子“These restrictions on operand stack manipulation are enforced through class file verification”,告诉您验证是强制性的。
此外,
§4.9.2.结构限制:
...
因此,没有办法推送或弹出动态数量的元素。
请注意,从Java 6开始,分支合并点必须有一个堆栈Map框架,描述此时的局部变量和操作数堆栈条目,并且所有代码路径都根据此描述进行验证。这种静态描述还在技术层面上排除了动态堆栈值。
进一步注意,这种帧中的类型描述可以表示xlm 6 nlx、xlm 7 nlx、xlm 8 nlx、xlm 9 nlx和引用类型,以及“xlm 10 nlx”,即验证类型系统的根,其表示不可用的条目,而不是“类型1”或“类型2”。
因此,当规范说“dup可以处理第1类计算类型的值”时,它是说它可以同等地处理
int
,float
和引用类型的缩写,但它总是在特定的代码位置。不存在它可能遇到非特定“类型1”的情况。虽然可以有两个分支,一个推送
float
,另一个推送int
,将两者合并到“top”类型的条目中,但不可能以任何方式使用它,甚至不能使用pop
删除值,因为值的类型“top”既不是“type 1”也不是“type 2”。后续代码唯一能做的就是忽略这个值,直到通过
return
或throw
退出该方法。在Java 6之前,没有堆栈Map,分支必须由验证器根据以下规则合并:
若要合并两个操作数堆栈,每个堆栈上的值的数量必须相同。然后,比较两个堆栈上的对应值,并计算合并堆栈上的值,如下所示:
如果无法合并操作数堆栈,则方法验证失败。
请注意,没有合并不同基元类型或合并基元类型和引用类型的规则,甚至没有像更新的类文件那样合并为“top”。
换句话说,在旧的类文件中,上面描述的两个代码路径根本不能合并。