【问题标题】:How to get around strict aliasing when casting uint8_t buffer bytes as another type?将 uint8_t 缓冲区字节转换为另一种类型时如何绕过严格的别名?
【发布时间】:2021-02-06 02:10:58
【问题描述】:

我想定义一个结构来映射通过串行流接收的数据包的字节。数据包中的有效载荷字节数是可变的,有效载荷字节后面有一个 16 位的 CRC。 (最后带有 CRC 的数据包字节布局是遗留的,无法更改。)结构的全部意义在于使用此类数据包在数据包缓冲区中查找命名字段而不关心字节的源代码数据包的每个部分的偏移量。并且由于 CRC 的偏移量根据有效负载数据的长度而变化,我为该结构编写了一个方法,该方法简单地计算适当的地址并将 uint16_t& 返回到 2 字节 CRC,从中使用它的源代码可以读取或写入 CRC。

这是我编写的数据包结构的简化版本,以及crc() 方法:

#include <stdint.h>

struct SPacket
{
   uint8_t  startByte;  // always 0xEE
   uint8_t  identity;   // device identity byte
   uint8_t  ctrl_bits;
   uint8_t  seq_nbr;
   uint16_t length;     // length of data[] in bytes
   uint8_t  data[1];    // placeholder for 1 or more payload data bytes
   uint16_t &crc();     // returns a reference to the packet's 16-bit CRC following the data
};

uint16_t &SPacket::crc()
{
   return *(uint16_t*)(data + length);
}

但是当我用 GCC 编译它时,我得到了这个错误:

C:\Users\phonetagger> g++ --version
g++ (GCC) 4.8.1
Copyright (C) 2013 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

C:\Users\phonetagger> g++ -o test.o -c -O3 -Wall test.cpp
C:\Users\phonetagger\test.cpp: In member function 'uint16_t& SPacket::crc()':
C:\Users\phonetagger\test.cpp:17:37: warning: dereferencing type-punned pointer will break strict-aliasing rules [-Wstrict-aliasing]
    return *(uint16_t*)(data + length);
                                     ^

顺便说一句,如果我删除 -O3 或 -Wall,警告就会消失,但我不能这样做;我的同事会冲我尖叫。有什么方法可以通过更改我的源代码来让 GCC 满意,但仍然有一个成员 crc() 方法在数据包的可变长度 data[] 之后将 uint16_t&amp; 返回到 2 字节 CRC?

顺便说一句...我知道你们中的一些人认为我的解决方案在无法处理硬件中未对齐访问的系统上被破坏了。我也知道你们中的一些人认为我的解决方案在字节序与串行流的字节顺序不匹配的系统上被破坏了。向那些想到这一点的人致敬。我的例子被大大简化了。实际上,我的结构使用用户定义类型 ube16_t(无符号大端 16 位类型),但同样的问题也适用。我不想通过包含ube16_t 类型的定义来使事情​​复杂化。

【问题讨论】:

  • @DavidC.Rankin 正如我所提到的,我的示例中的结构是从我实际使用的结构中简化的。在“真实事物”中,数据是大量有效载荷类型的匿名联合。但data[1] 是该联合的一部分,它是crc() 方法用作有效负载基地址的名称。
  • 是的,使用data[1] struct-hack 很好,但是您必须声明一个指向 struct 的指针并为有效负载的大小分配额外的空间。但是,那么 data[] 需要是最后一个元素或您的结构。好的,所以如果我知道发生了什么,您正在为您的有效负载创建空间,它将分配比data 更大的空间,以便保存crc(),然后用于查找变量数据的结尾包含在data[] 是/否?还是crc() 只是作为data 中的第一个两个字节?
  • @DavidC.Rankin 在接收方向上,数据包到达一个 UART 缓冲区(一个简单的 uint8_t 数组),一旦它到达那里,我将第一个 0xEE 字节的地址投射到该缓冲区中为reinterpret_cast&lt;SPacket*&gt;,然后计算数据包的CRC,并确保它匹配。 (除了crc() 方法,我还有一个computeCrc() 方法来计算CRC 应该。)在传输方向,我使用另一个缓冲区,预先分配到最大值任何数据包的大小,并将该缓冲区的起始地址转换为&lt;SPacket*&gt;,填写数据包,然后发送。
  • @DavidC.Rankin - data[] 数组的长度实际上是可变的。在我的结构中,它是最后一个固定偏移成员,对于唯一的可变偏移成员(CRC),我计算它的地址并将其作为crc() 方法中的引用返回。这样,结构的所有成员都被命名,即使是偏移量不固定的成员。
  • 这就是我的想法,你为你的有效载荷分配。 data 只是一个标记地址,crc 只是你的uint16_t 引用,所以你的数据是(data+3)[index] 到最后。为什么不将有效负载引用为(data + 3)[length] 并完全避免类型双关语? (尽管由于data 不是SPacket 的最后一个成员,你冒着被一两个填充字节搞砸的风险......)

标签: c++ gcc strict-aliasing


【解决方案1】:

好的,所以 Galik 建议提供两个单独的方法 uint16_t getCrc()void setCrc(uint16_t crc) 可能是解决此问题的更好方法,但我确实找到了解决严格别名警告的方法:

#include <stdint.h>
#include <stddef.h>

struct SPacket
{
   uint8_t  startByte;  // always 0xEE
   uint8_t  identity;   // device identity byte
   uint8_t  ctrl_bits;
   uint8_t  seq_nbr;
   uint16_t length;     // length of data[] in bytes
   uint8_t  data[1];    // placeholder for 1 or more data bytes
   uint16_t &crc();     // returns a reference to the packet's 16-bit CRC following the data
};

uint16_t &SPacket::crc()
{
   return *(uint16_t*)(((size_t)data) + length);
}

...或:

uint16_t &SPacket::crc()
{
   return *reinterpret_cast<uint16_t*>(reinterpret_cast<size_t>(data) + length);
}

【讨论】:

  • 我认为这仍然是未定义的行为。 C++ 并不简单,如果存在未定义的行为,您无法保证任何事情。
  • @user202729 - 我有点担心可能是这种情况。但是在协议头处理和驱动程序/嵌入代码中,对这种东西的需求如此普遍。我看到代码reinterpret_cast 是一个uint8_t* 指向不同/更大类型的指针一直,并且很少触发“取消引用类型- punned 指针破坏了严格的别名规则”警告(但正如你所说,即使它没有触发警告并不意味着它不是 UB)。既然将uint8_t* 重新解释为指向不同/更大类型的指针是一项基本需求,那么如何安全地完成呢?
猜你喜欢
  • 2020-09-10
  • 1970-01-01
  • 1970-01-01
  • 2017-05-08
  • 2020-01-09
  • 2012-12-17
  • 2014-12-28
  • 1970-01-01
  • 2010-12-27
相关资源
最近更新 更多