如果它们有相同的对齐方式,将 int 指针强制转换为 float 指针是否合法?

28 浏览
0 Comments

如果它们有相同的对齐方式,将 int 指针强制转换为 float 指针是否合法?

当询问C语言中的常见未定义行为时,人们有时会提到严格别名规则。

他们在谈些什么?

admin 更改状态以发布 2023年5月20日
0
0 Comments

我发现最好的解释是由Mike Acton提供的《了解Strict Aliasing》。这篇文章着重于PS3开发,但实质上只是GCC。

从这篇文章中得知:

“Strict aliasing是C(或C ++)编译器做出的一种假设,即指向不同类型的对象的指针解引用永远不会引用同一内存位置(即别名)。"

因此,如果您有一个指向包含int的内存的int*,然后指向该内存的float*并将其用作float,则会破坏该规则。 如果您的代码不遵守此规则,则编译器的优化器很可能会破坏您的代码。

例外是一个char*,它被允许指向任何类型。

0
0 Comments

一个典型的遇到严格别名问题的情况是将结构体(比如设备/网络消息)覆盖在系统的字大小缓冲区(比如指向uint32_t或uint16_t的指针)上。当你将结构体覆盖在这样的缓冲区上,或通过指针转换将缓冲区覆盖在这样的结构体上时,你很容易违反严格别名规则。

因此,在这种设置中,如果我想向某个东西发送消息,我必须有两个不兼容的指针指向相同的内存块。然后,我可能会天真地编写类似下面这样的代码:

```

typedef struct Msg
{
    unsigned int a;
    unsigned int b;
} Msg;
void SendWord(uint32_t);
int main(void)
{
    // Get a 32-bit buffer from the system
    uint32_t* buff = malloc(sizeof(Msg));
    // Alias that buffer through message
    Msg* msg = (Msg*)(buff);
    // Send a bunch of messages    
    for (int i = 0; i < 10; ++i)
    {
        msg->a = i;
        msg->b = i+1;
        SendWord(buff[0]);
        SendWord(buff[1]);   
    }
}

```

严格别名规则使得这种设置非法:对别名不兼容类型的对象进行解引用,或者对C 2011 6.5段71允许的其他类型之一进行解引用,导致未定义行为。不幸的是,你仍然可以以这种方式编写代码,也许会得到一些警告,编译也可以通过,但当你运行代码时会出现奇怪的意外行为。

(GCC在给予别名警告的能力方面似乎有一些不一致之处,有时会给我们一个友好的警告,有时可能不会。)

要了解为什么这种行为是未定义的,我们必须考虑严格别名规则为编译器带来了什么好处。基本上,有了这个规则,编译器就不必考虑在每次循环运行时插入刷新buff内容的指令。相反,在优化时,通过某些令人讨厌的未强制执行的关于别名的假设,它可以省略那些指令,将buff[0]buff[1]在循环运行前装入CPU寄存器一次,从而加快循环体的运行速度。在引入严格别名之前,编译器必须处于buff的内容可以通过任何前面的内存存储更改的偏执状态。因此,为了获得额外的性能优势,并假定大多数人不会类型转换指针,引入了严格别名规则。

请记住,如果你认为这个例子是人为的,那么即使在将缓冲区传递给另一个为你发送的函数时,这种情况也可能发生,如果改为以下方式,

```

void SendMessage(uint32_t* buff, size_t size32)
{
    for (int i = 0; i < size32; ++i) 
    {
        SendWord(buff[i]);
    }
}

```

并重写我们之前的循环以利用这个方便的函数

```

for (int i = 0; i < 10; ++i)
{
    msg->a = i;
    msg->b = i+1;
    SendMessage(buff, 2);
}

```

编译器可能能够或智能到尝试内联SendMessage,它可以选择加载或不加载buff。如果SendMessage是另一个API的一部分,它可能有指令来加载buff的内容。另一方面,也许你在C++中,这是一些编译器认为可以内联的模板头文件实现。或者这只是你在.c文件中为自己方便而编写的东西。无论如何,未定义的行为仍然可能发生。即使我们知道底层的一些情况,违反规则仍然是一种违反,因此不能保证有良好定义的行为。因此,仅仅通过包装一个以单词为分隔符的缓冲区的函数并不能帮助。

那么我该怎么办呢?

  • 使用联合。大多数编译器都支持这种做法,而不会对严格别名做出抱怨。这在C99中是允许的,而在C11中则被明确允许。

      union {
          Msg msg;
          unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
      };
    

  • 在编译器中禁用严格别名(在gcc中使用f[no-]strict-aliasing

  • 使用char* 代替系统的单词进行别名处理。规则允许char*(包括signed charunsigned char)作为一个例外。总是假设char* 与其他类型是别名。但是这种方法不能用于反过来:不假定您的结构体别名为一组字符缓冲区。

初学者要小心

将两种类型叠加在一起时,还有很多潜在的陷阱。您还应该了解字节序字对齐问题以及如何通过正确地压缩结构体解决对齐问题。

脚注

1 C 2011 6.5 7允许左值访问的类型为:

  • 与对象的有效类型兼容的类型,
  • 与对象的有效类型兼容的带限定符的类型,
  • 对象的有效类型的有符号或无符号对应类型,
  • 对象的有效类型的带限定符的有符号或无符号对应类型,
  • 包括上述类型之一的聚合或联合类型(对于一个子聚合或包含的联合的成员,也是递归的),或
  • 字符类型。
0