将在 sockaddr_storage 和 sockaddr_in 周围投射打破严格的别名

will casting around sockaddr_storage and sockaddr_in break 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;

在问这个问题之前,我已经在 SO 中搜索了关于同一主题的内容,并且得到了关于该主题的混合响应。例如,请参阅 this, this and this post which say that it is somehow safe to use this kind of code. Also there's another post 说要为此类任务使用联合,但对已接受答案的评论又有所不同。


Microsoft 在相同结构上的 documentation 表示 -

Application developers normally use only the ss_family member of the SOCKADDR_STORAGE. The remaining members ensure that the SOCKADDR_STORAGE can contain either an IPv6 or IPv4 address and the structure is padded appropriately to achieve 64-bit alignment. Such alignment enables protocol-specific socket address data structures to access fields within a SOCKADDR_STORAGE structure without alignment problems. With its padding, the SOCKADDR_STORAGE structure is 128 bytes in length.

Opengroup 的 documentation 状态 -

The header shall define the sockaddr_storage structure. This structure shall be:

Large enough to accommodate all supported protocol-specific address structures

Aligned at an appropriate boundary so that pointers to it can be cast as pointers to protocol-specific address structures and used to access the fields of those structures without alignment problems

socket 的手册页也说相同 -

In addition, the sockets API provides the data type struct sockaddr_storage. This type is suitable to accommodate all supported domain-specific socket address structures; it is large enough and is aligned properly. (In particular, it is large enough to hold IPv6 socket addresses.)


我已经在野外看到 CC++ 语言中使用此类转换的多个实现,现在我不确定哪一个是正确的,因为有一些帖子与上述说法相矛盾 - this and .

那么填充 sockaddr_storage 结构的安全正确方法是什么?这些指针转换安全吗?或者 union method? I'm also aware of the getaddrinfo() call but that seems a little complicated for the above task of just filling the structs. There is one other ,这样安全吗?

是的,这样做是违反别名的。所以不要。没有必要曾经使用sockaddr_storage;这是一个历史错误。但是有一些安全的使用方法:

  1. malloc(sizeof(struct sockaddr_storage))。在这种情况下,pointed-to 内存在您存储内容之前没有有效类型。
  2. 作为联合的一部分,明确访问您想要的成员。但是在这种情况下,只需将您想要的实际 sockaddr 类型(inin6 以及 un)放入联合中,而不是 sockaddr_storage.

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

C 和 C++ 编译器在过去十年中变得比设计 sockaddr 接口时甚至编写 C99 时都复杂得多。作为其中的一部分,"undefined behavior" 的理解 目的 已经改变。过去,未定义的行为通常旨在掩盖 硬件 实现之间关于操作语义的分歧。但如今,最终要感谢许多组织不想再编写 FORTRAN 并且有能力支付编译器工程师来实现这一目标,未定义的行为是编译器用来对代码进行推断的东西。左移是一个很好的例子:C99 6.5.7p3,4(为清楚起见稍微重新排列)读取

The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If the value of [E2] is negative or is greater than or equal to the width of the promoted [E1], the behavior is undefined.

因此,例如,1u << 33 是平台上的 UB,其中 unsigned int 是 32 位宽。委员会未定义这一点,因为不同的 CPU 架构的 left-shift 指令在这种情况下会做不同的事情:一些始终产生零,一些减少移位计数模宽度(x86),一些减少shift count modulo 一些更大的数字 (ARM),并且至少有一个 historically-common 架构会陷入困境(我不知道是哪一个,但这就是为什么它未定义而不是未指定的原因)。但是现在,如果你写

unsigned int left_shift(unsigned int x, unsigned int y)
{ return x << y; }

在 32 位 unsigned int 平台上,知道上述 UB 规则的编译器将 推断 y 必须具有 0 到 32 范围内的值 调用函数时。它将将该范围提供给过程间分析,并使用它来做一些事情,比如删除调用者中不必要的范围检查。如果程序员有理由认为它们 不是 不必要的,好吧,现在你开始明白为什么这个话题是一堆蠕虫了。

有关未定义行为目的的此更改的更多信息,请参阅 LLVM 人员的 three-part 关于该主题的文章 (1 2 3)。


既然你明白了,我其实可以回答你的问题了。

这些是 struct sockaddrstruct sockaddr_instruct 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,然后将其传递给例如connectconnect 的实现可以安全地取消引用 struct sockaddr 指针以检索 sa_family 字段,得知它正在查看 sockaddr_in,将其投回,然后继续。 C 标准一直说取消引用 struct sockaddr 指针会触发未定义的行为——这些规则自 C89 以来没有改变——但每个人都认为在这种情况下 是安全的,因为它会无论您实际使用哪种结构,都应使用相同的 "load 16 bits" 指令。这就是 POSIX 和 Windows 文档谈论对齐的原因;早在 1990 年代,编写这些规范的人就认为,这可能 实际上 的主要方式是如果您最终发出未对齐的内存访问。

但是标准的文本没有说明加载指令,也没有说明对齐。这就是它所说的 (C99 §6.5p7 + 脚注):

An object shall have its stored value accessed only by an lvalue expression that has one of the following types:73)

  • a type compatible with the effective type of the object,
  • a qualified version of a type compatible with the effective type of the object,
  • a type that is the signed or unsigned type corresponding to the effective type of the object,
  • a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
  • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
  • a character type.

73) The intent of this list is to specify those circumstances in which an object may or may not be aliased.

struct 类型仅与它们自身 "compatible" 相关,声明变量的 "effective type" 是其声明类型。所以你展示的代码...

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 呢?最短的答案是 "just use getaddrinfo and getnameinfo." 他们为您处理这个问题。

但也许您需要使用地址族,例如 AF_UNIXgetaddrinfo 无法处理。在大多数情况下,您只需为地址族声明一个正确类型的变量,并在调用采用 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 实现 必须克服一些困难才能确保安全,但这不是您的问题。

与另一个答案相反,一种情况,您可能想要使用sockaddr_storage;在需要处理 IPv4 和 IPv6 地址的服务器中与 getpeernamegetnameinfo 结合使用。这是了解要分配多大缓冲区的便捷方法。

#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;
}

(我也可以写 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;
};

... 由于 "aggregate or union that includes one of the aforementioned types" 规则,向上转型和向下转型将是完美的 well-defined。 如果您想知道如何在新的 C 代码中处理这个问题,请看这里。