使用 sockaddr 安全 reinterpret_cast?

Safe reinterpret_cast with sockaddr?

假设我正在做一些套接字编程:

struct sockaddr_in sa;
inet_pton(AF_INET, "127.0.0.1", &(sa.sin_addr));
auto *resa = reinterpret_cast<struct sockaddr*>(&sa);
bind(sfd, resa, sizeof(sa));

现在的问题是:我们执行 reinterpret_cast(或像教程或 man 中的 C 风格 (struct sockaddr *) 强制转换),但标准并不能保证它有效,对吧?另一方面,似乎没有办法以不同的方式做到这一点,bind() 需要 struct sockaddr*(并且它必须访问底层结构以确定它接收到的内容)。

那么在这种情况下,在不同类型之间进行 reinterpret_cast 安全吗?如果是那么为什么?

考虑到在示例代码中可以看到转换为 struct sockaddr* 并且您将其传递给 API 函数(对于一个有据可查且经过良好测试的库),它只是如果违反函数的先决条件是不安全的。

转换是必要的,因为有不同类型的 sockaddr(如 sockaddr_insockaddr_un)并且只有一种类型的 bind 函数。

无论如何,C++ 编译器在执行 C 风格转换时都会选择 reinterpret_cast,但为了可读性,最好更明确。

是的,这是绝对安全和正确的。

Winsock functions using sockaddr are not strictly interpreted to be pointers to a sockaddr structure. The structure is interpreted differently in the context of different address families. The only requirements are that the first u_short is the address family and the total size of the memory buffer in bytes is namelen.

所以你真的需要将 pointer 传递到从 u_short sin_family; 开始的某个位置。 sockaddr_in 符合这个条件。 reinterpret_cast 不更改指向 sockaddr_in sa; 的指针并且不生成任何二进制代码:reinterpret_cast<sockaddr*>(&sa);(sockaddr*)&sa&sa 相同。换句话说二进制 pointer &sa == reinterpret_cast<sockaddr*>(&sa)

所以你可以而且必须使用

 bind(sfd, reinterpret_cast<sockaddr*>(&sa), sizeof(sa));

在这里使用auto *resa毫无意义,为什么?

或者我们可以使用下一个代码:

union {
    sockaddr sa;
    sockaddr_in sa_in;
};

sa_in.sin_family = AF_INET;
sa_in.sin_port = *;
sa_in.sin_addr.S_un.S_addr = *;
bind(0, &sa, sizeof(sa_in));

和 main - 尝试将自己置于编写 bind 函数的位置。它必须如何与 sockaddr 一起使用?显然它首先查看它 sa_family 然后基于它 sa_familyreinterpret_cast 它到更具体的结构

标准将支持多种指针操作的能力视为实施质量问题。该标准不强制要求所有实现都适用于低级或系统编程,但适用于此类目的的质量实现例如。 Unix 应该支持此类平台上系统代码通常使用的语义种类。一个实现可能无法处理以类型不可知的方式处理结构的共享部分的代码,但仍然是高质量的实现用于某些不涉及任何低级或系统编程的特殊目的。另一方面,适合低级编程的高质量实现应该可以毫无问题地处理此类代码。任何无法处理此类代码的实现都应被视为不适合低级编程的低质量实现and/or,低级程序无法使用此类实现并不是缺陷。

So is it safe to do reinterpret_cast between different types in this case?

不,不是真的。您使用指向 sockaddr 的指针来指向类型 sockaddr_in 的对象。这些是不相关的类型,因此它暗示了一些不正确的事情:指向两个不相关的对象,但只分配了一个对象。

如果您在最受限的系统上工作,那么是的,您可能对此感到满意,正如@supercat 所说,您的实施者可能会支持您。但是您的代码将不可移植。

there does not seem to be a way to do that differently

规定的解决方案是为两个对象分配内存并使用std::memcpy使它们相等:

sockaddr sa2;
std::memcpy(&sa2, &sa, sizeof(sa));
bind(sfd, &sa2, sizeof(sa));

来自 cppreference.com:

Where strict aliasing prohibits examining the same memory as values of two different types, std::memcpy may be used to convert the values.

重要的是两个对象(sockaddr_insockaddr)的大小相同。你可以断言是这样的:

static_assert(sizeof(sa2) == sizeof(sa));

拨打 std::memcpy 并不总是免费的,但通常是免费的。 (example)

与最佳答案相反,我会说 bind 可以并且必须以 reinterpret_cast 在这里安全使用的方式编写。例如,bind 可以实现为:

int bind(SOCKET s, const sockaddr* addr, int addrlen) {
    std::uint16_t address_family;
    std::memcpy(&address_family, addr, sizeof(address_family));
    if (address_family == AF_INET) {
        const sockaddr_in* sin = reinterpret_cast<const sockaddr_in*>(addr);
        // Accessing sin->sin_addr is safe here
        ...
    } else if (address_family == AF_INET6) {
        const sockaddr_in6* sin6 = reinterpret_cast<const sockaddr_in6*>(addr);
        ...
    }
}

关键是 reinterpret_cast 本身不是 UB,它正在尝试访问 UB 的数据(参见 Type Aliasing):

Whenever an attempt is made to read or modify the stored value of an object of type DynamicType through a glvalue of type AliasedType, the behavior is undefined[...]

在上面的代码中,我们从来没有尝试通过sockaddr*类型的指针来读取addr的内容。我们检查值表示(原始字节)以获取地址族,它告诉我们要使用的结构的确切类型。然后我们可以安全地转换回原来的类型。标准允许强制转换为不同类型的指针然后返回原始类型。

更进一步,我会说 sockaddr_in6 实现必须正确处理 reinterpret_cast。自 sizeof(sockaddr_in6) > sizeof(sockaddr) 起,memcpy 技巧不再有效。 API 特别要求指向错误类型对象的指针,因此 API 实施者有责任正确使用指针。