c++ - 什么是严格的别名规则?

标签 c++ c undefined-behavior strict-aliasing type-punning

当询问 common undefined behavior in C ,人们有时会提到严格的别名规则。
他们在说什么?

最佳答案

遇到严格别名问题的典型情况是将结构(如设备/网络消息)覆盖到系统字大小的缓冲区(如指向 uint32_t s 或 uint16_t s 的指针)。当您将结构覆盖到这样的缓冲区上,或者通过指针转换将缓冲区覆盖到这样的结构上时,您很容易违反严格的别名规则。
所以在这种设置中,如果我想向某些东西发送消息,我必须有两个不兼容的指针指向同一块内存。然后我可能会天真地编写这样的代码:

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]);   
    }
}
严格的别名规则使此设置非法:取消引用为不属于 compatible type 的对象设置别名的指针。或 C 2011 6.5 第 71 段允许的其他类型之一是未定义行为。不幸的是,你仍然可以用这种方式编码,也许会得到一些警告,让它编译正常,只是在你运行代码时出现奇怪的意外行为。
(GCC 在给出别名警告的能力上似乎有些不一致,有时会给我们一个友好的警告,有时则不会。)
要了解为什么这种行为是未定义的,我们必须考虑严格的别名规则给编译器带来了什么。基本上,有了这个规则,就不用考虑插入指令来刷新buff的内容了。每次循环运行。相反,在优化时,使用一些令人讨厌的关于别名的非强制假设,它可以省略这些指令,加载 buff[0]buff[1]在循环运行之前进入 CPU 寄存器一次,并加速循环体。在引入严格别名之前,编译器不得不对 buff 的内容感到 panic 。可以通过任何先前的内存存储进行更改。因此,为了获得额外的性能优势,并假设大多数人不键入双关指针,则引入了严格的别名规则。
请记住,如果您认为该示例是人为设计的,那么如果您将缓冲区传递给另一个为您进行发送的函数(如果您有),甚至可能会发生这种情况。
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 文件中编写的内容。无论如何,未定义的行为可能仍然会发生。即使我们知道一些幕后发生的事情,它仍然违反了规则,因此不能保证明确定义的行为。因此,仅仅通过包装在我们的单词分隔缓冲区的函数中不一定有帮助。
那么我该如何解决这个问题呢?
  • 使用 union 。大多数编译器都支持这一点,而不会提示严格的别名。这在 C99 中是允许的,在 C11 中是明确允许的。
      union {
          Msg msg;
          unsigned int asBuffer[sizeof(Msg)/sizeof(unsigned int)];
      };
    
  • 您可以在编译器中禁用严格别名(gcc 中的 f[no-]strict-aliasing))
  • 您可以使用 char*用于别名而不是系统的单词。规则允许 char* 的异常(exception)情况(包括 signed charunsigned char )。总是假设 char*别名其他类型。然而,这不会以其他方式工作:没有假设您的结构为字符缓冲区别名。

  • 初学者当心
    当两种类型相互叠加时,这只是一个潜在的雷区。您还应该了解 endianness , word alignment ,以及如何通过 packing structs 处理对齐问题正确。
    脚注
    1 C 2011 6.5 7 允许左值访问的类型是:
  • 与对象的有效类型兼容的类型,
  • 与对象的有效类型兼容的类型的限定版本,
  • 一个类型,它是与对象的有效类型相对应的有符号或无符号类型,
  • 一种类型,它是与对象有效类型的限定版本相对应的有符号或无符号类型,
  • 在其成员中包含上述类型之一的聚合或 union 类型(递归地包括子聚合或包含 union 的成员),或
  • 一种字符类型。
  • 关于c++ - 什么是严格的别名规则?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/98650/

    相关文章:

    c++ - 在 c/c++ 中输入比 scanf for int 更快

    c++ - 为什么类的析构函数被调用了两次?共享指针

    c++ - 这是我的代码中的错误还是 g++ 对 -Weffc++ 的分析中的错误?

    c++ - recvfrom 在进入等待状态后只收到很少的数据包

    C++ 在没有 lambda 的情况下对索引 vector 进行排序

    c++ - 从 switch block 返回时有什么性能差异?

    c - Bison - 在简单的计算器中打印前缀符号

    c - 我的代码有什么问题?该程序只是终止而没有错误声明

    c - 通过 `extern` 变量访问 .text 段是否会导致未定义的行为?

    c - 以下 C union 访问模式是否未定义行为?