如何投射 sockaddr_storage 并避免违反严格的别名规则
Posted
技术标签:
【中文标题】如何投射 sockaddr_storage 并避免违反严格的别名规则【英文标题】:How to cast sockaddr_storage and avoid breaking strict-aliasing rules 【发布时间】:2009-09-15 21:10:40 【问题描述】:我正在使用 Beej 的网络指南并遇到了别名问题。他提出了一个函数来返回特定结构的 IPv4 或 IPv6 地址:
1 void *get_in_addr( struct sockaddr *sa )
2
3 if (sa->sa_family == AF_INET)
4 return &(((struct sockaddr_in*)sa)->sin_addr);
5 else
6 return &(((struct sockaddr_in6*)sa)->sin6_addr);
7
这会导致 GCC 在第 3 行为 sa 吐出严格别名错误。据我了解,这是因为我这样调用此函数:
struct sockaddr_storage their_addr;
...
inet_ntop(their_addr.ss_family,
get_in_addr((struct sockaddr *)&their_addr),
connection_name,
sizeof connection_name);
我猜测别名与their_addr
变量的类型为sockaddr_storage
而另一个不同类型的指针指向同一个内存这一事实有关。
将sockaddr_storage
、sockaddr_in
和sockaddr_in6
加入工会的最佳方法是什么?看起来这在网络中应该是一个很好的领域,我只是找不到任何具有最佳实践的好例子。
另外,如果有人能准确解释混叠问题发生的位置,我将不胜感激。
【问题讨论】:
您可以将get_in_addr()
更改为struct sockaddr_storage *
并忘记通话中的演员吗?
谢谢你,mark4o!我不知道为什么我让这变得比它需要的更难。你的建议很有效。
@mark4o 那岂不是将问题转移到get_in_addr
的第4行和第6行?
【参考方案1】:
我倾向于这样做是为了让 GCC 使用 type-punning 做正确的事情,这在工会中是明确允许的:
/*! 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;
【讨论】:
Union 可能是为此而发明的,我同意编译器应该处理它。但标准并没有具体说明。对它的支持是 gcc 提供的额外保证,因此它可能在另一个编译器上失败,并且编译器的开发人员会争辩说他们是对的。在未来,gcc 开发人员也可以做同样的事情。在 C 世界中,趋势是打破现有的编程实践,在速度基准测试中获得 0.5%,而严格别名本身只是这种趋势的一个实例。 C99 标准特别允许联合用于此目的。请参阅open-std.org/jtc1/sc22/wg14/www/docs/n1124.pdf §6.5,第 7 段。 我似乎记得 POSIX 保留了以 _t 结尾的类型名称。对不起,我很迂腐。 @NikolaiNFetissov “union 正是为此而发明的”来源?这是对union
的滥用!
另外,6.5.2.3/3 “值是指定成员的值”,没有 C90 的警告说,如果访问与上次用于存储一个不同的联合成员,它是实现定义的价值。即它在 C90 中是实现定义的,但不在 C99 和 C11 中。说你可能做不到的规范措辞被故意删除,所以现在你绝对可以做到。【参考方案2】:
我倾向于这样做是为了让 GCC 使用类型双关语做正确的事情,这在联合中是明确允许的
我很确定这种(错误)使用 union 不会对 GCC 起作用(或只是偶然):
短 type_pun2 (int i, int *pi, short *ps) *pi = 我; 返回 *ps; 联合 U 诠释我; 短裤; ; 短类型双关(int i) 你; 返回 type_pun2 (i, &u.i, &u.s);正确的做法是使用memcpy
,而不是union
。
【讨论】:
但是在您的代码中,您传递了两个 do 别名的指针。 Nikolai 对sockaddr
类型代码别名的建议并不意味着这一点,并且是类型双关的有效使用,它在设计上与 GCC 一起使用。
@JonathanWakely "但是在你的代码中你传递了两个做别名的指针。" 是的,而且? “and is an valid use of type-punning”谁说的?
@cmccabe 任何人都应该明白,联合只有在其行为得到明确定义的情况下才有用。不能用联合进行类型双关的想法当然并不意味着联合是无用的,只是对类型双关无用。是什么让您认为工会是为了进行类型双关而发明的? “C99 的语言指定您可以安全地访问联合中的任何值” 在哪里? “任何价值”是指“任何成员”吗? “安全”是什么意思?
@cmccabe 您的“这对任何人来说都应该是显而易见的”似乎暗示我在这里遗漏了一些非常明显的东西。这不仅有点侮辱,而且考虑到我已经大量参与了 C++ 标准工作,而且(较少)参与了 C 标准工作,错过“任何人都清楚”的东西对我来说是不可思议的。一般来说,涉及类型双关的问题在 C 或 C++ 中都不是“显而易见的”,即使对于 C 和 C++ 委员会成员也是如此。
@davmac 我担心 C 委员会失去了所有有能力的成员,没有人能够解决这个微妙的问题或写出体面的规范。这个问题很糟糕很糟糕。对于 C 的许多实际应用,它在实践中是基本且必不可少的,但没有人能回答简单的问题。 通过联合允许类型双关语会打开一堆蠕虫,直到基于类型的别名规则没有意义。 我们得到的不是数学,而是感觉和模糊的逻辑。 C 委员会甚至破坏了“memcpy
对于类型双关语是安全的”,从而使事情变得更糟。 C++ 也很糟糕。【参考方案3】:
我最近在尝试编写代码以获取机器的 MAC 地址时,在 HPUX 系统上遇到了类似的别名警告
&(((struct sockaddr_in *)addr)->sin_addr)
抱怨严格的别名规则
这是某些上下文中的代码
char ip[INET6_ADDRSTRLEN] = 0;
strucut sockaddr *addr
...
get addr from ioctl(socket,SOCGIFCONF...) call
...
inet_ntop(AF_INET, &(((struct sockaddr_in *)addr)->sin_addr),ip,sizeof ip);
我通过执行以下操作克服了别名警告
struct sockaddr_in sin;
memcpy(&sin,addr,sizeof(struct sockaddr));
inet_ntop(AF_INET, &sin.sin_addr,ip,sizeof ip);
虽然这可能很危险,但我在它之前添加了以下几行
static_assert(sizeof(sockaddr)==sizeof(sockaddr_in));
我不确定这是否会被认为是不好的做法,但它确实有效,并且可以跨平台到其他 *Nix 风格和编译器
【讨论】:
【参考方案4】:该问题与对函数的调用无关。相反,它是((struct sockaddr_in*)sa)->sin_addr
。问题是 sa
是一种类型的指针,但您将其转换为不同类型的指针,然后取消引用它。这打破了一个叫做“严格别名”的规则,它说不同类型的变量永远不能别名。在您的情况下,别名为不同的类型正是您想要做的。
简单的解决方案是关闭此优化,它允许以这种方式出现别名。在 GCC 上,标志是 -fno-strict-aliasing
。
如 Nikolai 所述,更好的解决方案是使用联合。
void *get_in_addr(struct sockaddr *sa)
union
struct sockaddr *sa;
struct sockaddr_in *sa_in;
struct sockaddr_in6 *sa_in6;
u;
u.sa = sa;
if (sa->sa_family == AF_INET)
return &(u.sa_in->sin_addr);
else
return &(u.sa_in6->sin6_addr);
也就是说,在使用您的原始代码时,我实际上无法让 GCC 给我一个警告,所以我不确定这是否能给您带来任何好处。
【讨论】:
使用指针联合在指向不同类型对象的指针之间进行转换可能会对编译器隐藏严格别名违规,但它仍然是违规。 Nikolai 正确地建议使用结构联合,而不是指针联合,因为这是在标准允许的结构类型之间进行转换的唯一安全方法。 您使用union
进行类型双关语,因此您现在有两个非法别名,而不是一个非法别名:原始sockaddr_storage
与sockaddr
别名,还有sockaddr*
与@ 987654329@ 与 sockaddr_in6*
混叠。通过将其隐藏在函数中,编译器更有可能看不到正在发生的事情,并且它会生成错误代码并且不会发出警告。
所有投反对票的人都是错误的。对于 99% 的项目,使用 -fno-strict-aliasing 是很好的建议。其余的可以在热路径上使用“限制”。最后,那些说工会行为不明确的人是错误的。这是来自 C99 的语言:“如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同,则将值的对象表示的适当部分重新解释为6.2.6 中描述的新类型中的对象表示(有时称为“类型双关语”的过程)。”
@cmccabe "对于 99% 的项目来说,使用 -fno-strict-aliasing 是很好的建议。" 鉴于 GCC 团队对此非常困惑被称为“严格别名”规则,到了这一点他们说my_malloc
函数不能在符合C代码的情况下实现,禁用这种疯狂可能是个好主意!
@cmccabe "这是来自 C99 的语言:“如果用于访问联合对象内容的成员与上次用于在对象中存储值的成员不同" 你错过了整点:这仅适用于联合,这里没有联合。有一个sockaddr
。此外,你错过了另一点,即没有迹象表明不同的对象指针类型具有兼容的表示。以上是关于如何投射 sockaddr_storage 并避免违反严格的别名规则的主要内容,如果未能解决你的问题,请参考以下文章