Abh*_*yal 14 c c++ sockets linux strict-aliasing
按照我之前的问题,我对这段代码非常好奇 -
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;
Run Code Online (Sandbox Code Playgroud)
在提出这个问题之前,我已经搜索了关于同一主题的SO,并对此主题进行了混合回答.例如,看到这个,这个和这个帖子说,使用这种代码在某种程度上是安全的.还有另一篇文章说使用工会来完成这样的任务,但是对接受的答案的评论再次提出不同意见.
微软关于相同结构的文档说 -
应用程序开发人员通常只使用SOCKADDR_STORAGE的ss_family成员.其余成员确保SOCKADDR_STORAGE可以包含IPv6或IPv4地址,并且适当填充结构以实现64位对齐.这种对齐使协议特定的套接字地址数据结构能够访问SOCKADDR_STORAGE结构中的字段而不会出现对齐问题.通过填充,SOCKADDR_STORAGE结构的长度为128个字节.
Opengroup的文件说明 -
标题应定义sockaddr_storage结构.该结构应为:
足够大以容纳所有支持的协议特定的地址结构
在适当的边界对齐,以便指向它的指针可以作为指向协议特定地址结构的指针,并用于访问这些结构的字段而没有对齐问题
socket的 man页面也说同样 -
此外,套接字API提供数据类型struct sockaddr_storage.此类型适用于容纳所有受支持的特定于域的套接字地址结构; 它足够大并且正确对齐.(特别是,它足以容纳IPv6套接字地址.)
我已经使用这两种类型转换看到多种实现C
与C++
语言在野外,现在我不确定的事实,哪一个是正确的,因为有一些帖子说上述的权利要求相矛盾- 这个和这个.
那么哪一个是填充sockaddr_storage
结构的安全和正确的方法?这些指针转换是否安全?还是工会方法?我也知道这个getaddrinfo()
电话,但对于刚刚填充结构的上述任务来说,这似乎有点复杂.memcpy还有另外一种推荐的方法,这样安全吗?
zwo*_*wol 20
在过去十年中,C和C++编译器变得比sockaddr
设计接口时更加复杂,甚至在编写C99时也是如此.作为其中的一部分,"未定义行为" 的理解目的已经改变.在当天,未定义的行为通常旨在涵盖硬件实现之间关于操作的语义是什么的不一致.但是现在,最终要感谢许多想要停止编写FORTRAN且能够支付编译工程师来实现这一目标的组织,未定义的行为是编译器用来推断代码的事情.左移是一个很好的例子:C99 6.5.7p3,4(为了清晰而重新排列)读取
结果
E1 << E2
是E1
左移位E2
位置; 腾出的位用零填充.如果[E2
]的值为负或大于或等于promote [E1
] 的宽度,则行为未定义.
因此,例如,1u << 33
UB位于unsigned int
32位宽的平台上.委员会对此进行了定义,因为不同的CPU体系结构的左移指令在这种情况下做了不同的事情:一些产生零一致,一些减少移位数模数的宽度(x86),一些减少移位数模数较大的数字(ARM),至少有一个历史上常见的架构会陷阱(我不知道哪一个,但这就是为什么它是未定义的而不是未指定的).但是现在,如果你写的话
unsigned int left_shift(unsigned int x, unsigned int y)
{ return x << y; }
Run Code Online (Sandbox Code Playgroud)
在具有32位的平台上unsigned int
,编译器知道上述UB规则,将推断y
在调用函数时必须具有0到32范围内的值.它会将该范围提供给过程间分析,并使用它来执行诸如在调用者中删除不必要的范围检查之类的操作.如果程序员有理由认为它们不是不必要的,那么现在你开始明白为什么这个主题就是这样一种蠕虫.
有关未定义行为目的的更改,请参阅LLVM人员关于该主题的三篇文章(1 2 3).
既然你明白了,我实际上可以回答你的问题.
这些的定义struct sockaddr
,struct sockaddr_in
以及struct sockaddr_storage
,eliding一些无关痛痒的并发症后:
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;
};
Run Code Online (Sandbox Code Playgroud)
这是穷人的子类.它是C中无处不在的习语.你定义了一组结构,它们都具有相同的初始字段,这是一个代码编号,告诉你实际上已经传递了哪个结构.回到那一天,每个人都期望如果你分配并填写了一个struct sockaddr_in
,并将其转发给struct sockaddr
,并将其传递给例如connect
,connect
可以struct sockaddr
安全地取消引用指针的实现以检索该sa_family
字段,了解它正在查看a sockaddr_in
,将其转回,继续.C标准总是说取消引用struct sockaddr
指针触发未定义的行为 - 这些规则自C89以来没有改变 - 但是每个人都认为在这种情况下它是安全的,因为无论你是哪种结构它都是相同的"加载16位"指令真的很合作.这就是POSIX和Windows文档谈论对齐的原因; 早在20世纪90年代,编写这些规范的人认为,实际上可能遇到麻烦的主要方式是,如果你最后发布一个错位的内存访问.
但是标准的文本没有说明加载指令,也没有对齐.这就是它所说的(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;
Run Code Online (Sandbox Code Playgroud)
...具有未定义的行为,编译器可以从中做出推论,即使天真的代码生成将按预期运行.现代编译器可能从中推断出,case AF_INET
永远不能执行.它将删除整个块作为死代码,并且随之而来的是欢闹.
那么你如何sockaddr
安全地工作?最简单的答案是"只是使用getaddrinfo
和" getnameinfo
.他们为你处理这个问题.
但也许您需要使用地址系列,例如AF_UNIX
,getaddrinfo
无法处理的地址系列.在大多数情况下,您只需为地址族声明一个正确类型的变量,并仅在调用带有a的函数时强制转换它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;
}
Run Code Online (Sandbox Code Playgroud)
该实施的connect
有通过一些跳铁圈,使这个安全的,但是这不是你的问题.
魂斗罗对方的回答,有是一个情况下,你可能想要使用sockaddr_storage
; 会同getpeername
并getnameinfo
在需要同时处理IPv4和IPv6地址的服务器.这是一种了解分配缓冲区大小的便捷方法.
#ifndef NI_IDN
#define NI_IDN 0
#endif
char *get_peer_hostname(int sock)
{
char addrbuf[sizeof(struct sockaddr_storage)];
socklen_t addrlen = sizeof addrbuf;
if (getpeername(sock, (struct sockaddr *)addrbuf, &addrlen))
return 0;
char *peer_hostname = malloc(MAX_HOSTNAME_LEN+1);
if (!peer_hostname) return 0;
if (getnameinfo((struct sockaddr *)addrbuf, addrlen,
peer_hostname, MAX_HOSTNAME_LEN+1,
0, 0, NI_IDN) {
free(peer_hostname);
return 0;
}
return peer_hostname;
}
Run Code Online (Sandbox Code Playgroud)
(我也可以写struct sockaddr_storage addrbuf
,但我想强调,我从来没有真正需要addrbuf
直接访问内容.)
最后要注意的:如果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;
};
Run Code Online (Sandbox Code Playgroud)
由于"包含上述类型之一的聚合或联合"规则,...向上和向下倾斜将完全明确定义.如果您想知道如何在新的C代码中处理这个问题,那么就去吧.
是的,执行此操作违反了别名.所以不要.有没有必要不断使用sockaddr_storage
; 这是一个历史错误.但是有一些安全的方法可以使用它:
malloc(sizeof(struct sockaddr_storage))
.在这种情况下,指向内存在您存储内容之前没有有效类型.sockaddr
,你想要的(类型in
和in6
也许un
)的工会,而不是sockaddr_storage
.在现代编程当然,你不应该需要创建一个类型的对象struct sockaddr_*
在所有.只需使用getaddrinfo
和getnameinfo
转换字符串表示和sockaddr
对象之间的地址,并将后者视为完全不透明的对象.
归档时间: |
|
查看次数: |
3178 次 |
最近记录: |