围绕 sockaddr_storage 和 sockaddr_in 进行转换是否会破坏严格别名

2024-10-28 08:37:00
admin
原创
56
摘要:问题描述:根据我之前的问题,我对这个代码非常好奇 -case AF_INET: { struct sockaddr_in * tmp = reinterpret_cast<struct sockaddr_in *> (&ad...

问题描述:

根据我之前的问题,我对这个代码非常好奇 -

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 上搜索过关于同一主题的内容,并得到了关于这个主题的不同回复。例如,请参阅这个、这个和这个帖子,它们说使用这种代码在某种程度上是安全的。另外还有另一篇帖子说使用 union 来完成这样的任务,但对已接受答案的评论再次持不同意见。


微软关于同一结构的文档称 -

应用程序开发人员通常仅使用 SOCKADDR_STORAGE 的 ss_family 成员。其余成员确保 SOCKADDR_STORAGE 可以包含 IPv6 或 IPv4 地址,并且结构经过适当填充以实现 64 位对齐。这种对齐使协议特定的套接字地址数据结构能够访问 SOCKADDR_STORAGE 结构中的字段而不会出现对齐问题。加上填充,SOCKADDR_STORAGE 结构的长度为 128 字节。

Opengroup 的文档指出 -

标头应定义 sockaddr_storage 结构。此结构应为:

足够大以容纳所有支持的协议特定地址结构

在适当的边界上对齐,以便指向它的指针可以转换为指向协议特定的地址结构的指针,并用于访问这些结构的字段,而不会出现对齐问题

套接字的手册页也说了同样的话 -

此外,套接字 API 还提供了数据类型 struct sockaddr_storage。此类型适合容纳所有受支持的域特定套接字地址结构;它足够大并且对齐正确。(特别是,它足够大以容纳 IPv6 套接字地址。)


C我已经在和语言中看到了使用此类强制转换的多种实现C++,现在我不确定哪一个是正确的,因为有些帖子与上述说法相矛盾——这个和这个。

那么,哪种方法才是安全且正确的填充sockaddr_storage结构的方法呢?这些指针强制转换安全吗?还是union 方法?我也知道这个getaddrinfo()调用,但对于上述仅填充结构的任务来说,这似乎有点复杂。还有另一种推荐的 memcpy 方法,这安全吗?


解决方案 1:

在过去十年中,C 和 C++ 编译器已经比sockaddr设计接口时甚至编写 C99 时复杂得多。其中,“未定义行为”的理解目的已经发生了变化。过去,未定义行为通常旨在掩盖硬件实现之间关于操作语义的分歧。但如今,由于许多组织希望不再编写 FORTRAN 并且有能力支付编译器工程师的费用来实现这一点,未定义行为已成为编译器用来推断代码的一种手段。左移就是一个很好的例子:C99 6.5.7p3,4(为了清晰起见略作重新排列)显示

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

例如,在 32 位宽的1u << 33平台上是 UB unsigned int。委员会将其设为未定义,因为不同 CPU 架构的左移指令在这种情况下会执行不同的操作:有些会一致地产生零,有些会以类型宽度为模数减少移位计数(x86),有些会以某个较大的数字为模数减少移位计数(ARM),并且至少有一种历史上常见的架构会陷入困境(我不知道是哪一种,但这就是它未定义而不是未指定的原因)。但是现在,如果你写

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

在 32 位平台上unsigned int,编译器知道上述 UB 规则,在调用函数时会推断出 的y值必须在 0 到 31 的范围内。它会将该范围输入到过程间分析中,并使用它来执行诸如删除调用者中不必要的范围检查之类的操作。如果程序员有理由认为它们不是不必要的,那么现在您开始明白为什么这个话题如此棘手了。(现代编译器可以优化x << (y&31)为 x86 等 ISA 的单个移位指令,其中移位指令实现该掩码。)

有关未定义行为目的的这种变化的更多信息,请参阅 LLVM 人员关于此主题的三部分文章(1 2 3)。


现在您明白了这一点,我实际上可以回答您的问题了。

struct sockaddr在省略了一些不相关的复杂因素后,这些是、struct sockaddr_in和的定义struct 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,则的实现connect可以struct sockaddr安全地取消引用指针以检索sa_family字段,了解它正在查看一个sockaddr_in,将其返回,然后继续。 C 标准一直说取消引用指针会触发未定义的行为 - 这些规则自 C89 以来一直没有改变 - 但每个人都期望在这种情况下struct sockaddr它是安全的,因为无论您实际使用哪种结构,它都是相同的“加载 16 位”指令。这就是为什么 POSIX 和 Windows 文档讨论对齐的原因;早在 1990 年代,编写这些规范的人就认为,这实际上可能造成麻烦的主要方式是您最终发出未对齐的内存访问。

但是标准文本没有提到任何有关加载指令或对齐的内容。它的内容如下(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经过一些麻烦才能确保安全,但这不是你的问题。

[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_INETaddrbuf.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 代码中处理这个问题,请看这里。

解决方案 2:

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

  1. malloc(sizeof(struct sockaddr_storage))在这种情况下,指向的内存没有有效类型,直到你向其中存储某些内容。

  2. 作为联合的一部分,明确访问您想要的成员。但在这种情况下,只需将sockaddr您想要的实际类型(inin6和可能un)放入联合中,而不是sockaddr_storage

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

相关推荐
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   601  
  华为IPD与传统研发模式的8大差异在快速变化的商业环境中,产品研发模式的选择直接决定了企业的市场响应速度和竞争力。华为作为全球领先的通信技术解决方案供应商,其成功在很大程度上得益于对产品研发模式的持续创新。华为引入并深度定制的集成产品开发(IPD)体系,相较于传统的研发模式,展现出了显著的差异和优势。本文将详细探讨华为...
IPD流程是谁发明的   7  
  如何通过IPD流程缩短产品上市时间?在快速变化的市场环境中,产品上市时间成为企业竞争力的关键因素之一。集成产品开发(IPD, Integrated Product Development)作为一种先进的产品研发管理方法,通过其结构化的流程设计和跨部门协作机制,显著缩短了产品上市时间,提高了市场响应速度。本文将深入探讨如...
华为IPD流程   9  
  在项目管理领域,IPD(Integrated Product Development,集成产品开发)流程图是连接创意、设计与市场成功的桥梁。它不仅是一个视觉工具,更是一种战略思维方式的体现,帮助团队高效协同,确保产品按时、按质、按量推向市场。尽管IPD流程图可能初看之下显得错综复杂,但只需掌握几个关键点,你便能轻松驾驭...
IPD开发流程管理   8  
  在项目管理领域,集成产品开发(IPD)流程被视为提升产品上市速度、增强团队协作与创新能力的重要工具。然而,尽管IPD流程拥有诸多优势,其实施过程中仍可能遭遇多种挑战,导致项目失败。本文旨在深入探讨八个常见的IPD流程失败原因,并提出相应的解决方法,以帮助项目管理者规避风险,确保项目成功。缺乏明确的项目目标与战略对齐IP...
IPD流程图   8  
热门文章
项目管理软件有哪些?
云禅道AD
禅道项目管理软件

云端的项目管理软件

尊享禅道项目软件收费版功能

无需维护,随时随地协同办公

内置subversion和git源码管理

每天备份,随时转为私有部署

免费试用