为什么 JSR/RET 不推荐使用 Java 字节码?

发布于 2024-11-05 09:53:43 字数 103 浏览 6 评论 0原文

有谁知道为什么 JSR/RET 字节码对在 Java 6 中被弃用?

我在网上找到的唯一有意义的解释是,它们使运行时的代码分析变得更加困难且执行速度更慢。有谁知道另一个原因吗?

Does anyone know why the JSR/RET bytecode pair is deprecated in Java 6?

The only meaningful explanation I found on the net was that they made code analysis by the runtime harder and slower to perform. Does anyone know another reason?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

朕就是辣么酷 2024-11-12 09:53:44

JSR 和 RET 使字节码验证变得更加困难,因为放宽了一些正常的字节码约束(例如在进入 JSR 时具有一致的堆栈形状)。优点非常小(在某些情况下可能会稍小一些),并且验证器处理奇怪的 JSR/RET 模式(以及潜在的安全漏洞,以及完整验证的相关运行时成本)的持续困难使其成为一个无用的功能继续拥有。

堆栈映射和由于数据而启用的轻量级验证器在类加载过程中取得了巨大的性能优势,而且不牺牲安全性。

JSR and RET make bytecode verification a lot more difficult than it might otherwise be due to the relaxation of some normal bytecode constraints (such as having a consistent stack shape on entry to a JSR). The upside is very minor (potentially slightly smaller methods in some cases) and the continuing difficulties in the verifier dealing with odd JSR/RET patterns (and potential security vulnerabilities, and the associated runtime cost of full verification) make it a non-useful feature to continue having.

Stack maps and the lighter-weight verifier that is enabled as a result of the data are a big performance win during class loading for no sacrifice in safety.

花辞树 2024-11-12 09:53:44

使用它们来混淆字节码的人解释了原因:

在处理输入问题时,jsr - ret 构造特别难以处理,因为每个子例程
可以从多个地方调用,需要合并类型信息,因此更加保守
估计[需要产生]。此外,反编译器通常会期望为每个 jsr 找到特定的 ret

最后一句仅与混淆器相关(例如基于 soot 的 JBCO),它甚至不放置 ret 而是 pop 返回地址,模拟 goto。大约 15 年后,这对于某些“现代”反编译器仍然足够有效:

org.benf.cfr.reader.util.ConfusedCFRException: Missing node tying up JSR block

撇开这个(相对简单的)技巧不谈,引文的第一部分说(即使)如果“按最初设计的”使用 jsrs导致(数据流)分析速度减慢。字节码验证器是一个数据流分析器,请参阅 Leroy 进行深入讨论——我可能应该在命名之前停下来 抽象解释在这里,尽管这也[概念上]涉及字节码验证......

第一个 JVM 字节码验证算法是由 Sun 的 Gosling 和 Yellin 提出的 [...]。
几乎所有现有的字节码验证器都实现了该算法。可以概括为数据流
分析应用于虚拟机的类型级抽象解释。

但是,在 Leroy 中更详细地讲,jsr 引入了这种复杂性:

虽然可以使用任何必须别名分析,但 Sun 的验证程序使用相当简单的分析,
而未初始化的对象由 new 的位置(程序计数器值)标识
创建它的指令。更准确地说,类型代数由类型 Cp 丰富,表示
由 PC p 上的 new 指令创建的 C 类的未初始化实例。 [...]

子例程[意思是jsr-ret]使对象初始化的验证变得复杂。作为
Freund 和 Mitchell [15] 发现,子例程中的新指令可以导致不同的结果
未初始化的对象具有相同的静态类型 Cp,从而欺骗 Sun 的验证者相信
在调用其中之一的初始化方法后,所有这些都将被初始化。解决方案
是在子例程调用中禁止或设置为“top”[=unialized]所有具有 Cp 类型的寄存器和堆栈位置。

Coglio [9] 观察到 Sun 对向后分支的限制以及 Freund 和 Mitchell 的限制
对于基于单变量数据流分析的字节码验证器来说,对 new 的限制是不必要的。
更准确地说,[9,第 5.8.2 节] 表明,在没有子例程的情况下,寄存器或堆栈
location 不能在包含new C 指令的程序点 p 之前具有类型 Cp。
因此,唯一的程序指向堆栈类型或寄存器类型中未初始化的对象类型
必须禁止(或变成“顶级”)的是子例程调用。

相关引用如下:

[15] 斯蒂芬·N·弗罗因德 (Stephen N. Freund) 和约翰·C·米切尔 (John C. Mitchell)。 Java 字节码语言中用于对象初始化的类型系统。 ACM 编程语言和系统汇刊,21(6):1196–1250, 1999。

[9] 亚历山德罗·科利奥。完善Java字节码验证的官方规范。并发与计算:实践与经验,15(2):155–179, 2003。

后者的结论 (2003) 论文

正如上述讨论所证明的,子例程是字节码复杂性的主要来源
验证。尽管第 5.9.5 节中描述的方法非常简单,但它会影响
整个验证算法,需要使用类型分配集。如果子程序不存在,
单一类型赋值就足够了;此外,与对象初始化的有害交互
第 5.8.3 节中描述的情况不会发生。

Java字节码中引入了子例程,以避免最终编译时出现代码重复
块,但人们发现普通代码中实际上节省的空间非常少[21,27]。这是
人们普遍认为,一开始就不引入子例程可能会更好。
虽然未来的 Java 编译器可以简单地避免生成子例程,但未来版本的
JVM 必须能够接受先前编译的可能包含子例程的代码。换句话说,
向后兼容的需要阻止了子例程的完全消除。

Coglio 的后续 (2004) 论文 指出(第 666 页)大多数 Java 字节码验证器实现都违反了 JVM 规范,并拒绝了一些涉及子例程的有效(规范方面)程序。

勒罗伊的另一个花絮/批评:

虽然在实践中有效,Sun 的子例程验证方法提出了一个具有挑战性的问题:确定子例程结构很困难。子程序不仅在语法上没有
分隔,但返回地址存储在通用寄存器中,而不是存储在子例程特定的堆栈上,这使得跟踪返回地址和匹配 ret/jsr 对更加困难。
为了方便确定子程序结构,JVM 规范对正确的 JVM 代码规定了许多限制,例如“两个不同的子程序不能‘合并’它们的
执行到单个 ret 指令”[33,第 4.9.6 节]。这些限制似乎相当临时
并且特定于 Sun 验证程序使用的特定子例程标记算法。此外,
JVM 规范中给出的子例程标记的描述非常不正式且不完整。

Coglio 2004 年的论文还指出,CLR 在其 VM 操作码级别有一个内置的 endfinally,这避免了 jsr/ret 的一些问题。看起来这是因为您 不能通过其他指令自由地跳入或跳出 CLR 中的此类块,而您可以跳入/跳出没有特定/强制边界的“子例程”在 JVM 中也是如此,这使事情变得复杂。

The people who use them to obfuscate bytecode explain why:

The jsr - ret construct is particularly difficult to handle when dealing with typing issues because each subroutine
can be called from multiple places, requiring that type information be merged and therefore a more conservative
estimate [need be produced]. Also, decompilers will usually expect to find a specific ret for every jsr.

The last sentence is only relevant for obfuscators (like the soot-based JBCO) which don't even put a ret but pop the return address, emulating a goto. That's still effective enough ~15 years later against some 'modern' decompilers:

org.benf.cfr.reader.util.ConfusedCFRException: Missing node tying up JSR block

That (relatively simple) trick aside, the first part of the quote says that (even) if used 'as originally designed' jsrs cause (dataflow) analysis slowdowns. A bytecode verifier is a dataflow analyzer, see Leroy for an in-depth discussion--I should probably stop before I namedrop abstract interpretation here, although that's also [conceptually] involved in bytecode verification...

The first JVM bytecode verification algorithm is due to Gosling and Yellin at Sun [...].
Almost all existing bytecode verifiers implement this algorithm. It can be summarized as a dataflow
analysis applied to a type-level abstract interpretation of the virtual machine.

But, in more detail in Leroy, there's this complication that jsr introduced:

While any must-alias analysis can be used, Sun’s verifier uses a fairly simple analysis,
whereas an uninitialized object is identified by the position (program counter value) of the new
instruction that created it. More precisely, the type algebra is enriched by the types Cp denoting
an uninitialized instance of class C created by a new instruction at PC p. [...]

Subroutines [meaning jsr-ret] complicate the verification of object initialization. As
discovered by Freund and Mitchell [15], a new instruction inside a subroutine can result in distinct
uninitialized objects having the same static type Cp, thus fooling Sun’s verifier into believing that
all of them become initialized after invoking an initialization method on one of them
. The solution
is to prohibit or set to 'top' [=unitialized] all registers and stack locations that have type Cp across a subroutine call.

Coglio [9] observes that Sun’s restriction on backward branches as well as Freund and Mitchell’s
restriction on new are unnecessary for a bytecode verifier based on monovariant dataflow analysis.
More precisely, [9, section 5.8.2] shows that, in the absence of subroutines, a register or stack
location cannot have the type Cp just before a program point p containing a new C instruction.
Thus, the only program points where uninitialized object types in stack types or register types
must be prohibited (or turned into 'top') are subroutine calls.

The relevant citations there being:

[15] Stephen N. Freund and John C. Mitchell. A type system for object initialization in the Java bytecode language. ACM Transactions on Programming Languages and Systems, 21(6):1196–1250, 1999.

[9] Alessandro Coglio. Improving the official specification of Java bytecode verification. Concurrency and Computation: Practice and Experience, 15(2):155–179, 2003.

The conclusions of the latter (2003) paper:

As evidenced by the above discussions, subroutines are a major source of complexity for bytecode
verification
. Even though the approach described in Section 5.9.5 is quite simple, it impacts on the
whole verification algorithm, requiring the use of sets of type assignments. If subroutines did not exist,
single type assignments would be sufficient;
moreover, the harmful interaction with object initialization
described in Section 5.8.3 would not happen.

Subroutines were introduced in Java bytecode to avoid code duplication when compiling finally
blocks, but it has been found that very little space is actually saved in mundane code [21,27]. It is
widely conjectured that it might have been better not to introduce subroutines in the first place.
While future Java compilers could simply avoid the generation of subroutines, future versions of the
JVM must be able to accept previously compiled code that may have subroutines. In other words, the
need for backward compatibility prevents the total elimination of subroutines.

A subsequent (2004) paper by Coglio notes (on p. 666) that most Java bytecode verifier implementations violated the JVM spec and rejected some valid (spec-wise) programs involving subroutines.

Another titbit/criticism from Leroy:

While effective in practice, Sun’s approach to subroutine verification raises a challenging issue: determining the subroutine structure is difficult. Not only are subroutines not syntactically
delimited, but return addresses are stored in general-purpose registers rather than on a subroutine-specific stack, which makes tracking return addresses and matching ret/jsr pairs more difficult.
To facilitate the determination of the subroutine structure, the JVM specification states a number of restrictions on correct JVM code, such as “two different subroutines cannot ‘merge’ their
execution to a single ret instruction” [33, section 4.9.6]. These restrictions seem rather ad-hoc
and specific to the particular subroutine labeling algorithm that Sun’s verifier uses. Moreover, the
description of subroutine labeling given in the JVM specification is very informal and incomplete.

Coglio's 2004 paper also noted that CLR has a built-in endfinally at their VM opcode level, which avoids some issues with jsr/ret. It looks like that's because you can't freely jump in or out of such blocks in CLR by means of other instructions, while you can goto in/out of a 'subroutine', which has no specific/enforced boundaries as such in the JVM, which complicated matters.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文