10

今天,我在尝试位域时发现了令人震惊的行为。为了讨论和简单起见,这里有一个示例程序:

#include <stdio.h>

struct Node
{
  int a:16 __attribute__ ((packed));
  int b:16 __attribute__ ((packed));

  unsigned int c:27 __attribute__ ((packed));
  unsigned int d:3 __attribute__ ((packed));
  unsigned int e:2 __attribute__ ((packed));
};

int main (int argc, char *argv[])
{
  Node n;
  n.a = 12345;
  n.b = -23456;
  n.c = 0x7ffffff;
  n.d = 0x7;
  n.e = 0x3;

  printf("3-bit field cast to int: %d\n",(int)n.d);

  n.d++;  

  printf("3-bit field cast to int: %d\n",(int)n.d);
}

该程序故意导致 3 位位域溢出。这是使用“g++ -O0”编译时的(正确)输出:

3 位字段转换为 int:7

3 位字段转换为 int:0

这是使用“g++ -O2”(和 -O3)编译时的输出:

3 位字段转换为 int:7

3 位字段转换为 int:8

检查后一个示例的程序集,我发现:

movl    $7, %esi
movl    $.LC1, %edi
xorl    %eax, %eax
call    printf
movl    $8, %esi
movl    $.LC1, %edi
xorl    %eax, %eax
call    printf
xorl    %eax, %eax
addq    $8, %rsp

优化刚刚插入“8”,假设 7+1=8,而实际上数字溢出并且为零。

幸运的是,据我所知,我关心的代码并没有溢出,但这种情况让我害怕——这是一个已知的错误、一个特性,还是这是预期的行为?我什么时候可以期望 gcc 在这方面是正确的?

编辑(重新:签名/未签名):

它被视为无符号,因为它被声明为无符号。将其声明为 int,您将获得输出(使用 O0):

3 位字段转换为 int:-1

3 位字段转换为 int:0

在这种情况下,-O2 会发生更有趣的事情:

3 位字段转换为 int:7

3 位字段转换为 int:8

我承认使用属性是一件可疑的事情;在这种情况下,这是我担心的优化设置的差异。

4

1 回答 1

8

如果你想获得技术,你使用的那一刻__attribute__(一个包含两个连续下划线的标识符)你的代码有/有未定义的行为。

如果你得到与那些删除相同的行为,它在我看来就像一个编译器错误。3位字段被视为无符号的事实7意味着它被视为无符号,因此当您溢出时,它应该像任何其他无符号一样,并为您提供模算术。

将位字段视为已签名也是合法的。在这种情况下,第一个结果是-1, -3or -0(可能打印为 just 0),第二个结果是未定义的(因为有符号整数的溢出会产生未定义的行为)。理论上,其他值在 C89 或当前 C++ 标准下可能是可能的,因为它们不限制有符号整数的表示。在 C99 或 C++0x 中,它只能是这三个(C99 将有符号整数限制为一个补码、二进制补码或符号幅度,C++0x 基于 C99 而不是 C90)。

糟糕:我没有给予足够的关注——因为它被定义为unsigned,它必须被视为unsigned,几乎没有任何回旋余地来摆脱它作为编译器的错误。

于 2010-05-14T19:44:37.470 回答