【问题标题】:ProGuard can cause incorrect calculationsProGuard 可能导致错误的计算
【发布时间】:2014-01-01 15:22:38
【问题描述】:

我遇到了一个非常奇怪的错误。下面的一小段代码使用了一个相当简单的数学运算。

protected double C_n_k(int n, int k)
{
  if(k<0 || k>n)
    return 0;
  double s=1;
  for(int i=1;i<=k;i++)
    s=s*(n+1-i)/i;
  return s;
}

编辑 在某些设备上使用 ProGuard 可能会出错。我已经在 HTC One S Android 4.1.1 build 3.16.401.8 上确认了这一点,但从我收到的电子邮件来看,很多 Android 4+ 的手机都受到了影响。对于其中一些(Galaxy S3),美国运营商品牌手机受到影响,而国际版本则不受影响。许多手机不受影响。

以下是计算 1

我有 3 个问题:

  1. 怎么可能?即使 ProGuard 出错了,设备和会话之间的计算也应该是一致的。

  2. 我们如何避免它?我知道在这种情况下用long 替换double 很好,但这不是一种通用方法。放弃使用double 或发布未混淆的版本是毫无疑问的。

  3. 哪些 Android 版本会受到影响?我在游戏中修复它的速度非常快,所以我只知道很多玩家都看过它,至少大多数玩家使用的是 Android 4.0

溢出是毫无疑问的,因为有时我在计算C(3,3)=3/1*2/2*1/3 时会看到错误。通常不正确的数字从 C(10,...) 的某个地方开始,看起来手机已经“忘记”了一些划分。

我的 SDK 工具是 22.3(最新),我在 Eclipse 和 IntelliJ IDEA 创建的构建中看到了它。

活动代码:

package com.karmangames.mathtest;

import android.app.Activity;
import android.os.Bundle;
import android.text.method.ScrollingMovementMethod;
import android.widget.TextView;

public class MathTestActivity extends Activity
{
  /**
   * Called when the activity is first created.
   */
  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    String s="";
    for(int n=0;n<=25;n++)
      for(int k=0;k<=n;k++)
      {
        double v=C_n_k_double(n,k);
        s+="C("+n+","+k+")="+v+(v==C_n_k_long(n,k) ? "" : "   Correct is "+C_n_k_long(n,k))+"\n";
        if(k==n)
          s+="\n";
      }
    System.out.println(s);
    ((TextView)findViewById(R.id.text)).setText(s);
    ((TextView)findViewById(R.id.text)).setMovementMethod(new ScrollingMovementMethod());
  }

  protected double C_n_k_double(int n, int k)
  {
    if(k<0 || k>n)
      return 0;
    //C_n^k
    double s=1;
    for(int i=1;i<=k;i++)
      s=s*(n+1-i)/i;
    return s;
  }

  protected double C_n_k_long(int n, int k)
  {
    if(k<0 || k>n)
      return 0;
    //C_n^k
    long s=1;
    for(int i=1;i<=k;i++)
      s=s*(n+1-i)/i;
    return (double)s;
  }

}

main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
  >

  <TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:id="@+id/text"
    android:text="Hello World!"
    />
</LinearLayout>

错误计算结果示例(记住,每次尝试都不一样)

C(0,0)=1.0

C(1,0)=1.0
C(1,1)=1.0

C(2,0)=1.0
C(2,1)=2.0
C(2,2)=1.0

C(3,0)=1.0
C(3,1)=3.0
C(3,2)=3.0
C(3,3)=1.0

C(4,0)=1.0
C(4,1)=4.0
C(4,2)=6.0
C(4,3)=4.0
C(4,4)=1.0

C(5,0)=1.0
C(5,1)=5.0
C(5,2)=10.0
C(5,3)=10.0
C(5,4)=30.0   Correct is 5.0
C(5,5)=1.0

C(6,0)=1.0
C(6,1)=6.0
C(6,2)=15.0
C(6,3)=40.0   Correct is 20.0
C(6,4)=90.0   Correct is 15.0
C(6,5)=144.0   Correct is 6.0
C(6,6)=120.0   Correct is 1.0

C(7,0)=1.0
C(7,1)=7.0
C(7,2)=21.0
C(7,3)=35.0
C(7,4)=105.0   Correct is 35.0
C(7,5)=504.0   Correct is 21.0
C(7,6)=840.0   Correct is 7.0
C(7,7)=720.0   Correct is 1.0

C(8,0)=1.0
C(8,1)=8.0
C(8,2)=28.0
C(8,3)=112.0   Correct is 56.0
C(8,4)=70.0
C(8,5)=1344.0   Correct is 56.0
C(8,6)=3360.0   Correct is 28.0
C(8,7)=5760.0   Correct is 8.0
C(8,8)=5040.0   Correct is 1.0

