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

2024-10-28 08:37:00
admin
原创
241
摘要:问题描述:根据我之前的问题,我对这个代码非常好奇 -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,并将后者视为完全不透明的对象

相关推荐
  政府信创国产化的10大政策解读一、信创国产化的背景与意义信创国产化,即信息技术应用创新国产化,是当前中国信息技术领域的一个重要发展方向。其核心在于通过自主研发和创新,实现信息技术应用的自主可控,减少对外部技术的依赖,并规避潜在的技术制裁和风险。随着全球信息技术竞争的加剧,以及某些国家对中国在科技领域的打压,信创国产化显...
工程项目管理   2634  
  为什么项目管理通常仍然耗时且低效?您是否还在反复更新电子表格、淹没在便利贴中并参加每周更新会议?这确实是耗费时间和精力。借助软件工具的帮助,您可以一目了然地全面了解您的项目。如今,国内外有足够多优秀的项目管理软件可以帮助您掌控每个项目。什么是项目管理软件?项目管理软件是广泛行业用于项目规划、资源分配和调度的软件。它使项...
项目管理软件   1585  
  IPD(Integrated Product Development)流程作为一种先进的产品开发管理模式,在众多企业中得到了广泛应用。其中,技术评审与决策评审是IPD流程中至关重要的环节,它们既有明显的区别,又存在紧密的协同关系。深入理解这两者的区别与协同,对于企业有效实施IPD流程,提升产品开发效率与质量具有重要意义...
IPD管理流程   50  
  本文介绍了以下10款项目管理软件工具:禅道项目管理软件、ClickUp、Freshdesk、GanttPRO、Planview、Smartsheet、Asana、Nifty、HubPlanner、Teamwork。在当今快速变化的商业环境中,项目管理软件已成为企业提升效率、优化资源分配和确保项目按时交付的关键工具。然而...
项目管理系统   49  
  建设工程项目质量关乎社会公众的生命财产安全,也影响着企业的声誉和可持续发展。高质量的建设工程不仅能为使用者提供舒适、安全的环境,还能提升城市形象,推动经济的健康发展。在实际的项目操作中,诸多因素会对工程质量产生影响,从规划设计到施工建设,再到后期的验收维护,每一个环节都至关重要。因此,探寻并运用有效的方法来提升建设工程...
工程项目管理制度   44  
热门文章
项目管理软件有哪些?
曾咪二维码

扫码咨询,免费领取项目管理大礼包!

云禅道AD
禅道项目管理软件

云端的项目管理软件

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

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

内置subversion和git源码管理

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

免费试用