围绕sockaddr_storage和sockaddr_in的强制转换是否会破坏严格别名

toe95027  于 2023-01-20  发布在  其他
关注(0)|答案(2)|浏览(231)

在我之前的question之后,我对这段代码非常好奇-

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,并得到了关于这个主题的混合响应。例如,请参见thisthisthis帖子,它们说使用这种代码在某种程度上是安全的。还有另一个post帖子说使用联合来完成这样的任务,但对接受的答案的评论再次提出不同意见。
微软的documentation在相同的结构上说-
应用程序开发人员通常只使用SOCKADDR_STORAGE的ss_family成员。其余成员确保SOCKADDR_STORAGE可以包含IPv6或IPv4地址,并且该结构被适当地填充以实现64位对齐。这种对齐使特定于协议的套接字地址数据结构能够访问SOCKADDR_STORAGE结构中的字段,而不会出现对齐问题。通过填充,SOCKADDR_STORAGE结构的长度为128字节。
OpenGroup的documentation状态-
标头应定义sockaddr_storage结构。该结构应为:
足够大,可容纳所有受支持的协议特定地址结构
在适当的边界对齐,以便指向它的指针可以转换为指向协议特定的地址结构的指针,并用于访问这些结构的字段,而不会出现对齐问题
socket的手册页也是这么说的-
此外,套接字API提供了数据类型struct sockaddr_storage,这种类型适合于容纳所有支持的域特定套接字地址结构;它足够大并且正确对齐。(特别是,它足够大以容纳IPv6套接字地址。)
我在CC++中看到过多种使用这种类型转换的实现,现在我不确定哪一种是正确的,因为有一些帖子与上面的声明相矛盾-thisthis
那么哪一个是安全和正确的填充sockaddr_storage结构的方法呢?这些指针类型转换安全吗?还是union method?我也知道getaddrinfo()调用,但是对于上面的填充结构的任务来说似乎有点复杂。还有另外一个recommended way with memcpy,这个安全吗?

vfhzx4xs

vfhzx4xs1#

在过去的十年里,C和C++编译器已经变得比sockaddr接口设计时,甚至比C99编写时复杂得多。作为其中的一部分,“未定义行为”的理解 * 目的 * 已经改变。回到过去,未定义的行为通常是为了掩盖硬件实现中关于操作语义的分歧。2但是现在,多亏了一些组织,他们不想再写FORTRAN,并且有能力支付编译器工程师的费用来实现这一目标,未定义行为是编译器用来 * 对代码进行推断 * 的一个东西。左移就是一个很好的例子:C99 6.5.7p3,4(为清晰起见,稍微重新排列)读作
E1 << E2的结果是E1左移E2比特位置;空出的位用零填充。如果[E2]的值为负值或大于或等于提升的[E1]的宽度,则行为未定义。
例如,1u << 33unsigned int为32位宽的平台上是UB,委员会之所以定义它是因为不同CPU架构的左移指令在这种情况下做不同的事情:有些始终产生零,有些减少了以类型宽度为模的移位数(x86),有些减少了以某个更大的数为模的移位数(ARM),并且至少有一个历史上常见的架构会陷阱(我不知道是哪一个,但这就是为什么它是未定义的,而不是未指定的)。

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

在32位unsigned int平台上,编译器知道上面的UB规则,当函数被调用时,它会推断y必须有一个0到31范围内的值。它会将这个范围提供给过程间分析,并使用它来做一些事情,比如删除调用者中不必要的范围检查。如果程序员有理由认为它们不是不必要的,那么,现在你开始明白为什么这个主题是如此的蠕虫了。2(现代编译器可以将x << (y&31)优化成一个移位指令,用于像x86这样的ISA,其中移位指令实现了屏蔽。3)
有关未定义行为的目的的变化的更多信息,请参见LLVM人员关于该主题的三部分文章(123)。
现在你明白了,我可以回答你的问题了。
以下是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,并将其传递给connect,X1 M17 N1 X的实现可以安全地解引用X1 M18 N1 X指针以检索X1 M19 N1 X字段,获知其正在查看X1 M20 N1 X,将其强制转换回,C标准总是说,解引用struct sockaddr指针会触发未定义的行为--这些规则自C89以来一直没有改变--但是每个人都期望它是安全的 * 在这种情况下 *,因为无论你实际上使用的是哪种结构,它都是相同的“加载16位”指令。早在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;

...有未定义的行为,编译器可以从中做出推断,* 即使 * 幼稚的代码生成会如预期的那样运行。现代编译器可能从中推断出case AF_INET永远不会被执行。它会删除整个代码块作为死代码,然后搞笑事件就会接踵而至。

那么如何安全地使用sockaddr呢?最简短的回答是“只需使用getaddrinfogetnameinfo”,他们为您处理这个问题。
但是也许你需要处理一个地址族,比如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的 * 实现 * 必须跳过一些限制才能使其安全,但这不是你的问题。
[EDIT 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;
}

有了这个公式,您不仅不需要编写任何类型转换来调用getpeernamegetnameinfo,而且 * 可以 * 安全地访问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代码中应该如何处理这个问题,这里可以找到。

uqxowvwt

uqxowvwt2#

是的,这样做违反了别名规则,所以不要这样做,没有必要 * 永远 * 使用sockaddr_storage;这是个历史性的错误。但有几种安全的方法可以使用它:

  1. malloc(sizeof(struct sockaddr_storage))。在这种情况下,直到您向指向的内存中存储了一些内容,它才具有有效类型。
    1.作为联合体的一部分,显式地访问你想要的成员,但是在这种情况下,只需要把你想要的实际sockaddr类型(inin6,也许还有un)放在联合体中,而不是sockaddr_storage
    当然,在现代编程中,你根本不需要创建struct sockaddr_*类型的对象,只需要使用getaddrinfogetnameinfo在字符串表示和sockaddr对象之间转换地址,并将后者视为完全不透明的对象

相关问题