当前位置:  开发笔记 > 运维 > 正文

GCC左移溢出

如何解决《GCC左移溢出》经验,为你挑选了3个好方法。

在Mac上使用GCC版本4.2.1(Apple Inc. build 5664)时,以下小程序非常笨拙.

#include 

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的值不同吗?
非常感谢.



1> PP...:

简短回答:英特尔处理器将移位计数屏蔽为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(评论是我自己的)

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)

显然ķ 移位由32位左.

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

我找到了http://faydoc.tripod.com/cpu/sal.htm的链接,它解释了移位计数(在CL寄存器中)被屏蔽为5位.这意味着如果您尝试移位32位,则执行的实际移位将为零位(即无更改).答案就是这样!



2> pmg..:

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

标准说:

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


您可以检查int 宽度位大小,例如:

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

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

#include 
#include 
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:对于有符号整数类型,对象表示的位应分为三组:值位,填充位和符号位.不需要任何填充位; 应该只有一个符号位


+*未定义的行为无法解释*.
*标准*无法解释未定义的行为.研究特定编译器和硬件的行为并不是人类的聪明才智.

3> JeremyP..:

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).

推荐阅读
爱唱歌的郭少文_
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有