4

POSIX 打算将指针指向可转换的变体struct sockaddr,但是根据 C 标准的解释,这可能违反了严格的别名规则,因此违反了 UB。(请参阅此答案及其下方的评论。)我至少可以确认 gcc 至少可能存在问题:此代码Bug!在启用优化和Yay!禁用优化的情况下打印:

#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>

sa_family_t test(struct sockaddr *a, struct sockaddr_in *b)
{
    a->sa_family = AF_UNSPEC;
    b->sin_family = AF_INET;
    return a->sa_family; // AF_INET please!
}

int main(void)
{
    struct sockaddr addr;
    sa_family_t x = test(&addr, (struct sockaddr_in*)&addr);
    if(x == AF_INET)
        printf("Yay!\n");
    else if(x == AF_UNSPEC)
        printf("Bug!\n");
    return 0;
}

在在线 IDE上观察此行为。

要解决此问题,此答案建议使用带有联合的类型双关语:

/*! Multi-family socket end-point address. */
typedef union address
{
    struct sockaddr sa;
    struct sockaddr_in sa_in;
    struct sockaddr_in6 sa_in6;
    struct sockaddr_storage sa_stor;
}
address_t;

然而,显然事情并没有看起来那么简单……引用@zwol 的评论

可以工作,但需要相当小心。超出了我可以放入此评论框的内容。

它需要什么样的照顾?使用带有联合的类型双关语在 的变体之间进行转换的陷阱是什么struct sockaddr

我宁愿问也不愿遇到 UB。

4

2 回答 2

1

鉴于address_t您提议的工会

typedef union address
{
    struct sockaddr sa;
    struct sockaddr_in sa_in;
    struct sockaddr_in6 sa_in6;
    struct sockaddr_storage sa_stor;
}
address_t;

和一个声明为address_t的变量,

address_t addr; 

您可以安全地初始化addr.sa.sa_family然后读取addr.sa_in.sin_family(或任何其他别名_family字段对)。您还可以安全地addr在对recvfromrecvmsgaccept或任何其他带有struct sockaddr *输出参数的套接字原语的调用中使用,例如

bytes_read = recvfrom(sockfd, buf, sizeof buf, &addr.sa, sizeof addr);
if (bytes_read < 0) goto recv_error;
switch (addr.sa.sa_family) {
  case AF_INET:
    printf("Datagram from %s:%d, %zu bytes\n",
           inet_ntoa(addr.sa_in.sin_addr), addr.sa_in.sin_port,
           (size_t) bytes_read);
    break;
  case AF_INET6:
    // etc
}

你也可以往另一个方向走,

memset(&addr, 0, sizeof addr);
addr.sa_in.sin_family = AF_INET;
addr.sa_in.sin_port = port;
inet_aton(address, &addr.sa_in.sin_addr);
connect(sockfd, &addr.sa, sizeof addr.sa_in);

也可以使用 分配address_t缓冲区malloc,或将其嵌入更大的结构中。

不安全的是将指向联合的各个子结构的指针传递给address_t您编写的函数。例如,您的test功能...

sa_family_t test(struct sockaddr *a, struct sockaddr_in *b)
{
    a->sa_family = AF_UNSPEC;
    b->sin_family = AF_INET;
    return a->sa_family; // AF_INET please!
}

...不能(void *)aequal to调用(void *)b,即使发生这种情况是因为调用站点传递了&addr.sa并且&addr.sa_in作为参数。address_t有些人曾经争辩说,当定义的完整声明在范围内时应该允许这样做test,但这对于编译器开发人员来说太像“ spukhafte Fernwirkung ”了。当前一代编译器采用的“公共初始子序列”规则(在 Felix 的回答中引用)的解释是,它仅适用于联合类型在特定访问中静态和本地涉及的情况。你必须改写

sa_family_t test2(address_t *x)
{
    x->sa.sa_family = AF_UNSPEC;
    x->sa_in.sa_family = AF_INET;
    return x->sa.sa_family;
}

您可能想知道为什么可以传递&addr.saconnect那时。非常粗略,connect有自己的内部address_t联合,它以类似的东西开头

int connect(int sock, struct sockaddr *addr, socklen_t len)
{
    address_t xaddr;
    memcpy(xaddr, addr, len);

在这一点上,它可以安全地检查xaddr.sa.sa_family然后xaddr.sa_in.sin_addr或其他什么。

当调用者本身可能没有使用这样的联合时,是否可以将connect参数转换addr为,我不清楚;address_t *我可以从标准的文本中想象两种方式的论点(在某些关键点上与“对象”、“访问”和“有效类型”的确切含义有关​​),我没有知道编译器实际上会做什么。实际上connect,无论如何都必须进行复制,因为它是一个系统调用,并且几乎所有通过用户/内核边界的内存块都必须被复制。

于 2017-05-31T13:09:01.867 回答
1

使用union这样是安全的,

从 C11 §6.5.2.3 开始:

  1. 后缀表达式后跟 . 运算符和标识符指定结构或联合对象的成员。该值是命名成员的值,95) 如果第一个表达式是左值,则该值是左值。如果第一个表达式具有限定类型,则结果具有指定成员类型的限定版本。

95) 如果用于读取联合对象内容的成员与上次用于在对象中存储值的成员不同,则将值的对象表示的适当部分重新解释为新对象中的对象表示6.2.6 中描述的类型(有时称为“类型双关”的过程)。这可能是一个陷阱表示。

  1. 一个特殊的保证是为了简化联合的使用:如果联合包含多个共享相同初始序列的结构(见下文),并且如果联合对象当前包含这些结构之一,则允许检查公共它们中的任何一个的初始部分,在任何地方都可以看到已完成联合类型的声明。如果对应的成员对于一个或多个初始成员的序列具有兼容的类型(并且,对于位域,具有相同的宽度),则两个结构共享一个共同的初始序列

(强调我认为最重要的)

通过访问该struct sockaddr成员,您将从通用初始部分读取。


注意:这不会使将指针传递给任何地方的成员并期望编译器知道它们引用相同的存储对象是安全的。因此,您的示例代码的文字版本可能仍然会中断,因为您test()的 theunion是未知的。

例子:

#include <stdio.h>

struct foo
{
    int fooid;
    char x;
};

struct bar
{
    int barid;
    double y;
};

union foobar
{
    struct foo a;
    struct bar b;
};

int test(struct foo *a, struct bar *b)
{
    a->fooid = 23;
    b->barid = 42;
    return a->fooid;
}

int test2(union foobar *a, union foobar *b)
{
    a->a.fooid = 23;
    b->b.barid = 42;
    return a->a.fooid;
}

int main(void)
{
    union foobar fb;
    int result = test(&fb.a, &fb.b);
    printf("%d\n", result);
    result = test2(&fb, &fb);
    printf("%d\n", result);
    return 0;
}

在这里,test()可能会中断,但test2()会是正确的。

于 2017-05-29T15:23:42.993 回答