将围绕sockaddr_storage和sockaddr_in强制转换为严格别名

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套接字地址.)


我已经使用这两种类型转换看到多种实现CC++语言在野外,现在我不确定的事实,哪一个是正确的,因为有一些帖子说上述的权利要求相矛盾- 这个这个.

那么哪一个是填充sockaddr_storage结构的安全和正确的方法?这些指针转换是否安全?还是工会方法?我也知道这个getaddrinfo()电话,但对于刚刚填充结构的上述任务来说,这似乎有点复杂.memcpy还有另外一种推荐的方法,这样安全吗?

zwo*_*wol 20

在过去十年中,C和C++编译器变得比sockaddr设计接口时更加复杂,甚至在编写C99时也是如此.作为其中的一部分,"未定义行为" 的理解目的已经改变.在当天,未定义的行为通常旨在涵盖硬件实现之间关于操作的语义是什么的不一致.但是现在,最终要感谢许多想要停止编写FORTRAN且能够支付编译工程师来实现这一目标的组织,未定义的行为是编译器用来推断代码的事情.左移是一个很好的例子:C99 6.5.7p3,4(为了清晰而重新排列)读取

结果E1 << E2E1左移位E2位置; 腾出的位用零填充.如果[ E2]的值为负或大于或等于promote [ E1] 的宽度,则行为未定义.

因此,例如,1u << 33UB位于unsigned int32位宽的平台上.委员会对此进行了定义,因为不同的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; 会同getpeernamegetnameinfo在需要同时处理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代码中处理这个问题,那么就去吧.

  • 请注意,虽然20世纪90年代的人们可能没有提出过这样的说法:`struct sockaddr z =*p;`如果p是指向sockaddr以外的东西的指针,那将是UB,我认为人们不会说使用`p-> sa_family`来访问公共初始序列的那一部分是一样的.相反,通过结构指针访问CIS成员的能力是使CIS*有用*的重要组成部分.代码应该为此目的使用"memcpy"的概念已经被广泛认为是愚蠢的,特别是因为... (2认同)
  • @supercat正如我之前所说的那样,我不想在这类答案的评论中与你讨论"事情应该如何",这些答案是关于"事情如何以及如何处理".我对捍卫或批评编译器开发人员的行为特别不感兴趣,编译器开发人员是一群不再包括我的人. (2认同)

R..*_*R.. 5

是的,执行此操作违反了别名.所以不要.有没有必要不断使用sockaddr_storage; 这是一个历史错误.但是有一些安全的方法可以使用它:

  1. malloc(sizeof(struct sockaddr_storage)).在这种情况下,指向内存在您存储内容之前没有有效类型.
  2. 作为联盟的一部分,明确访问您想要的成员.但在这种情况下,只是把实际的sockaddr,你想要的(类型inin6也许un)的工会,而不是sockaddr_storage.

在现代编程当然,你不应该需要创建一个类型的对象struct sockaddr_* 在所有.只需使用getaddrinfogetnameinfo转换字符串表示和sockaddr对象之间的地址,并将后者视为完全不透明的对象.

  • 那么,在现代编程中,你根本不应该在没有`-fno-strict-aliasing`的情况下进行编译.:) (2认同)
  • ......与"-fno-strict-aliasing"的成本相比,这样做的成本微不足道.此外,我建议即使你没有找到任何有用的别名结构,广泛使用一些别名模式来做其他事情无法做到的事情构成*初步证据表明它们是有用的 - 只是不是你. (2认同)
  • @pmor:clang 和 gcc 都使用与标准规则不兼容的抽象模型,该规则允许回收存储以保存不同类型。缺陷报告 236 被提议更改标准,以便任何曾经保存任何特定类型的存储都不能用于保存任何其他类型,但该请求被拒绝。尽管如此,clang 和 gcc 继续使用抽象模型,其中已写入类型 T 且后来写入类型 U 的有效存储类型稍后可能会自发恢复为 T,即使它从未被读取为除上次写入之外的任何类型。 (2认同)
  • 此外,gcc 有时会处理类似 `if (flag) *(long)ptr = 1; 的结构。else *(long long)ptr=1;` 等同于 `*(long long)ptr=1;` 并假设即使 `flag` 为 1,它们也永远不会访问 `long`。因此,指定任何优化而不使用`-fno-strict-aliasing` 将产生一种语言方言,它应该被视为只适合与从不将存储重新用作不同类型的程序一起使用,并且我会信任指定它只能使用 ` -fno-strict-aliasing` 远比我更信任未指定这一点的代码。 (2认同)