简体   繁体   English

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

[英]will casting around sockaddr_storage and sockaddr_in break strict aliasing

Following my previous question , I'm really curious about this code - 按照我之前的问题 ,我对这段代码非常好奇 -

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;

Before asking this question, I've searched across SO about same topic and have got mix responses about this topic. 在提出这个问题之前,我已经搜索了关于同一主题的SO,并对此主题进行了混合回答。 For example, see this , this and this post which say that it is somehow safe to use this kind of code. 例如,看到这个这个这个帖子说,使用这种代码在某种程度上是安全的。 Also there's another post that says to use unions for such task but again the comments on accepted answer beg to differ. 还有另一篇文章说使用工会来完成这样的任务,但是对接受的答案的评论再次提出不同意见。


Microsoft's documentation on same structure says - 微软关于相同结构的文档说 -

Application developers normally use only the ss_family member of the SOCKADDR_STORAGE. 应用程序开发人员通常只使用SOCKADDR_STORAGE的ss_family成员。 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. 其余成员确保SOCKADDR_STORAGE可以包含IPv6或IPv4地址,并且适当填充结构以实现64位对齐。 Such alignment enables protocol-specific socket address data structures to access fields within a SOCKADDR_STORAGE structure without alignment problems. 这种对齐使协议特定的套接字地址数据结构能够访问SOCKADDR_STORAGE结构中的字段而不会出现对齐问题。 With its padding, the SOCKADDR_STORAGE structure is 128 bytes in length. 通过填充,SOCKADDR_STORAGE结构的长度为128个字节。

Opengroup's documentation states - Opengroup的文件说明 -

The header shall define the sockaddr_storage structure. 标题应定义sockaddr_storage结构。 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 在适当的边界对齐,以便指向它的指针可以作为指向协议特定地址结构的指针,并用于访问这些结构的字段而没有对齐问题

Man page of socket also says same - socket的 man页面也说同样 -

In addition, the sockets API provides the data type struct sockaddr_storage. 此外,套接字API提供数据类型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.) (特别是,它足以容纳IPv6套接字地址。)


I've seen multiple implementation using such casts in both C and C++ languages in the wild and now I'm uncertain of the fact which one is right since there are some posts that contradict with above claims - this and this . 我已经看到了在CC++语言中使用这种演员的多种实现,现在我不确定哪一个是正确的,因为有些帖子与上述声明相矛盾 - 这就是 这个

So which one is the safe and right way to fill up a sockaddr_storage structure? 那么哪一个是填充sockaddr_storage结构的安全正确的方法? Are these pointer casts safe? 这些指针转换是否安全? or the 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. 我也知道getaddrinfo()调用,但对于刚刚填充结构的上述任务来说,这似乎有点复杂。 There is one other recommended way with memcpy , is this safe? memcpy还有另外一种推荐的方法 ,这样安全吗?

C and C++ compilers have become much more sophisticated in the past decade than they were when the sockaddr interfaces were designed, or even when C99 was written. 在过去十年中,C和C ++编译器已经变得比设计sockaddr界面时更加复杂,甚至在编写C99时也是如此。 As part of that, the understood purpose of "undefined behavior" has changed. 作为其中的一部分,“未定义行为”的理解目的已经改变。 Back in the day, undefined behavior was usually intended to cover disagreement among hardware implementations as to what the semantics of an operation was. 在当天,未定义的行为通常旨在涵盖硬件实现之间关于操作的语义是什么的不一致。 But nowadays, thanks ultimately to a number of organizations who wanted to stop having to write FORTRAN and could afford to pay compiler engineers to make that happen, undefined behavior is a thing that compilers use to make inferences about the code . 但是现在,最终要感谢许多想要停止编写FORTRAN且能够支付编译工程师来实现这一目标的组织,未定义的行为是编译器用来推断代码的事情。 Left shift is a good example: C99 6.5.7p3,4 (rearranged a little for clarity) reads 左移是一个很好的例子:C99 6.5.7p3,4(为了清晰而重新排列)读取

The result of E1 << E2 is E1 left-shifted E2 bit positions; E1 << E2的结果是E1左移E2位位置; 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. 如果[ E2 ]的值为负或大于或等于提升的[ E1 ]的宽度,则行为未定义。

