严格别名规则:将uint8_t缓冲区转换为结构体

9

假设我有一个包含位字段的typedef,如下所示。

typedef struct VIN_oCAN01_3abd61be
{
   uint64_t var1:24;
   uint64_t var2:4;
   uint64_t var3:4
}__attribute__((packed))Message;

我收到了如下的 uint8_t 缓冲区。

uint8_t buffer[4] = {0x1,0x2,0x3,0x14};

目前在我的生产程序中,我的团队建议采用以下方法。

Message *ptrMsg = (Message *)buffer;

也就是说,将uint8_t缓冲区分配给Message类型指针。 我建议采用下面的方式,因为这种方法不遵循严格别名规则。
Message msg;
memcpy(&msg,buffer, sizeof(msg));

请注意,没有手动将缓冲区复制到结构体的选项(逐个成员), 因为结构体非常大。
我的理解正确吗?如果是这样,请提供我可以用来证明我的观点的标准文档。

@AlainMerigot 很好的发现!我们在memcpy或指针赋值之前处理了字节序,只是这里没有展示出来。 - kiran Biradar
我认为strict aliasing是编译器的一种机制,通常情况下,它会有一个标志来指示不同优化级别是否可以假定严格别名。 - Alex Lop.
附注:uint8_t buffer[4] 太小,无法容纳 uint64_t - user694733
1
@user694733 我已经定义了位域。 - kiran Biradar
1
你的位域加起来总共是32位,在uint64_t中。你不知道这64位中的位域实际上在哪里。根据6.7.2.1p11:“一个不适合的位域是否放入下一个单元或重叠相邻单元是由实现定义的。位域在单元内的分配顺序(高位到低位或低位到高位)是由实现定义的。可寻址存储单元的对齐方式是未指定的。” 位域对于可移植性和可维护性来说只会带来麻烦。 - Andrew Henle
显示剩余2条评论
2个回答

6
我曾建议该方法不遵循严格的别名规则。
没错,ptrMsg = (Message *)buffer 意味着你不能访问 ptrMsg 的数据而不引发未定义行为。
你可以用 C17 6.5 §7(在这里引用 - 什么是严格别名规则?)证明你的观点。如 ptrMsg->var1 = value 这样的 lvalue 表达式不会通过与存储在那里的有效类型兼容的类型访问存储的值,也不会通过任何允许的表达式访问它。
但是,如果假设 uint8_t 是字符类型,则您可以从 Message 转换为 uint8_t 数组,而不违反严格别名。
然而,更大的问题是存在位域,这是不标准且不可移植的。例如,您无法知道位域的哪一部分是 MSB 和 LSB。您无法知道 64 位类型中的位对齐方式。使用 64 位类型作为位域是非标准扩展。它依赖于大小端。等等。
假设 24 位是指位 31 到 8(我们无法通过阅读您的代码知道),则严格遵循别名规则、位域疯狂和非标准 "结构填充杀手" 的适当代码如下所示:
typedef union
{
   uint32_t var;
   uint8_t  bytes[4];
} Message;


uint8_t buffer[4];
Message* ptrMsg = (Message*)buffer;
uint32_t var1 = (ptrMsg->var >> 8);
uint8_t  var2 = (ptrMsg->var >> 4) & 0x0F;
uint8_t  var3 = (ptrMsg->var) & 0x0F;

Message 是一个联合类型,它的成员中包含前面提到的类型之一。这意味着它包含与 uint8_t [4] 兼容的类型。

此代码也不包含复制操作,移位将被转换为机器码中相关的位访问。


5

你是正确的。

C17草案 § 6.5:

  1. 一个对象只能通过具有以下类型之一的lvalue表达式访问其存储值: 89)

    • 与对象的有效类型兼容的类型,

    • 与对象的有效类型兼容的类型的限定版本,

    • 与对象的有效类型对应的有符号或无符号类型,

    • 与对象的有效类型的限定版本对应的有符号或无符号类型,

    • 包括其中一种前述类型在其成员中的聚合或联合类型(包括子聚合或包含联合的成员,递归地),或
    • 字符类型。

在这种情况下,对象的类型为uint8_t[],lvalue表达式的类型为Message。上述任何异常都不适用。

使用memcpy覆盖解引用可以解决此问题,或者您可以在编译器中禁用严格别名检查,以便使用非C语言编写。

但即使在此之后,代码仍然存在许多问题,并且不可移植:位字段在标准中定义非常不明确,并且总体结构是处理序列化的非常笨拙的方式。您应该选择手动反序列化每个成员的选项。


1
公平地说,禁用严格别名并不是一个坏主意。特别是在嵌入式系统设计中,这似乎是如此。严格别名/有效类型是 C 语言的一个破碎部分,由不理解嵌入式系统中经常需要的类型转换的 PC 程序员指定。此外,GCC 是我所知道的唯一一种滥用严格别名以产生更快代码的编译器,但它却能高效地执行!最好总是使用 gcc -fno-strict-aliasing - Lundin
2
@Lundin 但这种情况的反面是那些在违反严格别名规则会实际出现问题的系统上没有经验的程序员。因此你会看到像这样的问题,一些明显完全不了解严格别名规则的程序员似乎促使某个了解其含义的人在Stackoverflow上发帖提问。 - Andrew Henle
@AndrewHenle 但是严格别名规则并不是普及的知识。只有经验丰富的C老手/语言爱好者才知道它并理解它。这反过来又使它成为一个安全隐患和语言缺陷。特别是在C99之前,没有编译器依赖于它进行积极的优化...而C99成为行业标准大约需要10年时间。直到2009年,当gcc开始成为ARM编译器时,我们才真正看到嵌入式系统中的严格别名错误。因此,非技术人员必须使用MISRA-C告诉他们“不要这样做”,而不理解原因。 - Lundin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接