【问题标题】:GCC left shift overflowGCC左移溢出
【发布时间】:2011-04-21 18:17:03
【问题描述】:

以下小程序在 Mac 上使用 GCC 版本 4.2.1(Apple Inc. build 5664)非常尴尬。

#include <stdio.h>

int main(){
        int x = 1 << 32;
        int y = 32;
        int z = 1 << y;
        printf("x:%d, z: %d\n", x, z);
}

结果是x:0, z: 1
知道为什么 x 和 z 的值不同吗?
非常感谢。

【问题讨论】:

  • 你是用 -Wall 编译的吗?
  • gcc 给出:“警告:左移计数 >= 类型宽度”用于 x 移
  • 我猜编译器会预先计算 (1 &lt;&lt; 32) 表达式,因为它是一个常量。
  • 我终于找到了答案。这是因为英特尔处理器将移位计数屏蔽为 5 位(即,将您的移位量和按位 OR 乘以 31)。
  • 我看到的最常见的原因是 ((x > S)),当 S 恰好最终为 0 时。请注意,要么行为会产生此计算所需的结果。

标签: c macos gcc


【解决方案1】:

简答:英特尔处理器将移位计数屏蔽为 5 位(最多 31 位)。也就是说,实际执行的移位是32&31,也就是0(没有变化)。

在 Linux 32 位 PC 上使用 gcc 会出现相同的结果。

我组装了这个程序的一个更短的版本,因为我很困惑为什么 32 位的左移会导致一个非零值:

int main(){
    int y = 32;
    unsigned int z = 1 << y;
    unsigned int k = 1;
    k <<= y;
    printf("z: %u, k: %u\n", z, k);
}

..使用命令gcc -Wall -o a.s -S deleteme.c(cmets是我自己的)

main:
leal    4(%esp), %ecx
andl    $-16, %esp
pushl   -4(%ecx)
pushl   %ebp
movl    %esp, %ebp
pushl   %ecx
subl    $36, %esp
movl    $32, -16(%ebp)  ; y = 32
movl    -16(%ebp), %ecx ; 32 in CX register
movl    $1, %eax        ; AX = 1
sall    %cl, %eax       ; AX <<= 32(32)
movl    %eax, -12(%ebp) ; z = AX
movl    $1, -8(%ebp)    ; k = 1
movl    -16(%ebp), %ecx ; CX = y = 32
sall    %cl, -8(%ebp)   ; k <<= CX(32)
movl    -8(%ebp), %eax  ; AX = k
movl    %eax, 8(%esp)
movl    -12(%ebp), %eax
movl    %eax, 4(%esp)
movl    $.LC0, (%esp)
call    printf
addl    $36, %esp
popl    %ecx
popl    %ebp
leal    -4(%ecx), %esp
ret

好的,这是什么意思?正是这条指令让我感到困惑:

sall    %cl, -8(%ebp)   ; k <<= CX(32)

显然 k 正在左移 32 位。

你找到了我 - 它使用的是 sall 指令,它是 arithmetic shift。我不知道为什么将其旋转 32 会导致该位重新出现在初始位置。我最初的猜想是处理器被优化为在一个时钟周期内执行这条指令——这意味着任何超过 31 的移位都将被视为无关紧要。但我很想找到这个问题的答案,因为我希望旋转应该导致所有位从数据类型的左端脱落。

我找到了一个指向http://faydoc.tripod.com/cpu/sal.htm 的链接,它解释了移位计数(在 CL 寄存器中)被屏蔽为 5 位。这意味着如果您尝试移动 32 位,则执行的实际移动将是零位(即没有变化)。答案来了!

【讨论】:

  • 您是对的,正在使用 SALL 指令 - 我也注意到这一点,检查 Intel 指令集参考显示:“但是,所有其他 IA-32 处理器(从 Intel 286 处理器开始)将移位计数屏蔽为 5 位,最大计数为 31。”所以 32 的值被忽略了 - 1 是不变的参数,没有旋转 32。
  • 屏蔽应该是 (32 & 31)。
  • faydoc链接坏了。
【解决方案2】:

如果您的 ints 是 32 位或更短,则行为未定义 ... 无法解释未定义行为

标准说:

6.5.7/3 [...] 如果右操作数的值为负数或大于或等于提升的左操作数的宽度,则行为未定义。