So, for instance, 1u << 33 is UB on a platform where unsigned int is 32 bits wide. 因此,例如, 1u << 33unsigned int为32位宽的平台上的UB。 The committee made this undefined because different CPU architectures' left-shift instructions do different things in this case: some produce zero consistently, some reduce the shift count modulo the width of the type (x86), some reduce the shift count modulo some larger number (ARM), and at least one historically-common architecture would trap (I don't know which one, but that's why it's undefined and not unspecified). 委员会对此进行了定义,因为不同的CPU体系结构的左移指令在这种情况下做了不同的事情:一些产生零一致,一些减少移位计数模数的宽度(x86),一些减少移位数模数一些更大的数字(ARM),至少有一个历史上常见的架构会陷阱(我不知道哪一个,但这就是为什么它是未定义的而不是未指定的)。 But nowadays, if you write 但是现在,如果你写的话

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

on a platform with 32-bit unsigned int , the compiler, knowing the above UB rule, will infer that y must have a value in the range 0 through 32 when the function is called. 在具有32位unsigned int的平台上,编译器知道上述UB规则,将推断在调用函数时y必须具有0到32范围内的值 It will feed that range into interprocedural analysis, and use it to do things like remove unnecessary range checks in the callers. 它会将该范围提供给过程间分析,并使用它来执行诸如在调用者中删除不必要的范围检查之类的操作。 If the programmer has reason to think they aren't unnecessary, well, now you begin to see why this topic is such a can of worms. 如果程序员有理由认为它们不是不必要的,那么现在你就开始明白为什么这个主题就是这样一种蠕虫。

For more on this change in the purpose of undefined behavior, please see the LLVM people's three-part essay on the subject ( 1 2 3 ). 有关未定义行为目的的更改,请参阅LLVM人员关于该主题的三篇文章( 1 2 3 )。


Now that you understand that, I can actually answer your question. 既然你明白了,我实际上可以回答你的问题。

These are the definitions of struct sockaddr , struct sockaddr_in , and struct sockaddr_storage , after eliding some irrelevant complications: 在消除了一些不相关的复杂问题之后,这些是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;
};

This is poor man's subclassing. 这是穷人的子类。 It is a ubiquitous idiom in C. You define a set of structures that all have the same initial field, which is a code number that tells you which structure you've actually been passed. 它是C中无处不在的习语。你定义了一组结构,它们都具有相同的初始字段,这是一个代码编号,告诉你实际上已经传递了哪个结构。 Back in the day, everyone expected that if you allocated and filled in a struct sockaddr_in , upcast it to struct sockaddr , and passed it to eg connect , the implementation of connect could dereference the struct sockaddr pointer safely to retrieve the sa_family field, learn that it was looking at a sockaddr_in , cast it back, and proceed. 早在一天,大家都预计,如果您分配并装入struct sockaddr_in ,上溯造型它struct sockaddr ,并把它递给如connect ,执行connect可提领的struct sockaddr指针安全地检索sa_family领域,得知它正在看着一个sockaddr_in ,把它sockaddr_in ,然后继续。 The C standard has always said that dereferencing the struct sockaddr pointer triggers undefined behavior—those rules are unchanged since C89—but everyone expected that it would be safe in this case because it would be the same "load 16 bits" instruction no matter which structure you were really working with. C标准总是说取消引用struct sockaddr指针触发未定义的行为 - 这些规则自C89以来没有改变 - 但是每个人都认为在这种情况下它是安全的因为无论哪种结构它都是相同的“加载16位”指令你真的和我一起工作。 That's why POSIX and the Windows documentation talk about alignment; 这就是POSIX和Windows文档谈论对齐的原因; the people who wrote those specs, back in the 1990s, thought that the primary way this could actually be trouble was if you wound up issuing a misaligned memory access. 早在20世纪90年代,编写这些规范的人认为, 实际上可能遇到麻烦的主要方式是,如果你最后发布一个错位的内存访问。

But the text of the standard doesn't say anything about load instructions, nor alignment. 但是标准的文本没有说明加载指令,也没有对齐。 This is what it says (C99 §6.5p7 + footnote): 这就是它所说的(C99§6.5p7+脚注):

An object shall have its stored value accessed only by an lvalue expression that has one of the following types: 73) 对象的存储值只能由具有以下类型之一的左值表达式访问: 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. 73)此列表的目的是指定对象可能或可能不具有别名的情况。

