如何在不违反严格别名规则的情况下合法地使用类型惩罚与联合在struct sockaddr的变体之间进行投射?

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

 #include  #include  #include  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 sockaddrstruct sockaddr之间使用类型双关语进行转换会有什么陷阱?

我更愿意问,而不是碰到UB。

使用这样的union是安全的,

来自C11§6.5.2.3:

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

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

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

(突出了我认为最重要的)

通过访问struct sockaddr成员,您将从 常见的初始部分中读取。


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

例:

 #include  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()会正确。

鉴于你提出的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 * out-parameter的套接字原语,例如

 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); 

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

什么是不安全的是将指针传递给address_t union的各个子结构到你编写的函数。 例如,你的testfunction......

 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 *)a等于(void *)b ,即使发生这种情况也是因为调用点传递了&addr.sa&addr.sa_in作为参数。 有些人曾经认为,当定义test时, address_t的完整声明在范围内时应该允许这样做,但这对于编译器开发者来说太像“ 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无论如何都必须进行复制,因为它是一个系统调用,几乎所有通过用户/内核边界传递的内存块都必须被复制。