【问题标题】:Correct, portable way to interpret buffer as a struct将缓冲区解释为结构的正确、可移植的方式
【发布时间】:2013-10-10 12:03:11
【问题描述】:

我的问题的背景是网络编程。假设我想通过网络在两个程序之间发送消息。为简单起见,假设消息看起来像这样,字节顺序不是问题。我想找到一种正确、可移植且有效的方法来将这些消息定义为 C 结构。我知道这有四种方法:显式强制转换、通过联合强制转换、复制和编组。

struct message {
    uint16_t logical_id;
    uint16_t command;
};

显式转换:

void send_message(struct message *msg) {
    uint8_t *bytes = (uint8_t *) msg;
    /* call to write/send/sendto here */
}

void receive_message(uint8_t *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    struct message *msg = (struct message*) bytes;
    /* And now use the message */
    if (msg->command == SELF_DESTRUCT)
        /* ... */
}

我的理解是send_message 不违反别名规则,因为字节/字符指针可以为任何类型设置别名。但是,反之亦然,因此receive_message 违反了别名规则,因此具有未定义的行为。

通过联合铸造:

union message_u {
    struct message m;
    uint8_t bytes[sizeof(struct message)];
};

void receive_message_union(uint8_t *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    union message_u *msgu = bytes;
    /* And now use the message */
    if (msgu->m.command == SELF_DESTRUCT)
        /* ... */
}

但是,这似乎违反了联合在任何给定时间仅包含其一个成员的想法。此外,如果源缓冲区未在字/半字边界上对齐,这似乎会导致对齐问题。

复制:

void receive_message_copy(uint8_t *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    struct message msg;
    memcpy(&msg, bytes, sizeof msg);
    /* And now use the message */
    if (msg.command == SELF_DESTRUCT)
        /* ... */
}

这似乎可以保证产生正确的结果,但我当然更愿意不必复制数据。

编组

void send_message(struct message *msg) {
    uint8_t bytes[4];
    bytes[0] = msg.logical_id >> 8;
    bytes[1] = msg.logical_id & 0xff;
    bytes[2] = msg.command >> 8;
    bytes[3] = msg.command & 0xff;
    /* call to write/send/sendto here */
}

void receive_message_marshal(uint8_t *bytes, size_t len) {
    /* No longer relying on the size of the struct being meaningful */
    assert(len >= 4);    
    struct message msg;
    msg.logical_id = (bytes[0] << 8) | bytes[1];    /* Big-endian */
    msg.command = (bytes[2] << 8) | bytes[3];
    /* And now use the message */
    if (msg.command == SELF_DESTRUCT)
        /* ... */
}

仍然需要复制,但现在与结构的表示分离。但是现在我们需要明确每个成员的位置和大小,字节序是一个更明显的问题。

相关信息:

What is the strict aliasing rule?

Aliasing array with pointer-to-struct without violating the standard

When is char* safe for strict pointer aliasing?

http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html

现实世界的例子

我一直在寻找网络代码的示例,以了解其他地方如何处理这种情况。 light-weight ip 有几个类似的案例。在udp.c 文件中包含以下代码:

/**
 * Process an incoming UDP datagram.
 *
 * Given an incoming UDP datagram (as a chain of pbufs) this function
 * finds a corresponding UDP PCB and hands over the pbuf to the pcbs
 * recv function. If no pcb is found or the datagram is incorrect, the
 * pbuf is freed.
 *
 * @param p pbuf to be demultiplexed to a UDP PCB (p->payload pointing to the UDP header)
 * @param inp network interface on which the datagram was received.
 *
 */
void
udp_input(struct pbuf *p, struct netif *inp)
{
  struct udp_hdr *udphdr;

  /* ... */

  udphdr = (struct udp_hdr *)p->payload;

  /* ... */
}

其中 struct udp_hdr 是 udp 标头的打包表示,p-&gt;payloadvoid * 类型。根据我的理解和this 的回答,这肯定 [edit-not] 破坏了严格的混叠,因此具有未定义的行为。

