2024 洛汝杰状态调查!中分享您的想法。

欢迎!请参阅关于页面以了解更多有关此页面如何运作的信息。

+52
询问 洛汝杰

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)。

似乎在锁定中的 monitor-enter、monitor-exit 和 try/finally 块的组合创建了 ART 标记为未平衡 monitorenter/monitorexit 字节码的路径。特别是,monitorenter 和 monitorexit 本身可以抛出(在 null 锁定对象上)。Java 字节码进行了一些复杂的异常表处理来覆盖这些情况,而 alaict)不可能在不修改 Clojure 编译器的情况下完成。

方法:一种可能的方法是让锁定在 Java 同步块的环境中调用传入的主体。这避免了通过将其交给 Java 解决复杂字节码的问题,但具有未知的影响。

补丁: clj-1472-3.patch

另请参阅:与 Java 同步块的字节码比较显示了一些差异
较弱 weaker://gist.weakert.com/AdamClements/2ae6c4919964b71eb470

筛选:由 Alex Miller - 我将此标记为筛选,因为我认为这是一个可行的解决方案,可以解决该问题,并且由于其使用频率不高,我并不担心这将成为性能问题。我会指出,我认为另一种处理方法是将锁定作为一个特殊形式加上编译器支持,但我不确定这是否值得做,所以我会把它留给 Rich 决定。

以以下注释关闭: 固定

41 答案

0
 
最佳答案

在此问题修复版本1.10.2-alpha1中发布。

0

评论者:adamclements

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

查看字节码,monitorenter在clojure.tools.nrepl.middleware.session/session-out的锁定和其他几个位置的锁定最终放在了完全不同的方法定义中,现在我们得到了JVM IllegalMonitorStateException以及对此函数的ART验证错误。

0

评论者:jafingerhut

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

0

评论者:adamclements

上传了新的补丁(并签署了贡献者协议)。这个补丁通过了JVM和ART的字节码验证,围绕monitor-exit的额外try/catch是可选的(带或不带验证都通过),但java版本在monitor-exit处无限重试,并在适当的时间显示死锁,如果在monitor-exit处未捕获错误,未来可能失败的未确定的monitor-enter可能会导致错误,而无法显示实际的问题。

它并不是非常美观,但如果没有更精细地控制生成的字节码,这可能是我能做到的最佳效果。

0

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

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

0

评论者:adamclements

是的,第一个补丁确实是错误的,我把它留在这里只是为了给对话添加一些上下文,但为了清晰起见,最好还是删除它。

对于任何在关注这次对话但不希望反编译和观察字节码的人来说,这里有一个保留文件,其中包含了 Java 同步块和 Clojure 锁定的差异。https://gist.github.com/AdamClements/2ae6c4919964b71eb470

我发现在确定规范中的偏差位置也很有难度,尽管我可以看出 Java 版本的差异。如果有什么不同的话,Clojure 版本看起来比 Java 版本更接近规范中的描述!

如果在这方面的知识比我更丰富的人能够在 AOSP 缺陷报告https://code.google.com/p/android/issues/detail?id=80823上进行讨论,那么也许我们可以将其解决为一个与 JVM 规范无关,而是过于关注 Java 实现的 Android 缺陷。或者,他们可能会发现 Clojure 实现中的某些错误。我已经上传了原始的 Clojure 行为,并要求他们解释更多为什么它失败的原因。

0

评论者:adamclements

ART 团队对我们认为我们违反了什么内容的回应是:

"结构化锁定"部分的描述如下:

"(链接: ...) 实现 (链接: ...) 被允许但不强制执行以下两条规则的,以保证结构化锁定。(链接: ...)"
ART 目前在验证时执行这两条规则,包括:

"方法调用期间,T 在 M 上执行的 monitor 退出次数,不得超过方法调用期间在 M 上执行的 monitor 入口次数。"

"如果在方法调用期间,T 在 M 上执行的 monitor 退出次数超过该方法调用期间在 M 上执行的 monitor 入口次数,则不会发生任何问题。"
"如果在方法调用期间,T 在 M 上执行的 monitor 退出次数超过该方法调用期间在 M 上执行的 monitor 入口次数,则将不会发生任何问题。"
如果在方法调用期间,T 在 M 上执行的 monitor 退出次数超过该方法调用期间在 M 上执行的 monitor 入口次数,则不会发生任何问题。

0

评论者:adamclements

如果例如指令https://gist.github.com/AdamClements/2ae6c4919964b71eb470#file-test_locks-class-L24或者在下一行的 monitor-enter 自身失败,它是否可能会导致最终处理子句,并尝试释放锁,尽管它从未被捕获过?

我认为这违反了您链接的 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。如果删除所有补丁,专用Clojure Android构建等,那么用Vanilla Clojure jar获取要验证失败的解释性Java程序列表,然后只是将它带到Android问题跟踪器并询问“嘿,这失败了验证,为什么?”

0
by

评论者:adamclements

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

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

0
by

评论者:gshayban

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

...