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: Rejecting class clojure.core.server$stop_server because it failed compile-time verification (declaration of 'clojure.core.server$stop_server' appears in /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 的 synchronized 块的上下文中执行。这通过将其移交给 Java 来避免了复杂的字节码问题,但具有未知的影响。

补丁:clj-1472-3.patch

另见:与 java synchronized 块相比的字节码检查显示出许多差异
https://gist.github.com/AdamClements/2ae6c4919964b71eb470

审查人员:Alex Miller - 我认为这是一个可行的解决方案,可以修复该问题,由于这种使用的不频繁,我认为这不会成为性能问题。我将标记我认为另一种处理方法是将 locking 制作成具有编译器支持的特称形式,但我不确定这是否值得去做,所以我将把这个交给 Rich 去决定。

已关闭并标注为 已修复

41 个答案

0
by
 
最佳答案

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

0
by

评论由:adamclements 添加

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

查看字节码,clojure.tools.nrepl.middleware.session/session-out中的锁定monitorenter以及在某些其他位置最终结束于一个完全不同的方法定义,我们现在会得到一个JVM IllegalMonitorStateException以及该函数的ART验证错误。

0
by

评论由:jafingerhut 添加

Adam,我无法评论你的补丁是否有兴趣,但确实如此,如果作者没有签署贡献者协议,那么就无法将补丁提交到Clojure,你可以在https://clojure.org/contributing在线签署。

0
by

评论由:adamclements 添加

上传了一个新的补丁(并签署了贡献者协议)。这个补丁通过两者的JVM和ART字节码验证,围绕monitor exit的额外try/catch是可选的(验证在有或没有它的情况下都通过),但是java版本会在无限重试monitor-exit并在正确的时间显示死锁,如果没有捕获monitor-exit的错误,那么在未来的某个不确定的时间点可能会失败的monitor-enter就不会显示实际的错误。

这并不很漂亮,但如果没有更好的方式来控制生成的字节码,这就是我能做到的最好。

0

评论由:adamclements 添加

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

正在与ART团队取得联系,看看他们是否能提供更多线索,并确认它是否能够在AOSP当前master分支上工作

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中实现synchronized块,从而保证了兼容性。这是以牺牲一点点额外的间接开销和名称/位置可能可以更好的代价。

但它确实修复了这个错误并在所有版本的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 添加

是的,第一个补丁肯定是错误的,我把它留在了对话的背景中,但最好还是为了清晰起见把它去掉。

对于任何想关注这个讨论而不想反编译和观察字节码的人来说,这里有一个包含Java同步块与Clojure锁定之间差异的 gist:https://gist.github.com/AdamClements/2ae6c4919964b71eb470

我还发现很难找出与规范差异的地方,尽管我可以看出Java版本的不同,但 Clojure版本似乎比Java版本更接近规范中的描述!

如果有人在主题上比我更了解情况,并且愿意参与 AOSP 缺陷 https://code.google.com/p/android/issues/detail?id=80823 的讨论,那么我们可能可以将此问题作为 Android 缺陷来解决,因为这个缺陷过于关注 Java 实现,而不是 JVM 规范,也许他们会发现 Clojure 实现中的问题。我已经上传了原始的 Clojure 行为,并请求他们对为什么它失败进行更多的解释。

0

评论由:adamclements 添加

ART 团队对我们认为我们违反了什么的问题的回复是:

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

“(链接: ...) 实现 (链接: ...) 是允许但不是必需强制执行
以下这两个保证结构化锁定的规则。 (链接: ...)”

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

“在方法调用过程中,T对M执行的监视器退出的次数不得超过
方法调用以来T在M上执行的监视器进入次数。”
“At no point during a method invocation may the number of monitor exits

0

评论由:adamclements 添加

例如,如果指令 https://gist.github.com/AdamClements/2ae6c4919964b71eb470#file-test_locks-class-L24 或下一行的 monitor-enter 本身失败了, couldn't it could end up in the finally clause and attempt to release the lock even though it has never been captured?

我认为这违反了你在链接中提到的 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和退出,所以不清楚开发者是在什么背景下回答的。如果删除所有补丁、专门针对Android的Clojure构建等,事情就会变得清楚很多;然后用纯Clojure jar文件获得要验证失败.javap的析构,然后将其带到Android问题跟踪器,并询问“嘿,这个没有通过验证,为什么?”

0

评论由:adamclements 添加

是的,我不应该将它与补丁版本混淆。现在虽然 gist 和最近上传的版本都使用原生版本的锁定宏。

我认为问题来自异常表和覆盖的指令。如果第 24 行可能会抛出异常,则在运行时将遇到monitor-exit,而没有遇到过monitor-enter。

0

评论者:gshayban

根据 Marcus Lagergren,JRockit 和 Hotspot 都考虑了锁释放在不同的方法中...或许 Android 有不同的(错误的)解释?

...