围绕 sockaddr_storage 和 sockaddr_in 进行转换是否会破坏严格别名
- 2024-10-28 08:37:00
- admin 原创
- 55
问题描述:
根据我之前的问题,我对这个代码非常好奇 -
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 上搜索过关于同一主题的内容,并得到了关于这个主题的不同回复。例如,请参阅这个、这个和这个帖子,它们说使用这种代码在某种程度上是安全的。另外还有另一篇帖子说使用 union 来完成这样的任务,但对已接受答案的评论再次持不同意见。
微软关于同一结构的文档称 -
应用程序开发人员通常仅使用 SOCKADDR_STORAGE 的 ss_family 成员。其余成员确保 SOCKADDR_STORAGE 可以包含 IPv6 或 IPv4 地址,并且结构经过适当填充以实现 64 位对齐。这种对齐使协议特定的套接字地址数据结构能够访问 SOCKADDR_STORAGE 结构中的字段而不会出现对齐问题。加上填充,SOCKADDR_STORAGE 结构的长度为 128 字节。
Opengroup 的文档指出 -
标头应定义 sockaddr_storage 结构。此结构应为:
足够大以容纳所有支持的协议特定地址结构
在适当的边界上对齐,以便指向它的指针可以转换为指向协议特定的地址结构的指针,并用于访问这些结构的字段,而不会出现对齐问题
套接字的手册页也说了同样的话 -
此外,套接字 API 还提供了数据类型 struct sockaddr_storage。此类型适合容纳所有受支持的域特定套接字地址结构;它足够大并且对齐正确。(特别是,它足够大以容纳 IPv6 套接字地址。)
C
我已经在和语言中看到了使用此类强制转换的多种实现C++
,现在我不确定哪一个是正确的,因为有些帖子与上述说法相矛盾——这个和这个。
那么,哪种方法才是安全且正确的填充sockaddr_storage
结构的方法呢?这些指针强制转换安全吗?还是union 方法?我也知道这个getaddrinfo()
调用,但对于上述仅填充结构的任务来说,这似乎有点复杂。还有另一种推荐的 memcpy 方法,这安全吗?
解决方案 1:
在过去十年中,C 和 C++ 编译器已经比sockaddr
设计接口时甚至编写 C99 时复杂得多。其中,“未定义行为”的理解目的已经发生了变化。过去,未定义行为通常旨在掩盖硬件实现之间关于操作语义的分歧。但如今,由于许多组织希望不再编写 FORTRAN 并且有能力支付编译器工程师的费用来实现这一点,未定义行为已成为编译器用来推断代码的一种手段。左移就是一个很好的例子:C99 6.5.7p3,4(为了清晰起见略作重新排列)显示
的结果
E1 << E2
是E1
左移位E2
位置;空出的位用零填充。如果 [E2
] 的值为负数或大于或等于提升的 [E1
] 的宽度,则行为未定义。
例如,在 32 位宽的1u << 33
平台上是 UB unsigned int
。委员会将其设为未定义,因为不同 CPU 架构的左移指令在这种情况下会执行不同的操作:有些会一致地产生零,有些会以类型宽度为模数减少移位计数(x86),有些会以某个较大的数字为模数减少移位计数(ARM),并且至少有一种历史上常见的架构会陷入困境(我不知道是哪一种,但这就是它未定义而不是未指定的原因)。但是现在,如果你写
unsigned int left_shift(unsigned int x, unsigned int y)
{ return x << y; }
在 32 位平台上unsigned int
,编译器知道上述 UB 规则,在调用函数时会推断出 的y
值必须在 0 到 31 的范围内。它会将该范围输入到过程间分析中,并使用它来执行诸如删除调用者中不必要的范围检查之类的操作。如果程序员有理由认为它们不是不必要的,那么现在您开始明白为什么这个话题如此棘手了。(现代编译器可以优化x << (y&31)
为 x86 等 ISA 的单个移位指令,其中移位指令实现该掩码。)
有关未定义行为目的的这种变化的更多信息,请参阅 LLVM 人员关于此主题的三部分文章(1 2 3)。
现在您明白了这一点,我实际上可以回答您的问题了。
struct sockaddr
在省略了一些不相关的复杂因素后,这些是、struct sockaddr_in
和的定义struct sockaddr_storage
:
struct sockaddr {
uint16_t sa_family;
};
struct sockaddr_in {
uint16_t sin_family;
uint16_t sin_port;
uint32_t sin_addr;
};
struct sockaddr_storage {
uint16_t ss_family;
char __ss_storage[128 - (sizeof(uint16_t) + sizeof(unsigned long))];
unsigned long int __ss_force_alignment;
};
这是穷人的子类化。这是 C 中一种普遍存在的习语。您定义一组具有相同初始字段的结构,该字段是一个代码编号,可告诉您实际传递了哪个结构。过去,每个人都期望,如果您分配并填充一个struct sockaddr_in
,将其向上转换为struct sockaddr
,并将其传递给例如connect
,则的实现connect
可以struct sockaddr
安全地取消引用指针以检索sa_family
字段,了解它正在查看一个sockaddr_in
,将其返回,然后继续。 C 标准一直说取消引用指针会触发未定义的行为 - 这些规则自 C89 以来一直没有改变 - 但每个人都期望在这种情况下struct sockaddr
它是安全的,因为无论您实际使用哪种结构,它都是相同的“加载 16 位”指令。这就是为什么 POSIX 和 Windows 文档讨论对齐的原因;早在 1990 年代,编写这些规范的人就认为,这实际上可能造成麻烦的主要方式是您最终发出未对齐的内存访问。
但是标准文本没有提到任何有关加载指令或对齐的内容。它的内容如下(C99 §6.5p7 + 脚注):
对象存储的值只能通过具有下列类型之一的左值表达式访问:73)
与对象的有效类型兼容的类型,
与对象有效类型兼容的类型的合格版本,
与对象有效类型相对应的有符号或无符号类型,
与对象有效类型的限定版本相对应的有符号或无符号类型,
聚合类型或联合类型,其成员中包含上述类型之一(递归地包括子聚合或包含联合的成员),或
一种角色类型。
73)此列表的目的是指定对象可以使用或不使用别名的情况。
struct
类型仅与自身“兼容”,声明变量的“有效类型”是其声明的类型。所以你展示的代码...
struct sockaddr_storage addrStruct;
/* ... */
case AF_INET:
{
struct sockaddr_in * tmp = (struct sockaddr_in *)&addrStruct;
tmp->sin_family = AF_INET;
tmp->sin_port = htons(port);
inet_pton(AF_INET, addr, tmp->sin_addr);
}
break;
... 具有未定义的行为,并且编译器可以从中推断,即使简单的代码生成会按预期运行。现代编译器可能会从中推断出case AF_INET
永远无法执行。它会将整个块作为死代码删除,然后就会发生有趣的事情。
那么如何安全地使用sockaddr
?最简短的答案是“只需使用getaddrinfo
和getnameinfo
”。他们会为您解决这个问题。
但也许你需要使用地址族,例如AF_UNIX
,它getaddrinfo
无法处理。在大多数情况下,你可以只声明一个地址族正确类型的变量,并只在调用接受struct sockaddr *
int connect_to_unix_socket(const char *path, int type)
{
struct sockaddr_un sun;
size_t plen = strlen(path);
if (plen >= sizeof(sun.sun_path)) {
errno = ENAMETOOLONG;
return -1;
}
sun.sun_family = AF_UNIX;
memcpy(sun.sun_path, path, plen+1);
int sock = socket(AF_UNIX, type, 0);
if (sock == -1) return -1;
if (connect(sock, (struct sockaddr *)&sun,
offsetof(struct sockaddr_un, sun_path) + plen)) {
int save_errno = errno;
close(sock);
errno = save_errno;
return -1;
}
return sock;
}
实施时必须connect
经过一些麻烦才能确保安全,但这不是你的问题。
[2023 年 1 月编辑:这个答案以前所说的sockaddr_storage
是错误的,我很尴尬地承认我六年来没有注意到这个问题。] 在需要处理 IPv4 和 IPv6 地址的服务器中,使用struct sockaddr_storage
作为一种方便的方式来知道对 的调用的缓冲区有多大是很诱人的。但是,如果您对您关心的每个具体地址系列getpeername
使用 ,再加上普通的 ,它就不容易出错,并且严格别名问题也更少:union
`struct sockaddr`
#ifndef NI_IDN
#define NI_IDN 0
#endif
union sockaddr_ipvX {
struct sockaddr sa;
struct sockaddr_in sin;
struct sockaddr_in6 sin6;
};
char *get_peer_hostname(int sock)
{
union sockaddr_ipvX addrbuf;
socklen_t addrlen = sizeof addrbuf;
if (getpeername(sock, &addrbuf.sa, &addrlen))
return 0;
char *peer_hostname = malloc(MAX_HOSTNAME_LEN+1);
if (!peer_hostname) return 0;
if (getnameinfo(&addrbuf.sa, addrlen,
peer_hostname, MAX_HOSTNAME_LEN+1,
0, 0, NI_IDN) {
free(peer_hostname);
return 0;
}
return peer_hostname;
}
通过这种表述,您不仅不需要编写任何强制类型转换来调用getpeername
或getnameinfo
,还可以安全地访问addrbuf.sa.sa_family
,然后,当 时sa_family == AF_INET
,addrbuf.sin.sin_*
。
最后说明一下:如果 BSD 人员对 sockaddr 结构的定义有一点点不同...
struct sockaddr {
uint16_t sa_family;
};
struct sockaddr_in {
struct sockaddr sin_base;
uint16_t sin_port;
uint32_t sin_addr;
};
struct sockaddr_storage {
struct sockaddr ss_base;
char __ss_storage[128 - (sizeof(uint16_t) + sizeof(unsigned long))];
unsigned long int __ss_force_alignment;
};
... 由于“包含上述类型之一的聚合或联合”规则,向上转型和向下转型本来可以得到完美定义。如果您想知道如何在新的 C 代码中处理这个问题,请看这里。
解决方案 2:
是的,这样做会违反别名规定。所以不要这样做。没有必要使用;这是一个历史错误。但有几种安全sockaddr_storage
的使用方法:
malloc(sizeof(struct sockaddr_storage))
在这种情况下,指向的内存没有有效类型,直到你向其中存储某些内容。作为联合的一部分,明确访问您想要的成员。但在这种情况下,只需将
sockaddr
您想要的实际类型(in
和in6
和可能un
)放入联合中,而不是sockaddr_storage
。
struct sockaddr_*
当然,在现代编程中,你根本不需要创建 类型的对象。只需使用getaddrinfo
和getnameinfo
在字符串表示形式和对象之间转换地址sockaddr
,并将后者视为完全不透明的对象。
- 2024年20款好用的项目管理软件推荐,项目管理提效的20个工具和技巧
- 2024年开源项目管理软件有哪些?推荐5款好用的项目管理工具
- 项目管理软件有哪些?推荐7款超好用的项目管理工具
- 项目管理软件哪个最好用?盘点推荐5款好用的项目管理工具
- 项目管理软件有哪些最好用?推荐6款好用的项目管理工具
- 项目管理软件有哪些,盘点推荐国内外超好用的7款项目管理工具
- 2024项目管理软件排行榜(10类常用的项目管理工具全推荐)
- 项目管理软件排行榜:2024年项目经理必备5款开源项目管理软件汇总
- 2024年常用的项目管理软件有哪些?推荐这10款国内外好用的项目管理工具
- 项目管理必备:盘点2024年13款好用的项目管理软件