从空字节数组转换为结构指针可能会违反严格的别名?
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从空字节数组转换为结构指针可能会违反严格的别名?相关的知识,希望对你有一定的参考价值。
大多数人关心的是如果他们收到带有数据的字节数组并且他们想要将数组转换为结构指针会发生什么 - 这可能违反严格的别名规则。我不确定是否初始化一个足够大小的空字节数组,将其转换为结构指针,然后填充结构成员将违反严格的别名规则。
细节:说我有2个打包的结构:
#pragma pack(1)
typedef struct
{
int a;
char b[2];
uint16_t c : 8;
uint16_t d : 7;
uint16_t e : 1;
} in_t;
typedef struct
{
int x;
char y;
} out_t;
#pragma pack()
我有许多类型的输入/输出打包结构用于不同的消息,所以请忽略我为该示例提供的特定成员。结构可以包含位域,其他结构和联合。此外,endianess得到了照顾。另外,我不能使用新的c标准(> = c99)功能。
我收到一个包含in_t
的缓冲区(缓冲区大到足以包含out_t
,无论它有多大)都像void *
void recv_msg(void *data)
{
in_t *in_data = (in_t*)data;
out_t *out_data = (out_t*)data;
// ... do something with in_data then set values in out_t.
// make sure values aren't overwritten.
}
现在我有一个新类型的结构
#pragma pack(1)
typedef struct
{
int a;
char b[3];
uint32_t c;
} in_new_api_t;
typedef struct
{
int x;
char y[2];
} out_new_api_t;
#pragma pack()
现在,当移动到新api但保持旧api向后兼容时,我想将旧的in_t
中的值复制到in_new_api_t
,使用in_new_api_t
,在out_new_api_t
中设置值,然后将值复制到out_t
。
我想这样做的方法是分配一个大小为max(sizeof(in_new_api_t), sizeof(out_new_api_t));
的空字节数组,将其转换为in_new_api_t *
,将值从in_t
转换为in_new_api_t
,将新的api结构发送到新的api函数,然后将值从out_new_api_t
转换为out_t
。
void recv_msg(void *data)
{
uint8_t new_api_buf[max(sizeof(in_new_api_t), sizeof(out_new_api_t))] = {0};
in_new_api_t *new_in_data = (in_new_api_t*)new_api_buf;
in_t *in_data = (in_t*)data;
// ... copy values from in_data to new_in_data
// I'M NOT SURE I CAN ACCESS MEMBERS OF new_in_data WITHOUT VIOLATING STRICT ALIASING RULES.
new_in_data->a = in_data->a;
memcpy(new_in_data->b, in_data->b, 2);
// ...
new_recv_msg((void*)new_in_data);
out_new_api_t *new_out_data = (out_new_api_t*)new_api_buf;
out_t *out_data = (out_t*)data;
// ... copy values from new_out_data to out_data
}
我不确定的一点是,从'uint8_t []'到'in_new_api_t *'的转换是否会违反严格的别名规则或引起任何其他问题。访问性能问题也是一个问题。
如果是这样,最好的解决方案是什么?
我可以复制in_t和out_t
并使in_new_api_t
指向data
然后我需要复制数据4次以确保我没有覆盖值:从data
到in_t tmp_in
,从tmp_in
到in_new_api,
然后从out_new_api
到out_t tmp_out
和从tmp_out
到out_t out
。
听起来你想要的是几种union
类型。根据标准,struct
的union
成员的公共初始序列是布局兼容的,并且可以以与每个sockaddr_*
类型的族字段完全相同的方式彼此映射。联合上的类型惩罚在C语言中是合法的,但在C ++中则不合适(尽管它在每个编译器上都与POD一起使用,但是没有尝试与现有代码兼容的编译器会破坏它,并且任何可能的替代方案也是未定义的行为)。这可能会消除对副本的需要。
保证union
适合两者。如果你确实使用指针,为了以防万一,Alignas
对象可能是一个好主意。
来自memcpy()
阵列的unsigned char
也是合法的;语言标准在复制对象表示后调用数组的内容。
你在recv_msg()
所做的事情显然是未定义的行为,并且可能有一天会破坏你的代码,因为编译器有权在从*in_data
移动到*out_data
时做任何想做的事情。此外,如果void* data
论证不是来自malloc()
(和表兄弟)或来自最初是in_t
的对象,那么即使在那里你也有UB和对齐问题。
保存RAM的方法非常危险。即使你足够大胆地忽略了使用非法但正确对齐的类型访问内存的更理论的UB情况,你仍然会遇到问题,因为根本无法保证从一个结构到就地复制的操作顺序其他不会丢弃您的数据。
这是相当直截了当的:
- 当
void*
的指向数据具有任何不同类型时,转换为指向结构的指针类型是严格的别名冲突。 - 从指向原始字符缓冲区的指针转换为指向结构的指针是严格的别名冲突。 (但是你可以反过来说:从指针到结构到指向char的指针。)
所以你的代码看起来非常不安全,并且由于void指针也有点令人困惑。所以排名第一的是摆脱那个危险,危险的无效指针!您可以创建一个类型,例如:
typedef union
{
in_t old;
in_new_api_t new;
uint8_t bytes [sizeof(in_new_api_t)];
} in_api_t;
然后将其用作函数的参数。
这将首先允许您以安全的方式访问每个结构的初始部分,而不会违反别名(6.5.2.3,关于常见初始序列的规则)。也就是说,成员a
和b
将在两个结构中相互对应。您唯一不能依赖的是不一样的成员 - 必须使用memcpy明确复制这些成员。
其次,当您需要序列化数据时,您现在可以使用bytes
成员。如果以类似的方式将“out”结构写为联合,并且它们也包含完全相同大小的bytes
成员,则可以安全地从一种类型转换为另一种类型,而不会出现严格的别名违规。这是C11 6.5允许的:
对象的存储值只能由具有以下类型之一的左值表达式访问: - 与对象的有效类型兼容的类型 / - / - 聚合或联合类型,其成员中包含上述类型之一
如果你的union由一个指向union类型的指针访问,它包含一个完全相同大小的字节数组(兼容类型),那么这是允许的。
以上是关于从空字节数组转换为结构指针可能会违反严格的别名?的主要内容,如果未能解决你的问题,请参考以下文章
C++ 严格别名:这不是 MSFT 示例 UB 吗? [复制]