在我之前的question之后,我对这段代码非常好奇-
case AF_INET:
{
struct sockaddr_in * tmp =
reinterpret_cast<struct sockaddr_in *> (&addrStruct);
tmp->sin_family = AF_INET;
tmp->sin_port = htons(port);
inet_pton(AF_INET, addr, tmp->sin_addr);
}
break;
在问这个问题之前,我已经搜索了关于同一主题的SO,并得到了关于这个主题的混合响应。例如,请参见this,this和this帖子,它们说使用这种代码在某种程度上是安全的。还有另一个post帖子说使用联合来完成这样的任务,但对接受的答案的评论再次提出不同意见。
微软的documentation在相同的结构上说-
应用程序开发人员通常只使用SOCKADDR_STORAGE的ss_family成员。其余成员确保SOCKADDR_STORAGE可以包含IPv6或IPv4地址,并且该结构被适当地填充以实现64位对齐。这种对齐使特定于协议的套接字地址数据结构能够访问SOCKADDR_STORAGE结构中的字段,而不会出现对齐问题。通过填充,SOCKADDR_STORAGE结构的长度为128字节。
OpenGroup的documentation状态-
标头应定义sockaddr_storage结构。该结构应为:
足够大,可容纳所有受支持的协议特定地址结构
在适当的边界对齐,以便指向它的指针可以转换为指向协议特定的地址结构的指针,并用于访问这些结构的字段,而不会出现对齐问题
socket的手册页也是这么说的-
此外,套接字API提供了数据类型struct sockaddr_storage,这种类型适合于容纳所有支持的域特定套接字地址结构;它足够大并且正确对齐。(特别是,它足够大以容纳IPv6套接字地址。)
我在C
和C++
中看到过多种使用这种类型转换的实现,现在我不确定哪一种是正确的,因为有一些帖子与上面的声明相矛盾-this和this。
那么哪一个是安全和正确的填充sockaddr_storage
结构的方法呢?这些指针类型转换安全吗?还是union method?我也知道getaddrinfo()
调用,但是对于上面的填充结构的任务来说似乎有点复杂。还有另外一个recommended way with memcpy,这个安全吗?
2条答案
按热度按时间vfhzx4xs1#
在过去的十年里,C和C++编译器已经变得比
sockaddr
接口设计时,甚至比C99编写时复杂得多。作为其中的一部分,“未定义行为”的理解 * 目的 * 已经改变。回到过去,未定义的行为通常是为了掩盖硬件实现中关于操作语义的分歧。2但是现在,多亏了一些组织,他们不想再写FORTRAN,并且有能力支付编译器工程师的费用来实现这一目标,未定义行为是编译器用来 * 对代码进行推断 * 的一个东西。左移就是一个很好的例子:C99 6.5.7p3,4(为清晰起见,稍微重新排列)读作E1 << E2
的结果是E1
左移E2
比特位置;空出的位用零填充。如果[E2
]的值为负值或大于或等于提升的[E1
]的宽度,则行为未定义。例如,
1u << 33
在unsigned int
为32位宽的平台上是UB,委员会之所以定义它是因为不同CPU架构的左移指令在这种情况下做不同的事情:有些始终产生零,有些减少了以类型宽度为模的移位数(x86),有些减少了以某个更大的数为模的移位数(ARM),并且至少有一个历史上常见的架构会陷阱(我不知道是哪一个,但这就是为什么它是未定义的,而不是未指定的)。在32位
unsigned int
平台上,编译器知道上面的UB规则,当函数被调用时,它会推断y
必须有一个0到31范围内的值。它会将这个范围提供给过程间分析,并使用它来做一些事情,比如删除调用者中不必要的范围检查。如果程序员有理由认为它们不是不必要的,那么,现在你开始明白为什么这个主题是如此的蠕虫了。2(现代编译器可以将x << (y&31)
优化成一个移位指令,用于像x86这样的ISA,其中移位指令实现了屏蔽。3)有关未定义行为的目的的变化的更多信息,请参见LLVM人员关于该主题的三部分文章(123)。
现在你明白了,我可以回答你的问题了。
以下是
struct sockaddr
、struct sockaddr_in
和struct sockaddr_storage
的定义,省略了一些不相关的并发症:这是穷人的子类化。这是C语言中普遍存在的习惯用法。你定义了一组结构,它们都有相同的初始字段,初始字段是一个代码号,告诉你实际上传递了哪个结构。以前,每个人都认为如果你分配并填充了一个
struct sockaddr_in
,将其上转换为struct sockaddr
,并将其传递给connect
,X1 M17 N1 X的实现可以安全地解引用X1 M18 N1 X指针以检索X1 M19 N1 X字段,获知其正在查看X1 M20 N1 X,将其强制转换回,C标准总是说,解引用struct sockaddr
指针会触发未定义的行为--这些规则自C89以来一直没有改变--但是每个人都期望它是安全的 * 在这种情况下 *,因为无论你实际上使用的是哪种结构,它都是相同的“加载16位”指令。早在20世纪90年代,编写这些规范的人就认为,如果你最终发出了一个未对齐的内存访问,那么这可能会成为麻烦。但是标准的文本没有提到任何关于加载指令和对齐的内容,它是这样说的(C99 §6.5p7 +脚注):
一个对象的存储值只能由具有以下类型之一的左值表达式访问:73)
73)此列表的目的是指定对象可以使用别名或不使用别名的情况。
struct
类型只与自身“兼容”,声明变量的“有效类型”就是它的声明类型。...有未定义的行为,编译器可以从中做出推断,* 即使 * 幼稚的代码生成会如预期的那样运行。现代编译器可能从中推断出
case AF_INET
永远不会被执行。它会删除整个代码块作为死代码,然后搞笑事件就会接踵而至。那么如何安全地使用
sockaddr
呢?最简短的回答是“只需使用getaddrinfo
和getnameinfo
”,他们为您处理这个问题。但是也许你需要处理一个地址族,比如
AF_UNIX
,而getaddrinfo
不能处理,在大多数情况下你可以只声明一个地址族的正确类型的变量,并且只在调用接受struct sockaddr *
的函数时才强制转换它connect
的 * 实现 * 必须跳过一些限制才能使其安全,但这不是你的问题。[EDIT 2023年1月:这个答案过去对
sockaddr_storage
的描述是错误的,我很不好意思承认我六年来都没有注意到这个问题。]在需要同时处理IPv4和IPv6地址的服务器中,使用struct sockaddr_storage
作为了解调用getpeername
的缓冲区大小的一种方便方法是很诱人的。如果您将union
与您关心的每个具体地址族一起使用,再加上普通的struct sockaddr
,则更不容易出错,并且严格别名问题也更少:有了这个公式,您不仅不需要编写任何类型转换来调用
getpeername
或getnameinfo
,而且 * 可以 * 安全地访问addrbuf.sa.sa_family
,然后在访问sa_family == AF_INET
时访问addrbuf.sin.sin_*
。最后一点:如果BSD的人对sockaddr结构的定义稍微有一点不同...
...由于“包含上述类型之一的聚合或联合”规则,向上转换和向下转换将被完美地定义。如果你想知道在新的C代码中应该如何处理这个问题,这里可以找到。
uqxowvwt2#
是的,这样做违反了别名规则,所以不要这样做,没有必要 * 永远 * 使用
sockaddr_storage
;这是个历史性的错误。但有几种安全的方法可以使用它:malloc(sizeof(struct sockaddr_storage))
。在这种情况下,直到您向指向的内存中存储了一些内容,它才具有有效类型。1.作为联合体的一部分,显式地访问你想要的成员,但是在这种情况下,只需要把你想要的实际
sockaddr
类型(in
和in6
,也许还有un
)放在联合体中,而不是sockaddr_storage
。当然,在现代编程中,你根本不需要创建
struct sockaddr_*
类型的对象,只需要使用getaddrinfo
和getnameinfo
在字符串表示和sockaddr
对象之间转换地址,并将后者视为完全不透明的对象。