Flexible Array成员访问union原始字节

时间:2016-10-28 11:51:31

标签: c embedded standards

在嵌入式平台上。

假设我通过串行线从从设备接收字节,数据被正确序列化和反序列化,以确保字节顺序和数据大小正确。

我真正想要实现的是让struct test具有可变大小,以允许将来扩展数组成员。

  • 多平台应用程序是否存在意外行为?
  • 是否有c标准的部分禁止此类访问?
#include <stdio.h>
#include <stdint.h>

struct test
{
    uint32_t a;
    uint32_t b;
    uint32_t c[];
};

union test1
{
    struct test A;
    uint8_t B[256];
};

int main(void)
{
    union test1 test2;

    for (uint32_t i=0; i<256; i++)
    {
        test2.B[i] = i;
    }

    for (size_t i=0; i<(sizeof(test2.B)/sizeof(uint32_t))-2; i++)
        printf("Test: 0x%08X\n", test2.A.c[i]);
}

2 个答案:

答案 0 :(得分:3)

需要考虑的一些事项:

  • 对齐。您不能轻易地假设结构或联合不会有填充字节。从理论上讲,一些与int大小相关的模糊对齐要求的系统可能会导致结构中的填充字节。

    由于该场景主要是理论上的,因此您可以通过添加

    来确保不会发生这种情况
    _Static_assert(sizeof(struct test) == sizeof(int)+sizeof(int),
               "Padding detected!");
    
  • 正如问题所述,Endianess是一个真正的问题,必须在某处处理。

  • 签名int可能在联盟或嵌入式系统中的任何其他地方都没有任何意义。这些可能会在很多方面造成破坏,但不会发布任何代码。它们应该用stdint.h中的确定性大小和签名类型替换。
  • 打字。虽然实现定义,但类型惩罚很好。我不明白为什么你的代码会在常规的二进制补码系统上引起问题。但理论上,你可能会在不使用二进制补码的异常系统中获得可移植性问题,但实现填充位,陷阱位等。我不会过分关注这种几乎不存在的系统的可移植性。
  • C标准。您显然无法将灵活的阵列成员代码移植到C90系统。它可能在那里编译但是调用未定义的行为。此外,标准委员会的一些迟钝分支在C11中选择了stdint.h。我不会那么担心。

总的来说,我会说代码很好并且可以移植到所有有用的系统,只要你在某处处理endianess并摆脱int

答案 1 :(得分:2)

在C89和C99的原始出版物中,写入union的一个成员并从另一个成员读取具有实现定义的行为。在TC1到C99中,它已更改为未指定的行为。无论哪种方式,实际意义都是相同的:你可以写一个联盟的一个成员并从另一个回读,而不用担心恶魔飞出你的鼻子;标准并没有告诉你你会得到什么,但是如果知道实施它应该是可以预测的。

话虽如此,您很可能遇到struct test中的填充,字节顺序不一致等问题。使用stdint.h固定宽度类型而不是int可以减轻其中一些问题,并尽可能使用无符号类型。我还强烈建议您从外部协议所处的任何明确的字节顺序中编写显式转换函数,例如

static int32_t
be32_to_cpu(const unsigned char *p)
{
    uint32_t x = 0;
    x |= ((uint32_t)p[0]) << 24;
    x |= ((uint32_t)p[1]) << 16;
    x |= ((uint32_t)p[2]) <<  8;
    x |= ((uint32_t)p[3]) <<  0;
    return (int32_t)x;
}

并使用手动计算的偏移量从unsigned char缓冲区手动复制,例如

struct test
{
    int32_t a;
    int32_t b;
    int32_t c[62];
}

void convert_block(struct test *restrict out,
                   const unsigned char *restrict buf)
{
    out->a = be32_to_cpu(&buf[0]);
    out->b = be32_to_cpu(&buf[4]);
    for (int i = 0; i < 62; i++)
        out->c[i] = be32_to_cpu(&buf[4 * (i+2)]);
}

现代编译器将识别be32_to_cpu中的习语并生成最佳代码。对于小端,只需改变移位顺序。请注意,必须将值组合在无符号变量中并在之后转换为signed,因为转换为符号位具有未定义的行为。

如果您的有线协议发送可变大小的数据包,那么可能会有一个大小字段,您需要使用它们来知道何时停止读取,以及制作缓冲区的大小:

struct test
{
    uint32_t size;
    int32_t b;
    int32_t c[]; /* SIZE/4 - 2 values */
};

struct test *
read_block(int fd)
{
    char b1[4];
    if (read(fd, b1, 4) < 4) abort();
    uint32_t size = be32u_to_cpu(b1);

    char b2[size - 4];
    if (read(fd, b2, size - 4) < size - 4) abort();

    struct test *out = malloc(size);
    out->size = size;
    out->b = be32s_to_cpu(&b2[0]);
    for (int i = 0; i < size/4 - 2; i++)
        out->c[i] = be32s_to_cpu(&b2[(i+1)*4]);

    return out;
}

正确处理错误和短读作为练习。