C(9,0)=1.0
C(9,1)=9.0
C(9,2)=36.0
C(9,3)=168.0   Correct is 84.0
C(9,4)=756.0   Correct is 126.0
C(9,5)=3024.0   Correct is 126.0
C(9,6)=10080.0   Correct is 84.0
C(9,7)=25920.0   Correct is 36.0
C(9,8)=45360.0   Correct is 9.0
C(9,9)=40320.0   Correct is 1.0

C(10,0)=1.0
C(10,1)=10.0
C(10,2)=45.0
C(10,3)=120.0
C(10,4)=210.0
C(10,5)=252.0
C(10,6)=25200.0   Correct is 210.0
C(10,7)=120.0
C(10,8)=315.0   Correct is 45.0
C(10,9)=16800.0   Correct is 10.0
C(10,10)=1.0

【问题讨论】:

  • 听起来不太可能。您是否尝试过在该循环中记录 s 的每个值以了解计算的不同之处?
  • 哪个是“正确的?”如果有的话?
  • FWIW,在数学中,C(12, 4) = 495。
  • 问题肯定出在您的代码上。一个字——调试器!
  • 由于longdouble 不适用的地方工作,并且您声称没有溢出,我建议尝试使用strictfp 关键字if 差异小得多。有这么大的差异,(并且在这个例子中没有丢失精度的事实)我非常怀疑这是一个舍入错误,但是。

标签: java android proguard android-sdk-tools


【解决方案1】:

原始代码和处理后的代码在 Java VM 和大多数 Dalvik VM 上都可以正常工作,因此它必须是有效的。如果处理后的代码在一些 Dalvik VM 上产生虚假结果,则问题很可能是由这些 VM 中的 JIT 编译器引起的。 Google 的 Android 团队应该对此进行调查。

ProGuard 在这里应用的最明显的优化是内联方法。在最终的字节码中重新排序了一些分支指令和局部变量,但这一小段代码的执行流程基本相同。很难确定 ProGuard 如何避免这个问题。您可以完全禁用优化步骤。

您可以检查在没有 ProGuard 的情况下手动内联代码是否会导致相同的问题(我的设备上似乎没有出现此问题)。

(我是ProGuard的开发者)

【讨论】:

  • 谢谢埃里克!我打算向android团队发布一个问题,只是想等待一天,以防我错过了一些明显的东西并且有人指出了它。我使用 ProGuard 进行移动开发大约有 10 年了,你做得很好。继续加油!
  • Eric,我检查了几个选项:-dontoptimize 没有帮助,-dontobfuscate 有帮助,但谁想要商业项目中的未混淆代码?我认为没有内联方法,因为我在mapping.txt 中看到了所有 3 种方法。不幸的是,我前段时间换到了 MacOS,错过了一些我用于逆向工程的工具。我已经发布了一个问题 63790 for android,希望能得到一些答案,但我认为不会很快。
  • 混淆步骤仅将方法重命名为'a'和'b'并删除有关局部变量的调试信息,但它确实导致dx编译器发出不同的字节码。您可以尝试-keepattributes LocalVariableTable 是否有所作为。您可以使用 Android SDK 中的dexdump 查看应用程序的字节码。
  • 是的,-keepattributes LocalVariableTable 有帮助,至少在我的手机上是这样。哪个风险较小,vmSafeMode 还是这个?
  • 你应该保留这个属性;它对于解释混淆的堆栈跟踪也很有用(请参阅 ProGuard 文档)。使用 vmSafeMode 标志禁用 JIT 编译可能是不可取的,因为它会非常严重地影响性能。现在,如果属性有所不同,您可能也可以在没有 ProGuard 的情况下通过使用标志 -g:none 进行编译来解决问题。
【解决方案2】:

Android 团队成员在我的issue 的评论中发布了一个可能的解决方案。如果我将android:vmSafeMode="true" 添加到清单文件的application 元素,则所有计算都会正确执行。这个选项没有很好的记录,老实说我不知道​​它会对速度有多大影响,但至少数学是正确的。我会将其标记为正确答案,直到找到更好的答案。

【讨论】:

    【解决方案3】:

    这一切都证明是 ProGuard 优化恰好触发的 JIT 编译器错误。

    正如 AOSP issue 解释的那样:

    在 Jelly Bean 发布时间中有一个窗口,在该窗口中,JIT 会错误地优化掉浮点双精度常量的使用,这些常量在低 32 位中是相同的(并且还满足了一些其他条件)。该缺陷于 2012 年 11 月下旬在 Google 内部树中引入,并于 2013 年 2 月在 aosp 中引入。 2013 年 4 月修复。

    更详细的解释在这个另一个AOSP issue

    【讨论】:

      猜你喜欢
      • 2018-07-29
      • 2018-05-23
      • 1970-01-01
      • 1970-01-01
      • 2016-06-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多