字节码中的类型

Posted

技术标签:

【中文标题】字节码中的类型【英文标题】:Types in Bytecode 【发布时间】:2011-02-07 22:26:31 【问题描述】:

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

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

编辑: 跟进 Reedy 的回答。我写了这个最小的程序:

public static void main(String args[])

    int x = 1;

编译成:

iconst_1
istore_1
return

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

iconst_1
fstore_1
return

它返回了一个 java.lang.VerifyError: Expecting to find float on stack。

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

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

【问题讨论】:

【参考方案1】:

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

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

案例一:

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。

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

【讨论】:

感谢您的回答。因此,验证者在执行类之前会进行一些数据流分析以检测此类错误。有趣的学习:)【参考方案2】:

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

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

【讨论】:

链接+1。感谢您提醒我,VerifyError 是在加载时而不是在运行时抛出的。【参考方案3】:

Geoff Reedy 在他的回答中解释了加载类时验证器会做什么。我只想补充一点,您可以使用 JVM 参数禁用验证程序。不推荐!

对于您的示例程序(带有 iconst 和 fstore),在禁用验证的情况下运行的结果是一个 VM 错误,该错误会停止 JVM,并显示以下消息:

=============== DEBUG MESSAGE: illegal bytecode sequence - method not verified ================

#
# An unexpected error has been detected by HotSpot Virtual Machine:
#
#  EXCEPTION_PRIV_INSTRUCTION (0xc0000096) at pc=0x00a82571, pid=2496, tid=3408
#
# Java VM: Java HotSpot(TM) Client VM (1.5.0_15-b04 mixed mode, sharing)
# Problematic frame:
# j  BytecodeMismatch.main([Ljava/lang/String;)V+0
#
...

【讨论】:

【参考方案4】:

所有字节码必须通过上述静态数据流分析证明是类型安全的。然而,这并不能真正解释为什么像 _store 这样的指令有不同的类型,因为类型可以从堆栈上的值的类型推断出来。事实上,有一些指令,如 pop、dup 和 swap,它们正是这样做的,并且对多种类型进行操作。为什么有些指令是输入的,有些不是,这只能由 Java 的原始开发者来解释。

【讨论】:

以上是关于字节码中的类型的主要内容,如果未能解决你的问题,请参考以下文章

反射机制

java 反编译

如何识别Java字节码中的覆盖方法?

Android 逆向启动 DEX 字节码中的 Activity 组件 ( 在 PathClassLoader 和 BootClassLoader 之间插入 DexClassLoader )

Android 逆向启动 DEX 字节码中的 Activity 组件 ( 替换 LoadedApk 中的类加载器 | 加载 DEX 文件中的 Activity 类并启动成功 )

Android 逆向启动 DEX 字节码中的 Activity 组件 ( 使用 DexClassLoader 获取组件类失败 | 失败原因分析 | 自定义类加载器没有加载组件类的权限 )