【问题讨论】:

  • 我想最正确(不正确)的方法是分配一个新结构,分别读取值并一一填写。
  • @Kninnug 是的,但我也在寻找效率和优雅。对于像这样的简单示例,手动执行它不会有问题,但是当有更多字段和不匹配的字节顺序时,它很快就会变得丑陋。
  • 由于字节序、填充等可能存在差异,因此不会有一种光滑、单行、优雅的方式来执行此操作。我认为@Kninnug 关于哪种方法最可靠是正确的。您可以进行联合或内存复制,但随后您需要对幕后发生的事情做出一些强有力的假设,而且它的可移植性会更差。
  • 请为 C 和 C++ 输入单独的问题,并从该问题中删除其中一个标签。对于不同的语言,答案是不同的。例如,C 1999 允许访问除最后一个存储的联合成员之外的联合成员(字节被重新解释为新类型),但 C++ 不允许。
  • 如果您不考虑字节顺序、成员大小、填充和对齐等问题,则很难将结果标记为“可移植”。

标签: c networking portability strict-aliasing


【解决方案1】:

我想这是我一直试图避免的,但我终于亲自去看看C99 standard。以下是我发现的(重点补充):
§6.3.2.2 无效

1 void 表达式(具有 void 类型的表达式)的(不存在的)值不应 以任何方式使用,并且不得进行隐式或显式转换(无效除外) 应用于这样的表达。如果任何其他类型的表达式被评估为 void 表达式,其值或指示符被丢弃。 (一个 void 表达式被评估为它的 副作用。)

§6.3.2.3 指针

1 指向 void 的指针可以转换为指向任何不完整或对象的指针或从指针转换为指向任何不完整或对象的指针 输入。指向任何不完整或对象类型的指针都可以转换为指向 void 的指针 又回来了;结果应与原始指针比较。

以及§3.14

1 个对象
执行环境中的数据存储区域,其内容可以表示 价值观

§6.5

一个对象的存储值只能由具有以下之一的左值表达式访问 以下类型:
与对象的有效类型兼容的类型,
— 与对象的有效类型兼容的类型的限定版本,
— 对应于对象有效类型的有符号或无符号类型,
— 有符号或无符号类型,对应于对象有效类型的限定版本,
— 聚合或联合类型,其中包含上述类型之一 成员(递归地包括子聚合或包含联合的成员),或
— 一种字符类型。

§6.5

访问其存储值的对象的有效类型是声明的类型
对象,如果有的话。 如果一个值通过 左值的类型不是字符类型,则左值的类型变为 该访问和不修改存储值的后续访问的对象的有效类型。如果一个值被复制到一个没有声明类型的对象中,使用 memcpy 或 memmove,或复制为字符类型的数组,则为有效类型 为该访问和后续访问不修改 value 是从中复制值的对象的有效类型(如果有的话)。为了 对没有声明类型的对象的所有其他访问,对象的有效类型是 只是用于访问的左值的类型。

§J.2 未定义的行为

— 尝试使用 void 表达式的值,或者隐式或显式 转换(除了 void)应用于 void 表达式(6.3.2.2)。

结论

void* 中转换是可以的(定义明确的),但在C99 中使用void 类型的值是不行的。因此,“现实世界的例子”不是未定义的行为。因此,显式转换方法可以通过以下修改使用,只要注意对齐、填充和字节顺序:

void receive_message(void *bytes, size_t len) {
    assert(len >= sizeof(struct message);
    struct message *msg = (struct message*) bytes;
    /* And now use the message */
    if (msg->command == SELF_DESTRUCT)
        /* ... */
}

【讨论】:

    【解决方案2】:

    正如您推测的那样,唯一正确的方法是将数据从char 缓冲区复制到您的结构中。您的其他替代方案违反了严格的别名规则或 one-member-of-union-active 规则。

    我想再花一点时间提醒您,即使您在单个主机上执行此操作并且字节顺序无关紧要,您仍然必须确保连接的两端都使用相同的选项和结构以相同的方式填充,类型的大小相同,等等。我建议至少花一点时间考虑真正的序列化实现,这样如果你需要支持更广泛的条件,你不需要'那么在你面前没有大的更新。

    【讨论】:

    • 我已经用我认为你描述的方法更新了帖子,以及来自开源项目的示例。
    • 所以我已经在帖子中介绍了所有基础知识?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-04-09
    • 1970-01-01
    相关资源
    最近更新 更多