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 自己可以抛出异常(在一个空的锁定对象上)。Java 字节码对这些情况进行了某些复杂的异常表处理,但据我所知,没有修改 Clojure 编译器是无法做到这一点的。

方法:一个可能的方法是在 Java 的 synchronized 块上下文中调用传递的 body,这样通过移交到 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 字节码验证,围绕监控退出的额外 try/catch 是可选的(有无都通过验证),但在 java 版本无限重试 monitor-exit 并在正确的时间显示死锁的情况下,如果未捕获 monitor-exit 中的错误,则未来的未知监控进入可能会失败,不会显示实际错误。

这并不是很好看,但没有更细粒度的对生成的字节码进行控制,我只能做到这个程度。

0
by

评论由:adamclements 发布

刚刚使用 Lollipop 进行测试,这个补丁可能不再足够。

联系 ART 团队以了解他们是否可以提供更多帮助,并验证它是否可以在 AOSP 当前主干分支上工作

0

评论由:adamclements 发布

已在 AOSP 项目中提交bug,希望他们能阐明这是否是我们的问题以及如何解决它。

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

0

评论由:adamclements 发布

我已上传了锁定宏的替代实现(0001-CLJ-1472-Locking-macro-without-explicit-monitor-ente.patch),这在某种程度上有些欺骗——实际上同步块是在Java中实现的,从而保证了兼容性。这是以稍微多一点的间接成本为代价的,命名/位置可能也可以做得更好。

但这确实解决了bug,并在所有版本的 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的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执行监视器进入操作的次数。"

“监听器退出次数超过方法调用时T在M上的监视器进入次数。”
“例如,如果指令https://gist.github.com/AdamClements/2ae6c4919964b71eb470#file-test_locks-class-L24或下一行的监视器进入在本身发生失败,不能最终企图释放锁,尽管它从未被捕获?”
我认为这违反了你链接的jvm规范的锁结构规则。

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的同一开发者发布的评论,所以我怀疑有些沟通上的误解。由于在之前的评论中提到了在方法之间拆分monitor-enter和monitor-exit,因此不清楚该开发者是在什么情况下回复的。如果所有的补丁、专用Clojure Android构建等都被移除,事情就会更加清晰,然后使用纯Clojure JAR获取失败的验证的javap转储,然后将它带到Android问题跟踪器,并问“嘿,这个验证失败,为什么?”

0

评论由:adamclements 发布

是的,我不应该将它与补丁版本混淆。现在,精简版的locking macro现在使用的是纯Clojure版本。

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

0

评论者:gshayban

根据Marcus Lagergren的说法,JRockit和Hotspot都将锁释放处理为不同方法中的操作...也许Android有不同的(错误的)解释?

...