<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>tanglei.name | 酷 壳 - CoolShell</title>
	<atom:link href="https://coolshell.cn/articles/author/tanglei-name/feed" rel="self" type="application/rss+xml" />
	<link>https://coolshell.cn</link>
	<description>享受编程和技术所带来的快乐 - Coding Your Ambition</description>
	<lastBuildDate>Tue, 14 Jul 2020 04:31:30 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.2</generator>
	<item>
		<title>计时攻击 Timing Attacks</title>
		<link>https://coolshell.cn/articles/21003.html</link>
					<comments>https://coolshell.cn/articles/21003.html#comments</comments>
		
		<dc:creator><![CDATA[tanglei.name]]></dc:creator>
		<pubDate>Sun, 05 Jul 2020 05:26:52 +0000</pubDate>
				<category><![CDATA[程序设计]]></category>
		<category><![CDATA[网络安全]]></category>
		<category><![CDATA[HMAC]]></category>
		<guid isPermaLink="false">https://coolshell.cn/?p=21003</guid>

					<description><![CDATA[<p>本文来自读者“程序猿石头”的投稿文章《这 10 行比较字符串相等的代码给我整懵了，不信你也来看看》，原文写的很好，但不够直接了当，信息密度不够高，所以我对原文进...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/21003.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/21003.html">计时攻击 Timing Attacks</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><img decoding="async" loading="lazy" class="alignright wp-image-21015 " src="https://coolshell.cn/wp-content/uploads/2020/06/time-bomb-300x300.png" alt="" width="240" height="240" srcset="https://coolshell.cn/wp-content/uploads/2020/06/time-bomb-300x300.png 300w, https://coolshell.cn/wp-content/uploads/2020/06/time-bomb-150x150.png 150w, https://coolshell.cn/wp-content/uploads/2020/06/time-bomb-200x200.png 200w, https://coolshell.cn/wp-content/uploads/2020/06/time-bomb-270x270.png 270w, https://coolshell.cn/wp-content/uploads/2020/06/time-bomb.png 512w" sizes="(max-width: 240px) 100vw, 240px" />本文来自读者“程序猿石头”的投稿文章《<a href="http://mp.weixin.qq.com/s?__biz=MzI3OTUzMzcwNw==&amp;mid=100002290&amp;idx=1&amp;sn=8829db16a065f485b257fba0c691d94f&amp;chksm=6b4708165c30810096133f36523c8c781ce5333d851c31905d6cc49dd9b756a3f08141fbc9e8#rd" target="_blank" rel="noopener noreferrer">这 10 行比较字符串相等的代码给我整懵了，不信你也来看看</a>》，原文写的很好，但不够直接了当，信息密度不够高，所以我对原文进行大量的删减、裁剪、改写和添加，主要删除了一些没有信息的段落，主要加入了如何实施计时攻击相关的其它内容，让这篇文章中的知识更系统一些，并且还指出了其它的一些问题。所以，我把标题也改成了《计时攻击 Timing Attacks》。</p>
<h4>另类的字符串比较</h4>
<p>在 Java 的 Play Framework 里有<a href="https://github.com/playframework/play1/blob/b01eb02eb8df2e94cac2793c028dd9c4c5a57b31/framework/src/play/mvc/CookieDataCodec.java#L82" target="_blank" rel="noopener noreferrer">一段代码</a>用来验证cookie(session)中的数据是否合法（包含签名的验证）的代码，如下所示：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="java">boolean safeEqual(String a, String b) {
   if (a.length() != b.length()) {
       return false;
   }
   int equal = 0;
   for (int i = 0; i &lt; a.length(); i++) {
       equal |= a.charAt(i) ^ b.charAt(i);
   }
   return equal == 0;
}</pre>
<p>相信刚看到这段源码的人会感觉挺奇怪的，这个函数的功能是比较两个字符串是否相等，如果要判断两个字符串是否相等，正常人的写法应该是下面这个样子的（来自JDK8 的 <code>String.equals()</code>-有删减）：</p>
<p><span id="more-21003"></span></p>
<pre class="EnlighterJSRAW" data-enlighter-language="java" data-enlighter-highlight="9,10">public boolean equals(Object anObject) {
    String anotherString = (String)anObject;
    int n = value.length;
    if (n == anotherString.value.length) {
        char v1[] = value;
        char v2[] = anotherString.value;
        int i = 0;
        while (n-- != 0) {
            if (v1[i] != v2[i]) // &lt;- 遇到第一个不一样的字符时退出
                return false;
            i++;
        }
        return true;
    }
    return false;
}</pre>
<p>我们可以看到，在比较两个字符串是否相等的正常写法是：</p>
<ol>
<li>先看一下两个字符串长度是否相等，如果不等直接返回 false。</li>
<li>如果长度相等，则依次判断每个字符是否相等，如果不等则返回 false。</li>
<li>如果全部相等，则返回 true。一旦遇到不一样的字符时，直接返回false。</li>
</ol>
<p>然而，Play Framework里的代码却不是这样的，尤其是上述的第2点，用到了异或，熟悉位操作的你很容易就能看懂，通过异或操作 <code>1^1=0</code> , <code>1^0=1</code>, <code>0^0=0</code>，来比较每一位，如果每一位都相等的话，两个字符串肯定相等，最后存储累计异或值的变量 <code>equal</code>必定为 0（因为相同的字符必然为偶数），否则为 1。</p>
<p>但是，这种异或的方式不是遇到第一个不一样的字符就返回 false 了，而是要做全量比较，这种比较完全没有效率，这是为什么呢？原因是为了安全。</p>
<h4>计时攻击(Timing Attack)</h4>
<p>计时攻击（<a href="https://en.wikipedia.org/wiki/Timing_attack" target="_blank" rel="noopener noreferrer">Wikipedia</a>）是<a href="https://en.wikipedia.org/wiki/Side-channel_attack" target="_blank" rel="noopener noreferrer">旁道攻击</a>(或称&#8221;侧信道攻击&#8221;， Side Channel Attack， 简称SCA) 的一种，<b>旁通道攻击</b>是指基于从计算机系统的实现中获得的信息的任何攻击 ，而不是基于实现的算法本身的弱点（例如，密码分析和软件错误）。时间信息，功耗，电磁泄漏甚至声音可以提供额外的信息来源，可以加以利用。在很多物理隔绝的环境中（黑盒），往往也能出奇制胜，这类新型攻击的有效性远高于传统的密码分析的数学方法。（注：企图通过社会工程学欺骗或强迫具有合法访问权限的人来破坏密码系统通常不被视为旁道攻击）</p>
<p>计时攻击是最常用的攻击方法。那么，正常的字符串比较是怎么被黑客进行时间攻击的呢？</p>
<p>我们知道，正常的字符串比较，一旦遇到每一个不同的字符就返回失败了，所以，理论上来说，前面只有2个字符相同字符串比较的耗时，要比前面有10个字符相同的比较要短。你会说，这能相差多少呢？可能几微秒吧。但是，我们可以放大这个事。比如，在Web应用时，记录每个请求的返回所需请求时间（一般是毫秒级），如果我们重复50次，我们可以查看平均时间或是p50的时间，以了解哪个字符返回的时间比较长，如果某个我们要尝试的字符串的时间比较长，我们就可以确定地得出这个这字符串的前面一段必然是正确的。（当然，你会说网络请求的燥音太多了，在毫秒级的请求上完全没办判断，这个需要用到统计学来降噪，后面会给出方法）</p>
<p>这个事情，可以用来做HMAC的攻击，所谓HMAC，你可以参看本站的《<a title="HTTP API 认证授权术" href="https://coolshell.cn/articles/19395.html" target="_blank" rel="noopener noreferrer">HTTP API 认证授权术</a>》文章了解更多的细节。简单来说，HMAC，就是客户端向服务端发来一个字符串和其签名字符串（HMAC），然后，服务端的程序用一个私钥来对客户端发来的字符串进行签名得到签名字符串，然后再比较这个签名字符串（所谓签名，也就是使用MD5或SHA这样的哈希算法进行编码，是不可逆的）</p>
<p>写成伪代码大概是这个样子：</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">bool verify(message, digest) {
    my_digest = HMAC(key, message);
    return my_digest.equals(digest) ;
}</pre>
<p>于是，攻击者在不知道签名算法和私钥的情况下，但是知道API的调用接口时，就可以通过一边穷举签名，一边统计调用时间的方式来非常有效率的破解签名。在这篇文章《<a href="http://www.eggie5.com/45-hmac-timing-attacks" target="_blank" rel="noopener noreferrer">HMAC Timing Attacks</a>》中记录了整个攻击的过程。文章中记载：</p>
<p>如果一个签名有40个长度，如：<code>f5acdffbf0bb39b2cdf59ccc19625015b33f55fe</code> 攻击者，从 <code>0000000000000000000000000000000000000000</code> 开始穷举，下面是穷举第一个字符（从<code>0</code>到<code>f</code>因为这是HMAC算法的取值范围）的时间统计。</p>
<pre>0 0.005450913
1 0.005829198
2 0.004905407
3 0.005286876
4 0.005597611
5 0.004814430
6 0.004969118
7 0.005335884
8 0.004433182
9 0.004440246
a 0.004860263
b 0.004561121
c 0.004463188
d 0.004406799
e 0.004978907
f 0.004887240
</pre>
<p>可以看到，第一次测试通过的计时结果（以秒为单位），而值“ f”与样品的其余部分之间没有较大的变化量，所有结果看起来都非常接近。换句话说，有很多噪声掩盖了信号。因此，有必要进行多个采样（对测试进行缩放）并使用统计工具从噪声中滤除信号。为了将信号与噪声分开，我们必须按任意常数对测试进行缩放。通过实验，作者发现500是一个很好的数字。换句话说：运行测试500次，并记录500个试验中每个试验的结果。然后，通过人的肉眼观察可以可能看到 f 的调用明显比别的要长，但是这种方法很难自动化。</p>
<p>所以，作者给了另一个统计算法，这个算法向服务器分别从 0 到 f 发出16个请求，并记录每个请求的响应时间，并将它们排序为1-16，其中1是最长（最慢）的请求，而16是最短（最快的请求），分别记录 0 &#8211; f 的名次，然后重复上述的过程 500 次。如下所示（仅显示25个样本，字符“ 0”首先被排名7、1、3，然后再次排名3……）：</p>
<pre>{
"0"=&gt;[7, 1, 3, 3, 15, 5, 4, 9, 15, 10, 13, 2, 14, 9, 4, 14, 7, 9, 15, 2, 14, 9, 14, 6, 11...],
"1"=&gt;[13, 4, 7, 11, 0, 4, 0, 2, 14, 11, 6, 7, 2, 2, 14, 11, 8, 10, 5, 13, 11, 7, 4, 9, 3...],
"2"=&gt;[14, 5, 15, 5, 1, 0, 3, 1, 9, 12, 4, 4, 1, 1, 8, 6, 9, 4, 9, 5, 8, 3, 12, 8, 5...],
"3"=&gt;[15, 2, 9, 7, 2, 1, 14, 11, 7, 8, 8, 1, 4, 7, 12, 15, 13, 0, 4, 1, 7, 0, 3, 0, 0...],
"4"=&gt;[12, 10, 14, 15, 8, 9, 10, 12, 10, 4, 1, 13, 15, 15, 3, 1, 6, 8, 2, 6, 15, 4, 0, 3, 2...],
"5"=&gt;[5, 13, 13, 12, 7, 8, 13, 14, 3, 13, 2, 12, 7, 14, 2, 10, 12, 5, 8, 0, 4, 10, 5, 10, 12...]
"6"=&gt;[0, 15, 11, 13, 5, 15, 8, 8, 4, 7, 12, 9, 10, 11, 11, 7, 0, 6, 0, 9, 2, 6, 15, 13, 14...]
"7"=&gt;[1, 9, 0, 10, 6, 6, 2, 4, 12, 9, 5, 10, 5, 10, 7, 2, 4, 14, 6, 7, 13, 11, 6, 12, 4...],
"8"=&gt;[4, 0, 2, 1, 9, 11, 12, 13, 11, 14, 0, 15, 9, 0, 0, 13, 11, 13, 1, 8, 6, 5, 11, 15, 7...],
"9"=&gt;[11, 11, 10, 4, 13, 7, 6, 3, 2, 2, 14, 5, 3, 3, 15, 9, 14, 7, 10, 3, 0, 14, 1, 5, 15...],
"a"=&gt;[8, 3, 6, 14, 10, 2, 7, 5, 1, 3, 3, 0, 0, 6, 10, 12, 15, 12, 12, 15, 9, 13, 13, 11, 9...],
"b"=&gt;[9, 12, 5, 8, 3, 3, 5, 15, 0, 6, 11, 11, 12, 8, 1, 3, 1, 11, 11, 14, 5, 1, 2, 1, 6...],
"c"=&gt;[6, 7, 8, 2, 12, 10, 9, 10, 6, 1, 10, 8, 6, 4, 6, 4, 3, 2, 7, 11, 1, 8, 7, 2, 13...],
"d"=&gt;[2, 14, 4, 0, 14, 12, 11, 0, 8, 0, 15, 3, 8, 12, 5, 0, 10, 1, 3, 4, 12, 12, 8, 14, 8...],
"e"=&gt;[10, 8, 12, 6, 11, 13, 1, 6, 13, 5, 7, 14, 11, 5, 9, 5, 2, 15, 14, 10, 10, 2, 10, 4, 1...],
"f"=&gt;[3, 6, 1, 9, 4, 14, 15, 7, 5, 15, 9, 6, 13, 13, 13, 8, 5, 3, 13, 12, 3, 15, 9, 7, 10...]
}</pre>
<p>然后将每个字符的500个排名进行平均，得出以下示例输出：</p>
<pre>"f", 5.302
"0", 7.17
"6", 7.396
"3", 7.472
"5", 7.562
"a", 7.602
"2", 7.608
"8", 7.626
"9", 7.688
"b", 7.698
"1", 7.704
"e", 7.812
"4", 7.82
"d", 7.826
"7", 7.854
"c", 7.86</pre>
<p>于是，<code>f</code> 就这样脱颖而出了。然后，再对剩余的39个字符重复此算法。</p>
<p><strong>这是一种统计技术，可让我们从噪声中滤出真实的信号</strong>。因此，总共需要调用：16 * 500 * 40 = 320,000个请求，而蛮力穷举需要花费16 ^ 40个请求。</p>
<p>另外，学术界的这篇论文就宣称用这种计时攻击的方法破解了 OpenSSL 0.9.7 的RSA加密算法了。这篇 <a href="http://crypto.stanford.edu/~dabo/papers/ssl-timing.pdf" target="_blank" rel="noopener noreferrer">Remote Timing Attacks are Practical （PDF）</a>论文中指出（我大致翻译下摘要，感兴趣的同学可以通过链接去看原文）：</p>
<blockquote><p>计时攻击往往用于攻击一些性能较弱的计算设备，例如一些智能卡。我们通过实验发现，也能用于攻击普通的软件系统。本文通过实验证明，通过这种计时攻击方式能够攻破一个基于 OpenSSL 的 web 服务器的私钥。结果证明计时攻击用于进行网络攻击在实践中可行的，因此各大安全系统需要抵御这种风险。</p></blockquote>
<p>参考资料：</p>
<ul>
<li>
<section><a href="http://www.cs.sjsu.edu/faculty/stamp/students/article.html">Timing Attacks on RSA: Revealing Your Secrets through the Fourth Dimension</a></section>
</li>
<li>
<section><a href="http://crypto.stanford.edu/~dabo/papers/ssl-timing.pdf">Remote Timing Attacks are Practical</a></section>
</li>
</ul>
<h4>各语言的对应函数</h4>
<p>下面，我们来看看各个语言对计时攻击的对应函数</p>
<p><strong>PHP</strong>: <a href="https://wiki.php.net/rfc/timing_attack" target="_blank" rel="noopener noreferrer">https://wiki.php.net/rfc/timing_attack</a></p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">bool hash_equals ( string $known_string , string $user_string )

boolean password_verify ( string $password , string $hash )</pre>
<p><strong>Java</strong>:  Java 在1.6时是有问题的，其在 1.6.0._17(6u17)才Fix了这个问题（<a href="http://hg.openjdk.java.net/jdk6/jdk6/jdk/rev/562da0baf70b" target="_blank" rel="noopener noreferrer">相关的fix patch</a>），下面是 <a href="https://hg.openjdk.java.net/jdk8u/jdk8u-dev/jdk/file/1832c29655eb/src/share/classes/java/security/MessageDigest.java#l442" target="_blank" rel="noopener noreferrer">JDK8源码</a> &#8211; <code>MessageDigest.isEqual()</code></p>
<pre class="EnlighterJSRAW" data-enlighter-language="java">public static boolean MessageDigest.isEqual(byte[] digesta, byte[] digestb) {
    if (digesta == digestb) return true;
    if (digesta == null || digestb == null) {
        return false;
    }
    if (digesta.length != digestb.length) {
        return false;
    }

    int result = 0;
    // time-constant comparison
    for (int i = 0; i &lt; digesta.length; i++) {
        result |= digesta[i] ^ digestb[i];
    }
    return result == 0;
}</pre>
<p><strong>C/C++</strong>：没有在常用的库中找到相关的函数，还是自己写吧。</p>
<pre class="EnlighterJSRAW" data-enlighter-language="c">int util_cmp_const(const void * a, const void *b, const size_t size) 
{
  const unsigned char *_a = (const unsigned char *) a;
  const unsigned char *_b = (const unsigned char *) b;
  unsigned char result = 0;
  size_t i;

  for (i = 0; i &lt; size; i++) {
    result |= _a[i] ^ _b[i];
  }

  return result; /* returns 0 if equal, nonzero otherwise */
}</pre>
<p><strong>Python</strong> &#8211; 2.7.7+使用 <code>hmac.compare_digest(a, b)</code>，否则，使用如下的Django的代码</p>
<pre class="EnlighterJSRAW" data-enlighter-language="python">#Taken from Django Source Code

def constant_time_compare(val1, val2):
    """
    Returns True if the two strings are equal, False otherwise.

    The time taken is independent of the number of characters that match.

    For the sake of simplicity, this function executes in constant time only
    when the two strings have the same length. It short-circuits when they
    have different lengths.
    """
    if len(val1) != len(val2):
        return False
    result = 0
    for x, y in zip(val1, val2):
        result |= ord(x) ^ ord(y)
    return result == 0</pre>
<p><strong>Go</strong>  &#8211; 使用 <code>crypto/subtle</code> 代码包</p>
<pre class="EnlighterJSRAW" data-enlighter-language="golang">func ConstantTimeByteEq(x, y uint8) int
func ConstantTimeCompare(x, y []byte) int
func ConstantTimeCopy(v int, x, y []byte)
func ConstantTimeEq(x, y int32) int
func ConstantTimeLessOrEq(x, y int) int
func ConstantTimeSelect(v, x, y int) int</pre>
<h4>One More Thing</h4>
<p>在文章结束前，再提一个事。</p>
<p>上面的所有的代码都还有一个问题——他们都要判断字符串的长度是否一致，如果不一致就返回了，所以，通过时间攻击是可以知道字符串的长度的。比如：你的密码长度。理论上来说，字符串的长度也应该属于“隐私数据”（当然，对于签名则不是）。</p>
<p>(全文完)<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" id="wp_rp_first"><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/21708.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2022/01/iStock-1175502114-150x150.png" alt="网络数字身份认证术" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21708.html" class="wp_rp_title">网络数字身份认证术</a></li><li ><a href="https://coolshell.cn/articles/21672.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2021/12/bachelor-mechanical-eng-icon@72x-150x150.png" alt="我做系统架构的一些原则" width="150" height="150" /></a><a href="https://coolshell.cn/articles/21672.html" class="wp_rp_title">我做系统架构的一些原则</a></li><li ><a href="https://coolshell.cn/articles/19395.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2019/05/Authorization-360x200-1-150x150.png" alt="HTTP API 认证授权术" width="150" height="150" /></a><a href="https://coolshell.cn/articles/19395.html" class="wp_rp_title">HTTP API 认证授权术</a></li><li ><a href="https://coolshell.cn/articles/1204.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/3.jpg" alt="Python也Spring了" width="150" height="150" /></a><a href="https://coolshell.cn/articles/1204.html" class="wp_rp_title">Python也Spring了</a></li><li ><a href="https://coolshell.cn/articles/5107.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/5.jpg" alt="10大经典错误" width="150" height="150" /></a><a href="https://coolshell.cn/articles/5107.html" class="wp_rp_title">10大经典错误</a></li><li ><a href="https://coolshell.cn/articles/780.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/17.jpg" alt="如何知道某网站运行在GAE上" width="150" height="150" /></a><a href="https://coolshell.cn/articles/780.html" class="wp_rp_title">如何知道某网站运行在GAE上</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/21003.html">计时攻击 Timing Attacks</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/21003.html/feed</wfw:commentRss>
			<slash:comments>46</slash:comments>
		
		
			</item>
		<item>
		<title>一个浮点数跨平台产生的问题</title>
		<link>https://coolshell.cn/articles/11235.html</link>
					<comments>https://coolshell.cn/articles/11235.html#comments</comments>
		
		<dc:creator><![CDATA[tanglei.name]]></dc:creator>
		<pubDate>Sat, 15 Mar 2014 12:44:24 +0000</pubDate>
				<category><![CDATA[.NET编程]]></category>
		<category><![CDATA[C/C++语言]]></category>
		<category><![CDATA[.NET]]></category>
		<category><![CDATA[C++]]></category>
		<category><![CDATA[float]]></category>
		<category><![CDATA[FPU]]></category>
		<category><![CDATA[SSE]]></category>
		<guid isPermaLink="false">http://coolshell.cn/?p=11235</guid>

					<description><![CDATA[<p>感谢网友唐磊（微博@唐磊_name）投稿，本文原文在唐磊的博客上（原文地址），原文分析还不够好，而且可能对人有误导，所以，我对原文做了很多修改，并加了Linux...</p>
<p class="read-more"><a class="btn btn-default" href="https://coolshell.cn/articles/11235.html"> Read More<span class="screen-reader-text">  Read More</span></a></p>
The post <a href="https://coolshell.cn/articles/11235.html">一个浮点数跨平台产生的问题</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></description>
										<content:encoded><![CDATA[<p><script async src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-3415450859608158"
     crossorigin="anonymous"></script><strong>感谢网友<a href="http://www.tanglei.name/" target="_blank">唐磊</a>（微博@<a title="唐磊_name" href="http://weibo.com/tangleithu?from=feed&amp;loc=nickname">唐磊_name</a>）投稿，本文原文在唐磊的博客上（<a href="http://www.tanglei.name/a-bug-relate-with-float-point-between-x86-and-x64-in-csharp/">原文地址</a>），原文分析还不够好，而且可能对人有误导，所以，我对原文做了很多修改，并加了Linux下的内容。浮点数是一个很复杂的事情，希望这篇文章有助于大家了解浮点数与其相关的C/C++的编译选项。</strong>（注：我没有Windows 32位以及C#的环境，所以，对于Windows 32位的程序和C#的程序没有验证过）</p>
<p>背景就简单点儿说，最近一个项目C#编写，涉及浮点运算，来龙去脉省去，直接看如下代码。</p>
<pre data-enlighter-language="csharp" class="EnlighterJSRAW">float p3x = 80838.0f;
float p2y = -2499.0f;
double v321 = p3x * p2y;
Console.WriteLine(v321);</pre>
<p>很简单吧，马上笔算下结果为-202014162，没问题，难道C#没有产生这样的结果？不可能吧，开启Visual Studio，copy代码试试，果然结果是-202014162。就这样完了么？显然没有！你把编译时的选项从AnyCPU改成x64试试~(服务器环境正是64位滴哦！！)结果居然边成了-202014160，对没错，就是-202014160。有点不相信，再跑两遍，仍然是-202014160。呃，想通了，因为浮点运算的误差，-202014160这个结果是合理的。</p>
<p>为什么合理呢？很正常，因为上面的p3x和p2y是两个float类型，虽然v321是double，但也是两个float类型计算完后再转成double的，<strong>float的精度本来也只有7位，所以，对于这个上亿的数，自然没有办法保证精度</strong>。</p>
<p><strong>但是为什么修改CPU的type会有不同的效果？</strong>嗯，我们再试试C/C++。</p>
<p><span id="more-11235"></span></p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#include
using namespace std;

int main()
{
    float p3x = 80838.0f;
    float p2y = -2499.0f;
    double v321 = p3x * p2y;
    std::cout.precision(15);
    std::cout &lt;&lt; v321 &lt;&lt; std::endl;

    return 0;
}
</pre>
<p>上面这段C++代码在不同的平台下的结果如下：</p>
<ul>
<li>Windows 32/64位下：-202014160</li>
<li>Linux 64位下（CentOS 6 gcc 4.4.7）-202014160，</li>
<li>Linux 32位下（Ubuntu 12.04+ gcc 4.6.3）是：-202014162</li>
</ul>
<p><strong>合理的结果应该是-202014160，正确的运算结果是-202014162</strong>，合理性是浮点精度不够造成的（文后解释了合理性）。若是用两个double相乘可得正确且合理的运算结果（注：把上面C++的程序中的p3x和p2y的类型声明成double，就能得到正确的结果，因为double是双精度的，float是单精度，所以double有足够的位数存放更多的数位）。<strong>但是我们有点不明白，为什么Linux 32位下，居然能算出“正确”的数，而不是“合理”的数</strong>。</p>
<p>与C++一样，C#在32位和64位（DEBUG下，这个后面会说）下没有得到一致的结果，那我们来看一下C++/C#的汇编代码（使用gdb的disassemble /m main 命令，另外下面只显示 float * float 然后转成double的那一行代码的汇编）</p>
<p><strong>Linux平台下用G++编译</strong></p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">//C++ 32位系统下 Ubuntu 12.04
8	    double v321 = p3x * p2y;
   0x0804860f &lt;+27&gt;:	flds   0x18(%esp)
   0x08048613 &lt;+31&gt;:	fmuls  0x1c(%esp)
   0x08048617 &lt;+35&gt;:	fstpl  0x10(%esp)

.......</pre>
<pre data-enlighter-language="c" class="EnlighterJSRAW">//C++ 64位系统下 CentOS 6
9           double v321 = p3x * p2y;
   0x000000000040083c &lt;+24&gt;:    movss  -0x20(%rbp),%xmm0
   0x0000000000400841 &lt;+29&gt;:    mulss  -0x1c(%rbp),%xmm0
   0x0000000000400846 &lt;+34&gt;:    unpcklps %xmm0,%xmm0
   0x0000000000400849 &lt;+37&gt;:    cvtps2pd %xmm0,%xmm0
   0x000000000040084c &lt;+40&gt;:    movsd  %xmm0,-0x18(%rbp)</pre>
<p><strong>Windows平台下用Visual Studio编译</strong></p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">//C# AnyCPU编译，Windows VS2012
double v321 = p3x * p2y;
00000049  fld         dword ptr [ebp-40h]
0000004c  fmul        dword ptr [ebp-44h]
0000004f  fstp        qword ptr [ebp-4Ch]</pre>
<pre data-enlighter-language="c" class="EnlighterJSRAW">//C# X64位编译 Windows7 VS2012
double v321 = p3x * p2y;&lt;/pre&gt;
009B43B8 movss xmm0,dword ptr [p3x]
009B43BD mulss xmm0,dword ptr [p2y]
009B43C2 cvtss2sd xmm0,xmm0
009B43C6 movsd mmword ptr [v321],xmm0</pre>
<p>从上面的汇编代码可以看出，无论是Linux和Windows，C++或C# 32位和64对浮点数的汇编指令并不一样。 32位生成代码用的指令是fld/fmul/fstp等，而64位下的使用了movss/mulss/movsd/的指令。看下来，似乎这个事情和平台有关系。</p>
<p>我们继续调查，我们发现，其中fld/fmul/fstp等指令是由<strong>FPU</strong>(float point unit)浮点运算处理器做的，准确的说，是FPU x87指令，FPU在进行浮点运算时，用了<strong>80位</strong>的寄存器做相关浮点运算，然后再根据是float/double截取成32位或64位，FPU默认上会尽量减少由于需要四舍五入带来的精度问题。可参看浮点运算标准<a href="http://en.wikipedia.org/wiki/IEEE_floating_point" target="_blank">IEEE-754</a> 推荐标准实现者提供浮点可扩展精度格式(<a href="http://en.wikipedia.org/wiki/Extended_precision" target="_blank">Extended precision</a>)，Intel x86处理器有FPU(float point unit)浮点运算处理器支持这种扩展。</p>
<p>非FPU的情况是用了SSE中128位寄存器(float实际只用了其中的32位，计算时也是以32位计算的)，这就是导致上述问题产生的最终原因。详细分析见文末说明。</p>
<p>知道了这一点，我们可以man g++ 看一下文档，我们可以找到一个编译选项叫：<strong>-mfpmath，在32位下，这个编译选项的默认值是：387，也就是x87 FPU指令，在64位下，这个编译选项的值是sse，也就是使用SSE的指令</strong>。所以，就这篇文章中的这个例子而言，如果你在64bits下加上如 -mfpmath=387，你会得到“正确的”结果，而不是“合理的”结果。</p>
<p>而在VS2012中C++，<a href="http://msdn.microsoft.com/zh-cn/library/vstudio/e7s85ffb(v=vs.110).aspx" target="_blank">编译选项可以设置(代码生成中)</a>可选，/fp:[precise | fast | strict]，本例中Release 32位下用precise 或者 strict将得到合理的结果(-202014160)，fast将产生正确的结果(-202014162), fast debug/release下结果也不一样哦(release下才优化了)。64系统下各个结果可以大家自己去测试下(Debug/Release)，分别看看VS编译后产生的中间代码长什么样。（陈皓注：我的VS2012在debug编译下，无论你怎么设置/fp的参数值，汇编都是一样的，使用SSE指令，而Release就不一样了，但是我的release下看代码的汇编非常怪异和源代码对上号，多年不用Windows开发了，对VS的使用仅停留在VC6++/VC2005上）</p>
<p>所以，我们在从x87 FPU指令向SSE指令做代码移植的时候，我们可能会遇到向这样的浮点数的精度问题，这个精度问题会多次科学计算中会更糟糕。<strong>这个问题并不简单的只是在32位和64位中的系统出算，这个问题主要还是看语言编译器的实现</strong>。在更为高级的语言中，如：C99或Fortran 2003中，引入了“long double”来做可扩展双精度（Extension Double），这样就可以消除更多的精度问题。</p>
<p>下面我们把程序改成long double，（注：其中的类型变成long double）</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">#include
using namespace std;

int main()
{
    long double p3x = 80838.0;
    long double p2y = -2499.0;
    long double v321 = p3x * p2y;
    std::cout.precision(15);
    std::cout &lt;&lt; v321 &lt;&lt; std::endl;

    return 0;
}</pre>
<p>用gdb的disassemble /m main你会看到其中的运算的汇编如下（使用了fmlp指令）：</p>
<pre data-enlighter-language="c" class="EnlighterJSRAW">//linux 32位系统
8	    long double v321 = p3x * p2y;
   0x08048633 &lt;+63&gt;:	fldt   0x10(%esp)
   0x08048637 &lt;+67&gt;:	fldt   0x20(%esp)
   0x0804863b &lt;+71&gt;:	fmulp  %st,%st(1)
   0x0804863d &lt;+73&gt;:	fstpt  0x30(%esp)
</pre>
<pre data-enlighter-language="c" class="EnlighterJSRAW">//linux 64位系统
8           long double v321 = p3x * p2y;
   0x0000000000400818 &lt;+52&gt;:    fldt   -0x30(%rbp)
   0x000000000040081b &lt;+55&gt;:    fldt   -0x20(%rbp)
   0x000000000040081e &lt;+58&gt;:    fmulp  %st,%st(1)
   0x0000000000400820 &lt;+60&gt;:    fstpt  -0x10(%rbp)
</pre>
<p><span style="line-height: 1.5em;">我们可以看到，32位系统和64位系统使用了同样的汇编指令（当然，我没有那么多物理机，我只是在VMWare Play的虚拟机上测试的，所以上面的示例并不一定适用于所有的地方，另外，C/C++语言和编译器和平台有非常大的关系） ，原因自然是我们用到了long double这个扩展双精度的数据类型。（注：如果你用double或float，在Linux上，32位用x87 FPU 指令编译，而64位用SSE指令编译）</span></p>
<p>好了，我们再回到C#上来，<span style="line-height: 1.5em;">C#的浮点是支持该标准的，其中</span><a style="line-height: 1.5em;" href="http://msdn.microsoft.com/en-us/library/aa691146(v=vs.71).aspx">其官方文档</a><span style="line-height: 1.5em;">也提到了浮点运算可能会产生比返回类型更高精度的值（正如上面的返回值精度就超过了float的精度），并说明如果硬件支持可扩展浮点精度的话，那么</span><strong style="line-height: 1.5em;">所有的</strong><span style="line-height: 1.5em;">浮点运算都将用此精度进行以提高效率，举个例子x*y/z, x*y的值可能都在double的能力范围之外了，但真实情况可能除以z后又能把结果拉回到double范围内，这样的话，用了FPU的结果就会得到一个准确的double值，而非FPU的就是无穷大之类的了。</span></p>
<p><span style="line-height: 1.5em;">所以，对于</span>C#来说，你显然无法找到一个像C/C++一样的利用编译器选项的来解决这个问题的“解决方案”（其实，用编译器参数是一个伪解决方案）<span style="line-height: 1.5em;">。</span></p>
<p><span style="line-height: 1.5em;"><strong>而且，要解决这个问题也不是要修改编译器选项，因为这个问题明显不是FPU或是SSE的问题，FPU是个过时的技术，SSE才是合理的技术，所以，<span style="color: #cc0000;">如果你不想你的浮点数在计算上有什么问题，而且你需要精度准确，正确的解决方案不是搞编译参数，而是——你一定要使用精度更高字节数更多的数据类型，比如：double 或是long double</span>。</strong></span></p>
<p>另外，大家在写代码的时候得保证实际运行环境/测试环境/开发环境的<strong>一致性(包括OS架构啊、编译选项等)</strong>啊（<strong>尤其是C/C++ 而且，编译器上的参数可能会有很多坑，而且有些坑可能会掩盖你程序中的问题</strong>），不然莫名其妙的问题会产生（本文就是开发环境与运行环境不一致导致的问题，纠结了好久才发现是这个原因）；遇到涉及浮点运算的时候别忘了有可能是这个原因产生的；<strong>float/double混用的情况得特别注意</strong>。</p>
<p><strong>Reference：</strong></p>
<p>[1] <a href="http://msdn.microsoft.com/en-us/library/aa691146(v=vs.71).aspx">C# Language Specification Floating point types</a><br />
[2] <a href="http://stackoverflow.com/questions/6683059/are-floating-point-numbers-consistent-in-c-can-they-be">Are floating-point numbers consistent in C#? Can they be? </a><br />
[3] <a href="http://www.plantation-productions.com/Webster/www.artofasm.com/Linux/HTML/RealArithmetica2.html">The FPU Instruction Set</a></p>
<h4><strong>附录</strong></h4>
<h5><strong>80838.0f * -2499.0f = -202014160.0浮点运算过程的说明</strong></h5>
<p>32位浮点数在计算机中的表示方式为：1位符号位(s)-8位指数位(E)-23位有效数字(M)。<br />
32位Float = (-1)^s * (1+m) * 2^(e-127), 其中e是实际转换成1.xxxxx*2^e的指数,m是前面的xxxxx(节约1位)</p>
<p>80838.0f = 1 0011 1011 1100 0110.0= 1.00111011110001100*2^16<br />
有效位M = 0011 1011 1100 0110 0000 000<br />
指数位E = 16 + 127 = 143 =  10001111<br />
内部表示 80838.0 =  0 [1000 1111] [0011 1011 1100 0110 0000 000]<br />
= 0100 0111 1001 1101 1110 0011 0000 0000<br />
= 47 9d e3 00 //实际调试时看到的内存值 可能是00 e3 9d 47是因为调试环境用了小端表示法法：低位字节排内存低地址端，高位排内存高地址</p>
<p>-2499.0 = -100111000011.0 = -1.001110000110 * 2^11<br />
有效位M = 0011 1000 0110 0000 0000 000<br />
指数位E = 11+127=138= 10001010<br />
符号位s = 1<br />
内部表示-2499.0 = 1 [10001010] [0011 1000 0110 0000 0000 000]<br />
=1100 0101 0001 1100 0011 0000 0000 0000<br />
=c5 1c 30 00</p>
<p>80838.0 * -2499.0 = ?</p>
<p>首先是指数 e = 11+16 = 27<br />
指数位E = e + 127 = 154 = 10011010<br />
有效位相乘结果为 1.1000 0001 0100 1111 1011 1010 01 //可以自己动手实际算下<br />
实际中只能有23位，后面的被截断即1000 0001 0100 1111 1011 101<span style="text-decoration: line-through;">0 01 </span><br />
相乘结果内部表示=1[10011010][1000 0001 0100 1111 1011 101]<br />
= 1100 1101 0100 0000 1010 0111 1101 1101<br />
= cd 40 a7 dd</p>
<p>结果 =  -1.1000 0001 0100 1111 1011 101 *2^27<br />
=  -11000 0001 0100 1111 1011 1010000<br />
=  -202014160<br />
再转成double后还是-202014160.</p>
<p>如果是FPU的话，上面的有效位结果不会被截断，即<br />
FPU结果 = -1.1000 0001 0100 1111 1011 101<strong>001</strong> *2^27<br />
= -11000 0001 0100 1111 1011 101<strong>001</strong>0<br />
= -202014162</p>
<p>全文完，若本文有纰漏之处欢迎指正。<!--



<p align="center"><a href= target=_blank><img decoding="async" src=""></a></p>





<p align="center"><img decoding="async" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.weixin.jpg"> <img decoding="async" loading="lazy" src="https://coolshell.cn/wp-content/uploads/2020/03/coolshell.mini_.jpg" width="300" height="300"> <br />关注CoolShell微信公众账号和微信小程序</p>

 

--></p>
<div style="margin-top: 15px; font-size: 16px;color: #cc0000;">
<p align="center"><strong>（转载本站文章请注明作者和出处 <a href="https://coolshell.cn/">酷 壳 &#8211; CoolShell</a> ，请勿用于任何商业用途）</strong></p>
</div>

<div class="wp_rp_wrap  wp_rp_vertical_m" ><div class="wp_rp_content"><h3 class="related_post_title">相关文章</h3><ul class="related_post wp_rp"><li ><a href="https://coolshell.cn/articles/3008.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/10.jpg" alt="Windows编程革命简史" width="150" height="150" /></a><a href="https://coolshell.cn/articles/3008.html" class="wp_rp_title">Windows编程革命简史</a></li><li ><a href="https://coolshell.cn/articles/2672.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/0.jpg" alt=".NET代码转换器" width="150" height="150" /></a><a href="https://coolshell.cn/articles/2672.html" class="wp_rp_title">.NET代码转换器</a></li><li ><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2020/03/rust-social-wide-150x150.jpg" alt="Rust语言的编程范式" width="150" height="150" /></a><a href="https://coolshell.cn/articles/20845.html" class="wp_rp_title">Rust语言的编程范式</a></li><li ><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2018/05/300x262-150x150.jpg" alt="程序员练级攻略（2018)  与我的专栏" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18360.html" class="wp_rp_title">程序员练级攻略（2018)  与我的专栏</a></li><li ><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/uploads/2017/07/api-design-300x278-2-150x150.jpg" alt="API设计原则 &#8211; Qt官网的设计实践总结" width="150" height="150" /></a><a href="https://coolshell.cn/articles/18024.html" class="wp_rp_title">API设计原则 &#8211; Qt官网的设计实践总结</a></li><li ><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_thumbnail"><img src="https://coolshell.cn/wp-content/plugins/wordpress-23-related-posts-plugin/static/thumbs/29.jpg" alt="Leetcode 编程训练" width="150" height="150" /></a><a href="https://coolshell.cn/articles/12052.html" class="wp_rp_title">Leetcode 编程训练</a></li></ul></div></div>The post <a href="https://coolshell.cn/articles/11235.html">一个浮点数跨平台产生的问题</a> first appeared on <a href="https://coolshell.cn">酷 壳 - CoolShell</a>.]]></content:encoded>
					
					<wfw:commentRss>https://coolshell.cn/articles/11235.html/feed</wfw:commentRss>
			<slash:comments>25</slash:comments>
		
		
			</item>
	</channel>
</rss>
