【问题标题】:gcc and clang produce different outputs while left-shifting with unsigned valuesgcc 和 clang 在使用无符号值左移时产生不同的输出
【发布时间】:2016-06-18 18:22:57
【问题描述】:

根据this interesting paper about undefined behavior optimization in c,表达式(x<<n)|(x>>32-n)“在n = 0 时在C 中执行未定义的行为”。 This stackoverflow discussion 确认负整数的行为未定义,并讨论了左移值的其他一些潜在缺陷。

考虑以下代码:

#include <stdio.h>
#include <stdint.h>

uint32_t rotl(uint32_t x, uint32_t n)
{
    return (x << n) | (x >> (32 - n));
}

int main()
{
    uint32_t y = rotl(10, 0);
    printf("%u\n", y);
    return 0;
}

使用以下参数编译:-O3 -std=c11 -pedantic -Wall -Wextra

  • 在 gcc >5.1.0 中,程序的输出是10
  • 在 clang >3.7.0 中,输出为4294967295

有趣的是,使用 c++ 编译时仍然如此:gcc resultsclang results

因此,我的问题如下:

  1. 根据标准中的语言,我的理解是,这应该调用未定义/实现定义的行为,因为这两个参数都是无符号整数,并且没有一个值是负数。它是否正确?如果不是,c11 和 c++11 标准的相关部分是什么?
  2. 如果前面的陈述为真,根据 c/c++ 标准,哪个编译器产生了正确的输出?直观地说,左移没有数字应该会给你返回值,即 gcc 输出的内容。
  3. 如果不是上述情况,为什么没有警告指出此代码可能会由于左移溢出而引发未定义行为?

【问题讨论】:

    标签: c++ c language-lawyer undefined-behavior


    【解决方案1】:

    来自 [expr.shift],强调我的:

    如果正确的操作数,则行为未定义 为负数,或大于或等于提升的左操作数的位长度

    你正在做:

    (x >> (32 - n))
    

    使用n == 0,因此您将 32 位数字右移 32。因此,UB。

    【讨论】:

    • 直接来自标准吗?为什么编译器不产生警告?
    • @ruser45381 是的,这是直接报价。可能只是无法弄清楚它需要警告(例如,只需输入x &gt;&gt; 32warns)。毕竟,编译器不必发出警告 - 这是一个 QoI 问题。
    • 它当然没有要警告,但是你最终会遇到链接论文提到的情况:在没有意识到的情况下调用未定义的行为并且没有好的方法优化代码以产生正确的结果。鉴于已经有so many ways 在 C 中使用 UB 杀死自己,我发现没有任何警告令人惊讶!
    • @ruser45381 是的,在理想的世界中,我们都会拥有完美的工具吗?我不知道该告诉你什么。
    • 我不是在攻击你!我只是发现结果令人惊讶,因为编译器可以在这种特殊情况下解决它。
    【解决方案2】:

    您的n0,因此执行x &lt;&lt; 32 未定义的行为,因为将uint32_t 移动32 位或更多位是未定义的。

    【讨论】:

      【解决方案3】:

      如果n 为0,则32-n 为32,并且由于x 有32 位,x&gt;&gt;(32-n) 为UB。

      链接的 SO 帖子中的问题有所不同。这与签名无关。

      【讨论】:

      • 这解释了未定义的行为,但是 noither 编译器对此给出警告的事实有点神秘。应该可以推断出 shift 是 32 或者更大吧?
      • @ruser45381:在这种情况下,它可以解决,但它没有义务这样做。未定义的行为是 undefined,而不是“编译器应该对此发出警告。”
      • 重点是这里的班次数是一个参数。为了弄清楚它必须执行一些静态分析,这不一定是它的功能。
      • @EugeneSh.: 奇怪的是,即使它确实进行静态分析并内联函数,将参数解析为常量(然后消除移位指令,因为它可以看到它不会op),它仍然不会产生警告消息。我想这与警告的实施方式有关。
      【解决方案4】:

      帖子的一部分没有完全回复。

      为什么没有警告表明此代码可能会由于左移溢出而引发未定义行为?

      查看add() 代码,编译器应该警告什么?如果总和超出INT_MIN ... INT_MAX 的范围,是否为UB。因为下面的代码没有采取预防措施防止溢出,like here,是不是应该警告?如果你这么认为,那么关于潜在的这个和那个的代码将会减少,程序员会很快关闭这个警告。

      int add(int a, int b) {
        return a + b;
      }
      

      这里的情况并没有太大的不同。如果n &gt; 0 &amp;&amp; n &lt; 32,则没有问题。

      uint32_t rotl(uint32_t x, uint32_t n) {
        return (x << n) | (x >> (32 - n));
      }
      

      C 创建快速代码主要是因为它缺少大量运行时错误检查,并且编译器能够执行非常好的优化代码。如果需要大量运行时检查,还有其他适合这些程序员的语言。

      C 是无网编码。

      【讨论】:

        【解决方案5】:

        在编写 C 标准时,某些实现在尝试执行非常大或负数的移位时会表现得很奇怪,例如左移 -1 可能会占用一个禁用中断的 CPU,而它的微码移动一个值 40 亿次,并且长时间禁用中断可能会导致其他系统故障。此外,虽然很少有实现会在完全按字长移动时做任何特别奇怪的事情,但实现对于返回的值并不一致。有些人会将其视为移位零,而其他人会产生与移位相同的结果,即字长倍数,有些人有时会做一个,有时会做另一个。

        如果标准的作者已经指定精确地改变字长可能会以未指定的方式在这两种可能的行为之间进行选择,那将是有用的,但标准的作者对指定所有内容不感兴趣编译器自然会有或没有授权。我认为他们没有考虑过这样的想法,即普通平台的实现不会自然地产生上面给出的“旋转”等表达式的普通行为,并且不想用这些细节混淆标准。

        然而,今天,一些编译器作者认为利用所有形式的 UB 进行“优化”比支持以前基本上所有常见实现都支持的有用的自然行为更为重要。当 y==0 时使“旋转”表达式发生故障是否允许编译器生成比其他情况更小的有用程序是无关紧要的。

        【讨论】:

          猜你喜欢
          • 2021-12-16
          • 2023-03-28
          • 2017-03-02
          • 2018-03-31
          • 1970-01-01
          • 2021-09-25
          • 2019-06-11
          • 2014-11-04
          • 2017-10-18
          相关资源
          最近更新 更多