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 字节码对这些情况进行了一些复杂的异常表处理,但这在 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(锁定)最终出现在完全不同的方法定义中,现在我们收到了JVMIllegalMonitorStateException以及该函数的ART验证错误。

0

评论由:jafingerhut发表

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

0

评论由:adamclements发表

上传了一个新的补丁(并签署了贡献协议)。此补丁通过了JVM和ART字节码验证,围绕monitor退出人员在try/catch中的额外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),这稍微有点作弊——实际上,synchronized块是用Java实现的,因此保证了兼容性。这是以牺牲一点额外的间接性为代价的,也许命名/位置可以更好。

但它确实修复了bug,并在所有版本的android及其art和jvm上工作。这种做法可以接受吗?

0

评论者:hiredman

我还没有看到任何证据表明clojure生成的字节码在某种程度上违反了jvm规范,所以我怀疑这个问题是clojure需要jvm来运行,而android并不提供jvm,只是如果你不偏离正轨的话,看起来像是一个。

0

评论者:hiredman

根据 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 上执行监控退出(monitor exit)的次数不得超过 T 在 M 上执行监控进入(monitor entry)的次数。”

“At no point during a method invocation may the number of monitor exits performed by T on M since the method invocation exceed the number of monitor entries performed by T on M since the method invocation."
如果下一行的指令 https://gist.github.com/AdamClements/2ae6c4919964b71eb470#file-test_locks-class-L24 或监控进入指令失败,不能它最终可能出现在 finally 子句中并且尝试释放锁,即使它从未被捕获过吗?
我认为这违反了 JVM 规范中你链接的结构化锁定规则。

0

评论由:adamclements发表

我明白了,我必须提供更详细的背景信息。

然后补充了一些示例代码。

0

评论者:hiredman

关于结构化锁定,有一个有趣的问题:规范是参考静态字节的码还是字节码的运行时行为。给定字节码链接(https://gist.github.com/AdamClements/2ae6c4919964b71eb470#file-test_locks-class-L24),静态字节码有相同数量的入口和出口,但动态行为可能不同。我想知道艺术人员声称在验证時に强制执行的是哪一个(似乎应该强制静态字节码,而不是动态属性,然后他们不应该失败验证)。查看谷歌代码问题,由同一个开发者发布的评论https://code.google.com/p/android/issues/detail?id=80823#c6https://code.google.com/p/android/issues/detail?id=80823#c3,所以我怀疑这里可能存在一些沟通错误。开发者回复时的上下文不太清楚,因为之前的评论中提到要在方法中分割monitor-enter和退出。如果所有补丁、专业的Clojure Android构建等都被删除,那么使用纯Clojure jar你将获得j ava p转储的验证失败的代码,然后只需将其提交到Android问题跟踪器并问道:“嗨,这失败了验证,为什么?”

0

评论由:adamclements发表

是的,我不应该与补丁版本混淆。现在大纲和当前上传的版本使用的是纯Clojure的锁定宏。

我认为这个问题来自异常表和覆盖的指令。如果第24行可以抛出异常,那么在运行时会有一个monitor-exit,但你从未遇到过monitor-enter。

0

由gshayban发表的评论

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

...