struct types are "compatible" only with themselves, and the "effective type" of a declared variable is its declared type. struct类型只与自身“兼容”,声明变量的“有效类型”是其声明的类型。 So the code you showed... 所以你展示的代码......

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;

... has undefined behavior, and compilers can make inferences from that, even though naive code generation would behave as expected. ...具有未定义的行为,编译器可以从中做出推论, 即使天真的代码生成将按预期运行。 What a modern compiler is likely to infer from this is that the case AF_INET can never be executed . 现代编译器可能从中推断出case AF_INET 永远不会被执行 It will delete the entire block as dead code, and hilarity will ensue. 它将删除整个块作为死代码,并且随之而来的是欢闹。


So how do you work with sockaddr safely? 那么你如何安全地使用sockaddr The shortest answer is "just use getaddrinfo and getnameinfo ." 最简单的答案是“只使用getaddrinfogetnameinfo They deal with this problem for you. 他们为你处理这个问题。

But maybe you need to work with an address family, such as AF_UNIX , that getaddrinfo doesn't handle. 但也许您需要使用getaddrinfo无法处理的地址系列,例如AF_UNIX In most cases you can just declare a variable of the correct type for the address family, and cast it only when calling functions that take a struct sockaddr * 在大多数情况下,您只需为地址族声明一个正确类型的变量,并在调用带有struct sockaddr *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;
}

The implementation of connect has to jump through some hoops to make this safe, but that is Not Your Problem. connect实现必须跳过一些箍以使其安全,但这不是你的问题。

Contra the other answer, there is one case where you might want to use sockaddr_storage ; 魂斗罗对方的回答, 一个情况下,你可能想使用sockaddr_storage ; in conjunction with getpeername and getnameinfo , in a server that needs to handle both IPv4 and IPv6 addresses. getpeernamegetnameinfo一起,在需要同时处理IPv4和IPv6地址的服务器中。 It is a convenient way to know how big of a buffer to allocate. 这是一种了解分配缓冲区大小的便捷方法。

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

(I could just as well have written struct sockaddr_storage addrbuf , but I wanted to emphasize that I never actually need to access the contents of addrbuf directly.) (我也可以编写struct sockaddr_storage addrbuf ,但我想强调一下,我实际上并不需要直接访问addrbuf的内容。)

A final note: if the BSD folks had defined the sockaddr structures just a little bit differently ... 最后要注意的:如果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;
};

... upcasts and downcasts would have been perfectly well-defined, thanks to the "aggregate or union that includes one of the aforementioned types" rule. 由于“包含上述类型之一的聚合或联合”规则,...向上和向下倾斜将完全明确定义。 If you're wondering how you should deal with this problem in new C code, here you go. 如果您想知道如何在新的C代码中处理这个问题,那么就去吧。

Yes, it's an aliasing violation to do this. 是的,执行此操作违反了别名。 So don't. 所以不要。 There's no need to ever use sockaddr_storage ; 有没有必要曾经使用sockaddr_storage ; it was a historical mistake. 这是一个历史错误。 But there are a few safe ways to use it: 但是有一些安全的方法可以使用它:

  1. malloc(sizeof(struct sockaddr_storage)) . malloc(sizeof(struct sockaddr_storage)) In this case, the pointed-to memory does not have an effective type until you store something to it. 在这种情况下,指向内存在您存储内容之前没有有效类型。
  2. As part of a union, explicitly accessing the member you want. 作为联盟的一部分,明确访问您想要的成员。 But in this case just put the actual sockaddr types you want ( in and in6 and maybe un ) in the union rather than sockaddr_storage . 但在这种情况下,只需将你想要的实际sockaddr类型( inin6以及un )放入union中,而不是sockaddr_storage

Of course in modern programming you should never need to create objects of type struct sockaddr_* at all . 在现代编程当然,你不应该需要创建一个类型的对象struct sockaddr_* 在所有 Simply use getaddrinfo and getnameinfo to translate addresses between string representations and sockaddr objects, and treat the latter as completely opaque objects . 只需使用getaddrinfogetnameinfo在字符串表示和sockaddr对象之间转换地址,并将后者视为完全不透明的对象

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM