请在 2024 年 Clojure 状态调查! 中分享您的想法。

欢迎!请在 关于 页面上查看有关此工作的一些更多信息。

+52
Clojure
关闭

Android ART 在字节码上运行编译时验证并拒绝任何锁定宏的使用。错误看起来像 CLJ-1829 中的错误(在这种情况下,clojure.core.server/stop-server 调用了锁定宏)

10-16 14:49:26.801 2008-2008/? E/AndroidRuntime: java.lang.VerifyError: 拒绝类 clojure.core.server$stop_server 因为它在编译时验证失败(clojure.core.server$stop_server 的声明出现在 /data/app/com.clojure_on_android-1/base.apk 中)

原因:根据 Android 问题的讨论(https://code.google.com/p/android/issues/detail?id=80823),这可能是由于更加严格地执行 JVM 规范中的“结构化锁定”规定(https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.11.10)。

似乎在 locking 中的 monitor-enter,monitor-exit 和 try/finally 块的混合创建了一些路径,ART 将其标记为没有平衡的 monitorenter/monitorexit 字节码。特别是,monitorenter 和 monitorexit 本身可以抛出异常(在 null 锁定对象上)。Java 字节码进行了一些巧妙的异常表处理来处理这些情况,这些(afaict)不可能在不修改 Clojure 编译器的情况下完成。

方法:一种可能的方法是将锁定在 Java 中的同步块上下文中调用传递的身体。这通过将处理委托给 Java 来避免了复杂的字节码问题,但具有未知的性能影响。

补丁:clj-1472-3.patch

另见:将字节码与 java synchronized 块进行比较,发现存在一些差异
https://gist.github.com/AdamClements/2ae6c4919964b71eb470

筛选:由 Alex Miller 筛选 - 我将其标记为已筛选,因为我认为这是一种可行的解决方案,可以修复问题,但由于其使用频率较低,我并不担心它会导致性能问题。我认为另一种处理方法是使 locking 成为具有编译器支持的特殊形式,但我不确定是否值得这样做,因此我将这个问题留给 Rich 决定。

已关闭,附注: 已修复

41 个答案

0
 
最佳答案

此问题的修复已包含在1.10.2-alpha1版本中。

0

评论者:adamclements

使用了一段时间之后,我发现将这段代码移出try块会导致nREPL崩溃。

检查字节码后,clojure.tools.nrepl.middleware.session/session-out和几个其他地方的monitorenter锁定位在完全不同的方法定义中,我们现在会得到一个JVM IllegalMonitorStateException以及这个函数的ART验证错误。

0

评论者:jafingerhut

Adam,我无法评论您的补丁是否有兴趣,但确实如此,如果作者没有签署贡献者协议,Clojure将不会接受任何补丁,现在可以在线完成,请参阅https://clojure.org/contributing

0

评论者:adamclements

上传了一个新的补丁(并签署了贡献者协议)。这个补丁通过了JVM和ART的字节码验证。围绕monitor退出周围的额外try/catch是可选的(验证在没有它的情况下也可以通过),但是java版本会无限期重试monitor-exit并适时显示死锁,而在monitor-exit中不捕获错误时,将可能在未来的某个时间点不小心地监控一个未确定的monitor-enter可能会失败,且无法显示实际的错误。

这可能不是很好看,但如果没有对生成的字节码有更细粒度的控制,这可能是我能做最好的。

0

评论者:adamclements

刚刚测试了Lollipop版本,这个补丁可能不再足够。

正在与ART团队联系,看看他们能否提供更多信息,并验证它是否能在AOSP当前主干分支上运行。

0

评论者:adamclements

已在AOSP项目上创建了一个错误报告,希望他们能提供一些关于是否属于我们的问题以及如何修复它的信息。

https://code.google.com/p/android/issues/detail?id=80823

0

评论者:adamclements

我已经上传了一个锁定宏的替代实现(0001-CLJ-1472-Locking-macro-without-explicit-monitor-ente.patch),这个实现采用了一些诡计——实际上同步块是用Java实现的,从而保证了兼容性。这是以额外的间接性和可能的更好的命名/位置为代价的。

但是在所有版本的Android、Android + ART和JVM上都能修复错误。这种做法能否接受?

0

评论者:hiredman

我还没有看到任何证据表明Clojure生成的字节码以某种方式违反了JVM规范,所以我怀疑问题是Clojure需要JVM来运行,而Android并不提供JVM,只是如果你不越出常规路径,看起来像是JVM的东西。

0

评论者:hiredman

鉴于《Java虚拟机规范》中关于结构化锁定的表述(https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.11.10),(locking nil)可能生成违反结构化锁定的运行时行为的字节码。这个问题的第一个补丁可能导致编译器在选定的方法中发出monitorenter/exit指令,这肯定违反了结构化锁定。

0

评论者:adamclements

是的,第一个补丁确实是错误的,我留了一些上下文以供讨论,但最好还是删除它以保持清晰。

对于跟踪这次对话却不想反汇编和观察字节码的人来说,这里有一个Gist,其中包含了Java同步块和Clojure锁定之间的差异 https://gist.github.com/AdamClements/2ae6c4919964b71eb470

我也很难找出偏差发生在规定中的哪个地方,尽管我能看到与Java版本的差异,但如果有什么不同的话,Clojure版本看起来比Java版本更接近规定中的描述!

如果有人在这方面比我知道得多的话,可以参与在AOSP上的bug讨论 https://code.google.com/p/android/issues/detail?id=80823,那么我们可能就能将这个问题作为安卓的一个问题来解决,它更多地关注Java实现,而不是JVM规范,或者他们可能会发现Clojure实现中有错误。我已经上传了原始的Clojure行为,并要求他们解释导致失败的原因。

0

评论者:adamclements

ART团队关于他们认为我们违反了什么的回应是:

在“结构化锁定”部分包含了以下内容:

“(链接: ...) 实现 (链接: ...) 被允许但不强制执行
以下两条规则保证结构化锁定。 (链接: ...)”

ART当前在验证时强制执行这两条规则,包括

“在方法调用过程中,T在M上执行的监控退出数不得
超过T在M上执行的监控进入数。”
自从方法调用以来。

0

评论者:adamclements

例如,如果下一行的指令 https://gist.github.com/AdamClements/2ae6c4919964b71eb470#file-test_locks-class-L24 或监控进入指令失败了,它不就可以进入finally子句并尝试释放锁,即使它从未被捕获吗?

我认为这违反了你链接的JVM规范中的结构化锁定规则。

0

评论者:hiredman

有关结构化锁定的一个有趣问题:规范是指静态字节码还是字节码的运行时行为。以已链接的字节码为例(https://gist.github.com/AdamClements/2ae6c4919964b71eb470#file-test_locks-class-L24),静态字节码有相同数量的入口和出口,但动态行为可能不同。我怀疑在这个验证时刻美术人员声称要执行的是哪个(看起来似乎应该是静态字节码,而不是动态属性,但既然它们没有在验证上失败,那么它们应该不会失败)。查看谷歌代码问题,高亮显示的评论(https://code.google.com/p/android/issues/detail?id=80823#c6)是由和(https://code.google.com/p/android/issues/detail?id=80823#c3)相同开发人员发表的,所以我有点怀疑这里存在某种误解。不清楚这位开发者是在什么背景下回复的,因为它引发了分割方法中的monitor-enter和exit。如果去掉所有补丁、专项Clojure Android构建等,事情就会更加清晰,然后用纯净的Clojure jar获取javap崩溃的验证失败,然后把那些推送到Android问题跟踪器,问“嘿,这个验证失败,为什么?”

0

评论者:adamclements

是的,我不应该将其与修正版本混淆。现在,摘要和当前上传版本都使用纯净的Clojure锁定宏版本。

我认为问题来自异常表和覆盖的指令。如果第24行可以抛出异常,那么在运行时结束monitor-exit时,您将从未遇到monitor-enter。

0

评论者:gshayban

据Marcus Lagergren说,JRockit和Hotspot都将锁释放计算在不同方法中…也许Android有另一种(错误的)解释?

...