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 字节码对这些情况进行了复杂的异常表处理,这(据我所知)除非修改 Clojure 编译器,否则无法实现。

方法: 一种可能的方法是在 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 退出的额外 try/catch 是可选的(即使不带它也通过验证),但是 java 版本会在 monitor-exit 无限重启,并在正确的时间显示死锁,如果不捕获 monitor-exit 错误,未来的不确定的 monitor-enter 可能会失败,不会显示出实际的错误。

它并不很漂亮,但没有更细粒度地控制生成的字节码,这是我能做到的最好了。

0
by

评论者: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

鉴于JVM规范中结构化锁定语句的https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.11.10(locking nil)可能生成违反结构化锁定的运行时行为的字节码。对此问题的第一个补丁可能使编译器在不同的方法中发出monitorenter/exit指令,这肯定违反了结构化锁定。

0

评论者:adamclements

是的,第一个补丁肯定是不正确的,我留下它是为了对话的上下文,但最好还是去掉它以保持清晰。

对于不想反汇编和查看字节码的读者,这是一个关于Java synchronized块和clojure locking之间差异的gist:https://gist.github.com/AdamClements/2ae6c4919964b71eb470

虽然我能看到Java版本之间的差异,但我难以找出偏离规格的具体位置,如果有什么区别的话,Clojure版本看起来比Java版本更接近规格中所描述的内容!

如果能有人对这个主题比我更有知识,参与到AOSP的bug https://code.google.com/p/android/issues/detail?id=80823 中,那么也许我们就可以将其作为一个android的bug来解决,这个bug过于关注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 或下一行的monitor-enter自身失败,它会不会最终导致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和monitor-exit,所以不清楚开发者是在什么背景下回复的。如果删除所有补丁、专业化的Android构建等,事情将会更加清晰,然后使用纯Clojure jar,你可以得到什么在验证中失败的jvap转储,然后将它带到Android问题跟踪器并询问“嘿,这失败了验证,为什么?”

0

评论者:adamclements

是的,我不应该和修补版本混淆。现在gist和目前上传的版本使用了纯Clojure版本的锁定宏。

我认为问题出在异常表和指令上。如果第24行可以抛出异常,那么在运行时遇到monitor-exit时可能从未遇到过monitor-enter。

0

评论者:gshayban

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

...