字节码中的类型

2022-09-04 07:09:45

我已经在(Java)字节码上工作了一段时间,但是,我从来没有想过要问为什么输入一些指令?我知道在ADD操作中,我们需要区分整数加法和FP加法(这就是为什么我们有IAD和FADD)。但是,为什么我们需要区分ISTORE和FSTORE?它们都涉及完全相同的操作,即将32位从堆栈移动到局部变量位置?

我能想到的唯一答案是类型安全,以防止这种情况:(ILOAD,ILOAD,FADD)。但是,我相信类型安全已经在Java语言级别强制执行。好的,类文件格式不直接与Java耦合,那么这是一种对不支持它的语言强制实施类型安全的方法吗?有什么想法吗?谢谢。

编辑:跟进里迪的答案。我写了这个最小的程序:

public static void main(String args[])
{
    int x = 1;
}

它编译为:

iconst_1
istore_1
return

使用字节码编辑器,我更改了第二条指令:

iconst_1
fstore_1
return

它返回了一个java.lang.VerifyError:期望在堆栈上找到浮点数。

我想知道,如果在堆栈上没有关于类型的信息,只有位,FSTORE指令怎么知道它处理的是int而不是浮点数?

注意:我找不到更好的标题来回答这个问题。随意改进它。


答案 1

键入这些说明是为了确保程序是类型安全的。加载类时,虚拟机对字节码执行验证,以确保例如,浮点数不会作为参数传递给需要整数的方法。此静态验证要求验证程序可以确定任何给定执行路径的堆栈上值的类型和数量。加载和存储指令需要 type 标记,因为堆栈帧中的局部变量不是类型化的(即,您可以将 istore 存储到局部变量,然后再存储到相同的位置)。指令上的类型标记允许验证程序知道每个局部变量中存储的值类型。

验证程序会查看方法中的每个操作码,并在执行每个操作码后跟踪堆栈和局部变量中的类型。你是对的,这是另一种形式的类型检查,并且确实复制了java编译器所做的一些检查。验证步骤可防止加载任何会导致 VM 执行非法指令的代码,并确保 Java 平台的安全属性,而不会在每次操作之前检查类型时造成较大的运行时损失。每次执行方法时,对每个操作码的运行时类型检查都会影响性能,但静态验证仅在加载类时执行一次。

案例1:

Instruction             Verification    Stack Types            Local Variable Types 
----------------------- --------------- ---------------------- ----------------------- 
<method entry>          OK              []                     1: none
iconst_1                OK              [int]                  1: none
istore_1                OK              []                     1: int
return                  OK              []                     1: int

案例2:

Instruction             Verification    Stack Types            Local Variable Types 
----------------------- --------------- ---------------------- ----------------------- 
<method entry>          OK              []                     1: none
iconst_1                OK              [int]                  1: none
fstore_1                Error: Expecting to find float on stack

给出错误是因为验证者知道fstore_1期望堆栈上有浮点数,但执行先前指令的结果会在堆栈上留下一个int。

这种验证是在不执行操作码的情况下完成的,而是通过查看指令的类型来完成的,就像java编译器在编写时给出错误一样。编译器不必运行程序即可知道这是一个字符串,不能强制转换为 。(Integer)"abcd""abcd"Integer


答案 2

用我最好的猜测来回答你的第一个问题:这些字节码是不同的,因为它们可能需要不同的实现。例如,特定体系结构可能在主堆栈上保留整数操作数,但在硬件寄存器中保留浮点操作数。

为了回答第二个问题,VerifyError 是在加载类时引发的,而不是在执行类时。此处描述了验证过程;注意通过#3。


推荐