SUMTEC -- There's a thing in my bloglet.

But it's not only one. It's many. It's the same as other things but it exactly likes nothing else...

  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::
  207 随笔 :: 19 文章 :: 1637 评论 :: 12 Trackbacks

首先感谢拓荒者为我们提供了错误样本
其次希望微软能给我奖金,或者至少稿费。


该错误在一般使用的情况下很少遇到,但在非常特殊的使用方式下才会产生,尤其在您特意使用一些C#的副作用的时候更容易产生。不过如果遇到了,说不定真是会损失惨重。(我个人感觉这种错误跟Intel多年前奔腾芯片的浮点错误非常神似。)

 

错误简述:

如果您的程序:
1、在某个整形变量上面进行不带检查的加法操作(unchecked,默认的行为方式,不包括减法、乘法、除法操作),并且产生溢出(溢出后数值必然是负值),并且
2、立刻紧跟在该加法操作后面判断是否小于常量零(加完之后有乘除或者函数调用等,或者判断的是某个变量里面的零,或者判断的是小于其他常量值等,都不符合该条件),并且
3、在该函数当中使用了该变量的引用,例如:a.XXXX() 或者 AnotherFunction(ref a)。

或者与此相反的:
1、……减法……(……正值)……
2、……大于常量零……
3、……

则会引起上述第二步的判断与我们的期望不符——似乎该数大于等于0,并且因此没有执行该条件分支的语句或者语句块。



下面是错误重现:

1、在C#里新建一个Console项目
2、插入下列代码:

 class Test
 {
  static void Main()
  {
   int a = 0x79de61c0; //2044617152;
   a +=    0x12345678;    
   //a 应为 0x8c12b838;  //-1944930248
 
   if( a < 0 ) a = -a;
 
   System.Console.WriteLine( a );
   string str2 = a.ToString();
   Console.ReadLine();
  }
 }

3、运行后发现,if(a<0) a = -a; 这一句话出现瑕疵,a < 0 测试出错,并因此没有执行后续的  a = -a 语句。

当我们去掉 string str2 = a.ToString(); 这一句话之后,错误消失。


下面是对该问题的具体分析:

该问题实质上是由于JIT引擎翻译逻辑有瑕疵引起的。当注释掉string str2 = a.ToString() 之后,我们调试时打开反编译窗口以及寄存器窗口。在寄存器窗口上面点击右键,选上“标志”。此时我们可以看到:



请注意图片黄色箭头处,if(a < 0) 实际上被翻译成jns 0016。jns机器指令的含义是,如果不是负数则跳转,实际上判断的是“符号标志”,也就是途中红圈圈上的"PL",这个标志位(以及其他一些标志位)由上一个指令add产生(这是该指令的副作用)。 由于符号为负,并没有条件转移,因此能够执行下一句a = -a (也就是neg esi)。但是请注意图中另外一个寄存器标志OV,该标志表示“溢出”。很明显我们的代码是因为相加溢出才导致结果变为负数,所以该标志位被置位。


当我们再去掉string str2 = a.ToString(); 并运行之后,我们可以看到:



注意图中红圈处,原来的jns指令现在被改为jge指令。jge机器指令的含义是,如果大于等于则跳转,实际上判断的是“OV”、“PL”,相当于if ((OV ^ PL) == false) goto xxx。也就是说,多关注了一个OV标志。

很明显由于前面的溢出,造成OV标志置位,因此条件转移成立,结果没有执行后续的neg esi。所以我们从源代码的角度看,似乎此时变量a的值是非负数,这跟该数为负数的事实不符。

尽管从源代码的角度看,似乎没有任何与if(a < 0)有关系的改动,实际上由于我不清楚的理由,当该函数中使用到了a的引用,结果造成了if(a < 0)翻译成机器指令的不同,进而对一些副作用的反应不相同。

此例当中对a的引用是a.ToString(),实际上如果您使用AnotherFunction(ref a)替代这一句话,也会引起相同的问题。与此相反的减法操作的问题,原因和解释类似。


该问题的解决方法:
1、尽可能不要利用C#相加/减溢出后变成负数的副作用,例如用下列方式避免副作用:
  checked
  {
     a += 1234;
  }

2、如果您确信相加后溢出的副作用是必须的,那么请采取下列措施避免该错误:
  a += 1234;
  b = a;
  if (b < 0)
  {
    a = -a;
  }

注意,这个并非微软的官方解决方法,目前我并不清楚微软的KB里面是否有该问题的纪录。

posted on 2005-03-06 00:09 Sumtec 阅读(4738) 评论(14)  编辑 收藏 所属分类: .NET 技术内幕

评论

#1楼  2005-03-06 01:07 拓荒者      
事实上,我还发现了其他的BUG。如果需要,可以进一步与我讨论。
---拓荒者
  回复  引用  查看    

#2楼  2005-03-06 02:14 问题男 [未注册用户]
可能尚不能算bug,OF这个标志被置位就意味着运算发生了逻辑错误,此时任何继续的依赖运算呈现未定状态是隐含允许的,这个检查应该由运算逻辑控制者负责才好吧

btw,80x86cpu的jge指令跳转条件是(SF 异或 OV) == 0
  回复  引用    

#3楼  2005-03-06 02:22 问题男 [未注册用户]
看了一下if(a < 0)... else都被译作
IL_0010: bge.s IL_0015
顺便体验了jit的实时目标机器指令映射,映射的规则很有趣阿
  回复  引用    

#4楼 [楼主] 2005-03-06 11:58 Sumtec      
@问题男
谢谢关于jge指令的提示,已经修正。

不过你说不算bug我不那么认为,你觉得在C++里面会遇到这样的情况吗?如果你在C++遇到这个情况你会觉得是不是很恼火?

我认为它就是一个Bug,你看我这么说你是否能够接受:

首先,C#与C++一样,都是对运算不作溢出检查的,并且承认这是一种合法的副作用,C#和C++的规范都没有禁止使用者一个副作用。(或者我孤陋寡闻,没有听说,但是我觉得既然他们的默认行为是不作检查,那么就不应该禁止用户使用这一点。反之,如果禁止用户使用这种副作用,那么应该更干脆一点,直接在编译好的代码里面做检查。)

其次,C#与C++一样,我只听说了关于溢出之后可能会导致逻辑不正常的例子只有这么一类:
本来是两个正值,相加溢出后变成负值,虽然按照数学上的逻辑本应该为正值,但是在这里if (a<0)就会被判断通过,与数学上的逻辑不符。
并没有这种导致逻辑不正常的例子:
如果本来是两个正值,相加溢出后变成负值后,if (a<0)的判断可能通过,也可能不通过,程序逻辑无法确定。

再次,如果一个语句if (a<0)的判断的结果,不依赖于a,并且可能通过,可能不通过,那么我认为if (a<0)这个语句就失效了。因为他根本就跟你源代码的意图没有任何关系。
  回复  引用  查看    

#5楼 [楼主] 2005-03-06 12:03 Sumtec      
@问题男

还有,你后来说的if (a<0) ... else 都被翻译成了jge,是不对的。是否被翻译成jge取决于我文章说的三个条件。如果这三个条件前两个成立,但是第三个不成立,那就会被翻译成jns;如果三个都成立,那就会被翻译成jge。

我估计你的测试里面肯定有 a.ToString 之类的代码。注意,使用a.ToString的位置只要在函数内部,就会导致被翻译成jge这一结果。
  回复  引用  查看    

#6楼  2005-03-06 14:53 问题男 [未注册用户]
呵呵,我没有说过都被翻译成jge啊,瞧,我的原话:
quote:
看了一下if(a < 0)... else都被译作
IL_0010: bge.s IL_0015
顺便体验了jit的实时目标机器指令映射,映射的规则很有趣阿
=========================================
是ilasm啊,呵呵。众所周知.net支持语言的程序源码在被编译时生成ilasm而不是目标机器码,待到执行的时候再实时映射,是这样的吧

咱们疑惑的焦点是什么机制导致了jit在x86环境中对同样的ilasm指令有时候选择映射成jge而另一些情况下选择jns

如你所述,如果被映射成jns(我看了好几个c/c++编译器都是如此对待a < 0),将什么问题也没有,令我想不通的是难道判断两个标志(SF、OV)并作运算的jge指令会比只判断一个(SF)的jns更具效率?显然不是,那么是什么原因使jge被选用了呢?当然,你谈到了触发条件,但这个仅仅是条件,尚未涉及(也不易推断出)其中机制。不管怎么说,这个大不了就是个效率上(效率差别很少,大约2-3周期/指令)的设计失误(如果确有其事),而算bug有些个牵强,呵呵

谈一谈jge这个罪魁,前面提到了,jge的跳转条件是OF == SF,在运算正确时,结果自然是正确的,在正数加法运算溢出时,OF是肯定会被置位,SF自然也会被置位,那么OF == SF成立,jump条件满足(相对的,jle之于负数减法溢出也是一样的)。为什么逻辑和实际不同呢,为什么在逻辑上不改跳转却跳转呢?这是由于intel在设计jge逻辑时,针对的是“正确”的运算(不然怎么会判断OF == SF这个奇怪的条件,呵呵),你在汇编程序中在正数加法(或负数减法)溢出后使用jge(或jle)同样会“令人疑惑”,如果这个算个bug,那么应该向intel提出才是,呵呵,jit大不了就是个错误使用(逻辑上并没有错)的问题,而并不能算作设计bug,这个就是我的观点了。

不过,既然你这么坚持,算bug就算一个吧,也未尝不可,呵呵,况且据你说可能还有好处不是,呵呵,支持报至ms

btw,上次回帖就发现了这个现象,即三个条件成立,则必然映射成jge,“如果这三个条件前两个成立,但是第三个不成立,那就会被翻译成jns”则并不能肯定,你可以尝试在这个函数的后面加上很多没有使用a的不相干代码,同样会被译成jge
  回复  引用    

#7楼  2005-03-06 15:09 Justin Shen      
overflow不能算是错误,某些散列算法中,要有意overflow才行
  回复  引用  查看    

#8楼  2005-03-06 15:17 Justin Shen      
PS一下,这个问题在VS2005里面也有...
  回复  引用  查看    

#9楼  2005-03-06 15:48 sumtec      
@问题男:

呵呵,我确实没有看清楚你说的,罪过罪过。

不过你关于是否称之为bug的说法,我还是有所保留。我认为这个可能不是Intel的bug,因为在x86体系里面,并没有规定某个寄存器或者某个内存变量是“有符号”的还是“无符号”的,甚至连指令也不区分是否有符号。

而影响各标志位的主要有两大类型,一种是专门用于产生标志的指令,例如cmp等,还有一些则是运算指令,例如add等。cmp实际上应该是通过sub产生的(只不过不把结果防盗目的操作数上面),这里也不讨论cmp。

对于add来说,例如0x7FFFFFFF + 0x7FFFFFFF = 0xFFFFFFFE,你说它是正数还是负数呢?说不清楚的,但是0x7FFFFFFF是正数总没有错吧?两个正数相加之后的结果应该是正数吧?此外jge的本意应该是计算结果大于等于零则跳转(不知道你对我这个说法有没有什么异议),因此在这里jge就应该跳。

我觉得还是微软的错误,撇开jge到底intel错了没错,MS也不应该出现if (a<0) 与我们的意图不一致的情况。至少不能够是可能会失效,也可能不会。我可不管MS跟Intel之间有什么毛病,我只知道MS给我的东西不是我想要的,也不是他所声称的那样。而且我也不明白,既然jns能够避免这个问题,有时候却要用jge来判断呢?

其实我们的困惑还是一样的,呵呵。
  回复  引用  查看    

#10楼  2005-03-06 15:54 kaneboy [未注册用户]
I'll send an email to a JIT team member. :)
  回复  引用    

#11楼  2005-03-06 16:55 问题男 [未注册用户]
我一开始就明白你的意思,但是当ms选择使用jge时,逻辑上是没有问题的阿,因此我觉得这个还算不上bug,呵呵

关于“因为在x86体系里面,并没有规定某个寄存器或者某个内存变量是有符号的还是无符号的,甚至连指令也不区分是否有符号”,正是我们的分歧之一,数据,在我看来,就是一坨bits,自然是没有意义的,更谈不上符号了,但是,指令是有意义的,自然也包括“有符号”这个意义,来看一下ia32 manual上关于jcc的一段:
。。。
JGE rel16/32 Jump near if greater or equal (SF=OF)
JL rel16/32 Jump near if less (SF<>OF)
JLE rel16/32 Jump near if less or equal (ZF=1 or SF<>OF)
JNA rel16/32 Jump near if not above (CF=1 or ZF=1)
JNAE rel16/32 Jump near if not above or equal (CF=1)
JNB rel16/32 Jump near if not below (CF=0)
JNBE rel16/32 Jump near if not below or equal (CF=0 and ZF=0)
。。。
以及intel的一段comment:
The conditions for each Jcc mnemonic are given in the “Description” column of the table on the
preceding page. The terms “less” and “greater” are used for comparisons of signed integers and
the terms “above” and “below” are used for unsigned integers.
可见,指令是“有符号”的

值得一提的是,当jcc的上一步是诸如add的指令是,设置的标志位能不能给予下一步操作正确的信息在某些情况下是未可知的(此例便是)。我们知道,jcc指令的逻辑功能实际上不仅仅是依赖一条指令完成的,基于上下文,不使用常规的“cmp、sub”等比较有符号数,而使用add来做,能不能在溢出情况下也得到预期的结果在逻辑上实在有些绕人,我是想不过来了,呵呵
  回复  引用    

#12楼  2005-03-06 17:00 问题男 [未注册用户]
quote:
“此外jge的本意应该是计算结果大于等于零则跳转(不知道你对我这个说法有没有什么异议),因此在这里jge就应该跳。”
=========================================
没错~~~

错就错在intel含糊其辞、ms自作聪明了,上一步两个有符正数用add指令运算,jge还能不能表达他的本义了呢?如果不能,错在谁?我看,谁都没错,或者都有错,呵呵,你看是不是

因此我还是建议提交这个问题,以避免这一尴尬的局面
  回复  引用    

#13楼  2005-03-06 17:18 问题男 [未注册用户]
@Justin Shen:
quote:
overflow不能算是错误,某些散列算法中,要有意overflow才行
==========================================
x86 flag register的overflow和carry是不同的两个概念,有些hash算法里面需要的是carry而非overflow

x86 flag register的overflow位表示的是“次高位向最高位进位(或借位) 异或 最高位向carry flag进位(或借位)”这一命题
  回复  引用    

#14楼  2005-03-08 11:03 kaneboy [未注册用户]
这个已经确认是一个JIT的Bug,在即将发布的VS2005 Beta2中,将修复这个Bug。

Thanks, sumtec !
  回复  引用    


标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2005-03-06 11:32 编辑过


相关链接:

历史上的今天:
2004-03-06 >>强人Anders的谈话