从空字节数组转换为结构指针可能会违反严格的别名?

时间:2018-03-06 08:48:07

标签: c casting memory-alignment strict-aliasing

大多数人关注的是如果他们收到带有数据的字节数组并且他们想要将数组转换为结构指针会发生什么 - 这可能违反严格的别名规则。我不确定是否初始化一个足够大的空字节数组,将其转换为结构指针,然后填充结构成员将违反严格的别名规则。

细节: 假设我有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次,以确保我不会覆盖值:从datain_t tmp_in,从tmp_inin_new_api,,然后从out_new_apiout_t tmp_out,从tmp_out到{{1 }}

3 个答案:

答案 0 :(得分:1)

听起来你想要的是几种union类型。 struct union成员的公共初始序列按照标准是布局兼容的,并且可以相互映射,与每个{{1}的族字段完全相同}类型。联合上的类型惩罚在C语言中是合法的,但在C ++中则不合适(尽管它在每个编译器上都与POD一起使用,但是没有尝试与现有代码兼容的编译器会破坏它,并且任何可能的替代方案也是未定义的行为)。这可能无需复制。

保证sockaddr_*与两者正确对齐。如果您确实使用指针,那么union两个类型的对象可能是个好主意,以防万一。

来自Alignas数组的memcpy()也是合法的;语言标准在复制对象表示后调用数组的内容。

答案 1 :(得分:1)

您在recv_msg()中所做的事情显然是未定义的行为,并且可能会在某一天破坏您的代码,因为编译器有权在从*in_data移动到*out_data时执行任何操作。另外,如果void* data参数不是来自malloc()(和表兄弟),或来自最初为in_t的对象,那么即使存在UB和对齐问题。

保存RAM的方法风险极大。即使你足够大胆地忽略了更为理论化的UB使用非法但正确对齐的类型访问内存的情况,你仍然会遇到问题,因为根本无法保证复制就地的操作顺序从一个结构到另一个结构,不会丢弃你的数据。

答案 2 :(得分:1)

这是相当简单的:

  • 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,关于常见初始序列的规则)。也就是说,成员ab将在两个结构中相互对应。您唯一不能依赖的是不相同的成员 - 必须使用memcpy明确复制这些成员。

其次,当您需要序列化数据时,您现在可以使用bytes成员。如果你写" out"以类似的方式构造为联合,并且它们也包含完全相同大小的bytes成员,您可以安全地从一种类型转换为另一种类型,而不会出现严格的别名违规。 C11 6.5允许这样做:

  

对象的存储值只能由具有以下类型之一的左值表达式访问:
   - 与对象的有效类型兼容的类型
  / - /
   - 聚合或联合类型,其成员中包含上述类型之一

如果你的联合是由一个指向联合类型的指针访问的,它包含一个大小完全相同的字节数组(一个兼容的类型),那么就允许这样做。