您可以检查您的int width 位大小,例如:

#include <limits.h>
#include <stdio.h>
int main(void) {
    printf("bits in an int: %d\n", CHAR_BIT * (int)sizeof (int));
    return 0;
}

您可以检查您的int 宽度(可能有填充位),例如:

#include <limits.h>
#include <stdio.h>
int main(void) {
    int width = 0;
    int tmp = INT_MAX;
    while (tmp) {
        tmp >>= 1;
        width++;
    }
    printf("width of an int: %d\n", width + 1 /* for the sign bit */);
    return 0;
}

标准 6.2.6.2/2:对于有符号整数类型,对象表示的位应分为三组:值位、填充位和符号位。不需要任何填充位;应该只有一个符号位

【讨论】:

  • 没有冒犯,但这并不能真正回答问题。
  • “标准只允许x &lt;&lt; y(y &lt; sizeof x)”。这个陈述在两个方面是错误的:标准 allows 转移但说结果是未定义的。对于特定架构上的特定编译器,它可能提供可预测的结果,但并非必须如此。第二部分也应该是y &lt; widthInBitsOf(x)
  • @pmg 希望你不是认真的。未定义的行为意味着编译器可以为所欲为。但这当然并不意味着它无法解释或不一致。
  • @Let_Me_Be:你的假设是不确定的。
  • 无法从标准解释未定义的行为。调查特定编译器和硬件的行为并没有超出人类的聪明才智。
【解决方案3】:

C99 标准规定,将数字移位操作数的位(或更多)宽度的结果是未定义的。为什么?

这允许编译器为特定架构创建最有效的代码。例如,i386 移位指令使用一个 5 位宽的字段来表示将 32 位操作数移位的位数。 C99 标准允许编译器简单地取出移位计数的低五位并将它们放入字段中。显然,这意味着 32 位的移位(= 100000 二进制)因此与 0 的移位相同,因此结果将是左操作数不变。

不同的 CPU 架构可能使用更宽的位域,比如 32 位。编译器仍然可以将移位计数直接放在字段中,但这次结果将为 0,因为 32 位的移位会将所有位移出左操作数。

如果 C99 将这些行为中的一种或其他定义为正确,则英特尔的编译器必须对太大的移位计数进行特殊检查,或者非 i386 的编译器必须屏蔽移位计数。

原因

   int x = 1 << 32;

   int z = 1 << y;

给出不同的结果是因为第一次计算是一个常量表达式,可以完全由编译器执行。编译器必须使用 64 位算术计算常量表达式。第二个表达式由编译器生成的代码计算得出。由于 y 和 z 的类型都是 int,因此代码使用 32 位宽的 int 生成计算(在 i386 和 x86_64 上 int 都是 32 位,在 Apple 上使用 gcc)。

【讨论】:

  • @DonHatch 当我说“必须”时,我是在推测编译器在这种情况下的确切行为,我并不是说“C 标准要求 64 位算术”。 (int)(1 &lt;&lt; 32) 为零表示 Intel 版本的 gcc 肯定没有使用 32 位版本的 shift 来计算 shift,或者如果是,它正在检查 shift 是否大于 31 .
  • 嘿@JeremyP,谢谢,这是有道理的,感谢您的解释。我做了一个建议的编辑,将“必须计算”更改为“必须计算”,这样可以避免我的误解。我删除了我之前的评论和我的反对意见。 (实际上,downvote 目前已锁定,但如果编辑被接受,我认为将被解锁)
  • 您是否推测它使用 64 位算法进行常量计算,因为该位从未溢出,而是可能继续向下移动到更大的位空间(例如 64)。我想我们可以测试它: int x = 1
  • 实际上x = 1 &lt;&lt; 64x = 0; x |= (1 &lt;&lt; 64) 不会告诉我们任何事情,因为无论如何我们都会将1 从左端推开,它不一定会环绕/溢出到低位
【解决方案4】:

在我看来“int x = y

但我遇到了类似的问题:

长 y = ... 长 x = y

即使相关目标上的 sizeof(long) 为 8,我也收到警告“警告:左移计数 >= 类型宽度”。我通过这样做摆脱了警告:

长 x = (y

这似乎奏效了。

在 64 位架构上没有警告。在 32 位架构上有。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-09-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-08-10
    • 2010-09-18
    相关资源
    最近更新 更多