gcc 带符号位字段的多个不一致行为

sg3maiej  于 2022-11-13  发布在  其他
关注(0)|答案(2)|浏览(153)

我在有符号位字段上遇到了一个奇怪的行为:

#include <stdio.h>

struct S {
    long long a31 : 31;
    long long a32 : 32;
    long long a33 : 33;
    long long : 0;
    unsigned long long b31 : 31;
    unsigned long long b32 : 32;
    unsigned long long b33 : 33;
};

long long f31(struct S *p) { return p->a31 + p->b31; }
long long f32(struct S *p) { return p->a32 + p->b32; }
long long f33(struct S *p) { return p->a33 + p->b33; }

int main() {
    struct S s = { -2, -2, -2, 1, 1, 1 };
    long long a32 = -2;
    unsigned long long b32 = 1;
    printf("f31(&s)       => %lld\n", f31(&s));
    printf("f32(&s)       => %lld\n", f32(&s));
    printf("f33(&s)       => %lld\n", f33(&s));
    printf("s.a31 + s.b31 => %lld\n", s.a31 + s.b31);
    printf("s.a32 + s.b32 => %lld\n", s.a32 + s.b32);
    printf("s.a33 + s.b33 => %lld\n", s.a33 + s.b33);
    printf("  a32 +   b32 => %lld\n",   a32 +   b32);
    return 0;
}

在OS/X上使用Clang,我得到以下输出:

f31(&s)       => -1
f32(&s)       => 4294967295
f33(&s)       => -1
s.a31 + s.b31 => 4294967295
s.a32 + s.b32 => 4294967295
s.a33 + s.b33 => -1
  a32 +   b32 => -1

在Linux上使用GCC,我得到了这个:

f31(&s)       => -1
f32(&s)       => 4294967295
f33(&s)       => 8589934591
s.a31 + s.b31 => 4294967295
s.a32 + s.b32 => 4294967295
s.a33 + s.b33 => 8589934591
  a32 +   b32 => -1

以上输出显示了3种类型的不一致:

  • 不同编译器的不同行为;
  • 对于不同位字段宽度的不同行为;
  • 内联表达式和函数中 Package 的等效表达式的不同行为。

C标准具有以下语言:

6.7.2类型说明符

...
每个以逗号分隔的多重集都指定相同的类型,但对于位字段,说明符int是指定与signed int相同的类型还是指定与unsigned int相同的类型是由实现定义的。
众所周知,位字段在许多较旧的编译器中是损坏的...
Clang和GCC的行为是否一致,或者这些不一致是一个或多个bug的结果?

4uqofj5v

4uqofj5v1#

Clang和GCC的行为是否一致,或者这些不一致是一个或多个bug的结果?
我认为最有可能的错误是在您的代码中,tbh.根据6.7.2.1p5:
比特字段的型别应该是_Bool、signed int、unsigned int或其他实作定义型别的限定或非限定版本。
这里没有提到long long,所以我们不一定一开始就把这段代码视为一致的。(例如,some ARM clang targets),而其他人则乐于让这种行为不被定义(例如,gcc手册似乎没有在“除_Bool,signed int,和无符号整型(C99和C11 6.7.2.1)”)。
此外,根据6.3.1.1p2:
在使用int或unsigned int的表达式中,可以使用以下语句:

  • 具有整数类型(int或unsigned int除外)的对象或表达式,其整数转换秩小于或等于int和unsigned int的秩。
  • 类型为_Bool、int、signed int或unsigned int的位字段。

如果一个int可以表示原始类型的所有值(对于位字段,受宽度限制),则该值被转换为int;否则,它将转换为无符号int。
换句话说,编译器不仅要支持这些类型的位字段,还要进行适当的类型转换,这样表达式才能正确转换。具体来说,这段代码看起来 * 非常可怕 *,因为%lld告诉printf要期待long long int,而我认为您可能只传递了一个int(或者unsigned):

printf("s.a31 + s.b31 => %lld\n", s.a31 + s.b31);
printf("s.a32 + s.b32 => %lld\n", s.a32 + s.b32);
printf("s.a33 + s.b33 => %lld\n", s.a33 + s.b33);
printf("  a32 +   b32 => %lld\n",   a32 +   b32);

我想我应该在结束时引用我对上面这个 * 毛茸茸的代码 * 的预期结果:
如果转换规范无效,则行为未定义。282)如果任何参数不是相应转换规范的正确类型,则行为未定义。
-- C11/7.21.6.1p9中的数据

doinxwow

doinxwow2#

请看一下建议的代码,它能正常工作并符合预期。
出于实际目的,我建议,只要确保

  • 添加兼容类型,
  • 返回正确的类型,
  • printf语句中的类型正确。

就这样了。
有关更多信息,请参见下面的参考文献[1]和[2]。
第一个
参考文献
[1][Signed to unsigned conversion in C - is it always safe?](https://stackoverflow.com/questions/50605/signed-to-unsigned-conversion-in-c-is-it-always-safe)
[2][https://www.geeksforgeeks.org/bit-fields-c/](https://www.geeksforgeeks.org/bit-fields-c/)“我们不能有指向位字段成员的指针,因为它们可能不在字节边界开始。”

相关问题