请在 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 同步块比较可以看到一些差异
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的字节码验证,额外的try/catch围住monitor exit是可选的(验证通过与否都可以通过),但是java版本无限重试monitor-exit并在正确的时间显示死锁,如果没有在monitor-exit处捕获错误,未来的未确定monitor-enter可能会失败,而不会显示出实际的错误。

这并不很美观,但如果没有对生成的字节码有更精细的控制,这就是我能做到的最好。

0
by

评论者:adamclements

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

联系ART团队,看看他们是否能提供更多的信息,并验证它是否适用于当前AOSP的master分支。

0
by

评论者:adamclements

在AOSP项目中提报了故障,希望他们能提供一些线索,看看这是否是我们的问题,如果是的话,我们该如何修复它。

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

0
by

评论者:adamclements

我上传了一个锁宏的替代实现(0001-CLJ-1472-Locking-macro-without-explicit-monitor-ente.patch),它有点作弊——实际上的synchronized块是在Java中实现的,因此保证了兼容性。这要以稍微多一点的间接和更佳的命名/位置为代价。

但它确实修复了错误,并且可以在所有版本的Android、Android + Art以及JVM上工作。这种方法是否可接受?

0
by

评论者:hiredman

我还没有看到任何证据表明生成的字节码Clojure违反了JVM规范,所以我怀疑问题在于Clojure需要JVM来运行,而Android并不提供JVM,只是如果你不偏离路径,它看起来像JVM。

0
by

评论者:hiredman

根据https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.11.10中的结构化锁的术语,(locking nil)可能生成违反结构化锁的运行时行为的字节码。该问题的第一个补丁可能导致编译器在方法中发出monitorenter/monitorexit指令,这肯定违反了结构化锁。

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 上执行的监视器进入次数。

在方法调用期间,T 在 M 上执行的监视器退出次数不能超过 T 在 M 上执行的监视器进入次数。
在方法调用期间,T 在 M 上执行的监视器退出次数不得超过 T 在 M 上执行的监视器进入次数。
在方法调用期间,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,所以不清楚开发者是在什么背景下回复的。如果所有补丁、特殊化的Android构建等都被移除,那么使用vanilla clojure jar获得无法验证失败的字节码的JavaP转储会更清晰,然后将这些带到Android问题跟踪器并询问“嘿,这失败了验证,为什么?”

0

评论者:adamclements

是的,我不应该把它与补丁版本混淆。不过,现在gist和当前上传的版本都使用vanilla clojure版本的锁定宏。

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

0

评论者:gshayban

据Marcus Lagergren说,JRockit和Hotspot都把锁释放放在了不同的方法...也许Android有不同的(错误的)解释?

...