You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用GetAdaptersAddresses时如何避免C++未定义行为

解决GetAdaptersAddresses缓冲区的C++未定义行为问题

问题背景

调用GetAdaptersAddresses()时需要传入IP_ADAPTER_ADDRESSES*类型的缓冲区作为AdapterAddresses参数。原本用std::unique_ptr<IP_ADAPTER_ADDRESSES[]>创建缓冲区,但发现该API会在缓冲区中写入IP_ADAPTER_ADDRESSES之外的其他结构和字符串(比如AdapterName、FirstUnicastAddress指向的内容),显然原用法存在问题。

考虑改用std::unique_ptr<std::byte[]>并依赖隐式对象创建(可能需要配合std::launder),但调用API时要将缓冲区转为IP_ADAPTER_ADDRESSES*类型,担心reinterpret_cast违反严格别名规则,因此需要修正代码以避免C++未定义行为。


相关API与原问题代码

GetAdaptersAddresses函数签名

IPHLPAPI_DLL_LINKAGE ULONG GetAdaptersAddresses(
  [in]      ULONG                 Family,
  [in]      ULONG                 Flags,
  [in]      PVOID                 Reserved,
  [in, out] PIP_ADAPTER_ADDRESSES AdapterAddresses,
  [in, out] PULONG                SizePointer
);

原问题代码(存在未定义行为)

std::optional<sockaddr> GetAdapterIPv4SockAddr(const std::string& name)
{
    int neededSize = 128;
    std::unique_ptr<IP_ADAPTER_ADDRESSES[]> adapterAddresses = std::make_unique<IP_ADAPTER_ADDRESSES[]>(neededSize);
    ULONG dwBufLen = sizeof(IP_ADAPTER_ADDRESSES) * neededSize;

    DWORD dwStatus = GetAdaptersAddresses(
        AF_UNSPEC,
        GAA_FLAG_INCLUDE_PREFIX | GAA_FLAG_INCLUDE_GATEWAYS,
        NULL,
        adapterAddresses.get(),
        &dwBufLen);

    if (dwStatus != ERROR_SUCCESS)
        return std::nullopt;

    PIP_ADAPTER_ADDRESSES pAdapterInfo = adapterAddresses.get();

    do
    {
        PIP_ADAPTER_UNICAST_ADDRESS address = nullptr;
        if (strcmp(pAdapterInfo->AdapterName, name.c_str()) == 0)
        {
            address = pAdapterInfo->FirstUnicastAddress;
            while (address != nullptr && address->Address.lpSockaddr->sa_family != AF_INET)
                address = address->Next;
        }

        if (address != nullptr)
            return *address->Address.lpSockaddr;
        else
            return std::nullopt;

        pAdapterInfo = pAdapterInfo->Next;
    } while (pAdapterInfo);

    return std::nullopt;
}

修正方案

核心思路

GetAdaptersAddresses是C风格API,会在传入的缓冲区中原地构造多个关联结构(如IP_ADAPTER_UNICAST_ADDRESS、字符串等),这些结构会紧接在第一个IP_ADAPTER_ADDRESSES之后占用空间。原用法的问题在于:初始化了IP_ADAPTER_ADDRESSES数组,但API会覆盖数组外的内存,还会在缓冲区中构造其他类型对象,触发未定义行为。

正确做法是用字节数组作为原始缓冲区,确保内存未被错误初始化,再通过合法方式转为IP_ADAPTER_ADDRESSES*供API使用,访问结构时严格遵守严格别名规则。

修正后的代码

#include <memory>
#include <optional>
#include <cstring>
#include <winsock2.h>
#include <iphlpapi.h>

#pragma comment(lib, "iphlpapi.lib")
#pragma comment(lib, "ws2_32.lib")

std::optional<sockaddr> GetAdapterIPv4SockAddr(const std::string& name)
{
    // 初始缓冲区大小,覆盖绝大多数场景
    ULONG dwBufLen = 16384;
    std::unique_ptr<std::byte[]> buffer = std::make_unique<std::byte[]>(dwBufLen);

    DWORD dwStatus = GetAdaptersAddresses(
        AF_UNSPEC,
        GAA_FLAG_INCLUDE_PREFIX | GAA_FLAG_INCLUDE_GATEWAYS,
        NULL,
        reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.get()),
        &dwBufLen);

    // 缓冲区不够时重新分配并再次调用
    if (dwStatus == ERROR_BUFFER_OVERFLOW)
    {
        buffer = std::make_unique<std::byte[]>(dwBufLen);
        dwStatus = GetAdaptersAddresses(
            AF_UNSPEC,
            GAA_FLAG_INCLUDE_PREFIX | GAA_FLAG_INCLUDE_GATEWAYS,
            NULL,
            reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.get()),
            &dwBufLen);
    }

    if (dwStatus != ERROR_SUCCESS)
        return std::nullopt;

    // 用std::launder告知编译器缓冲区中已构造合法对象
    PIP_ADAPTER_ADDRESSES pAdapterInfo = std::launder(
        reinterpret_cast<PIP_ADAPTER_ADDRESSES>(buffer.get()));

    do
    {
        PIP_ADAPTER_UNICAST_ADDRESS address = nullptr;
        if (strcmp(pAdapterInfo->AdapterName, name.c_str()) == 0)
        {
            // 通过偏移量计算地址,配合std::launder遵守严格别名规则
            address = std::launder(reinterpret_cast<PIP_ADAPTER_UNICAST_ADDRESS>(
                reinterpret_cast<std::byte*>(pAdapterInfo) + pAdapterInfo->FirstUnicastAddressOffset));
            
            while (address != nullptr)
            {
                sockaddr* addr = std::launder(reinterpret_cast<sockaddr*>(address->Address.lpSockaddr));
                if (addr->sa_family == AF_INET)
                    break;
                
                // 用Offset代替Next指针,避免跨类型指针访问问题
                if (address->NextOffset == 0)
                    address = nullptr;
                else
                    address = std::launder(reinterpret_cast<PIP_ADAPTER_UNICAST_ADDRESS>(
                        reinterpret_cast<std::byte*>(address) + address->NextOffset));
            }
        }

        if (address != nullptr)
        {
            sockaddr result = *std::launder(reinterpret_cast<sockaddr*>(address->Address.lpSockaddr));
            return result;
        }

        // 处理下一个适配器结构
        if (pAdapterInfo->NextOffset == 0)
            break;
        pAdapterInfo = std::launder(reinterpret_cast<PIP_ADAPTER_ADDRESSES>(
            reinterpret_cast<std::byte*>(pAdapterInfo) + pAdapterInfo->NextOffset));

    } while (pAdapterInfo);

    return std::nullopt;
}

关键要点说明

  • 用std::byte[]作为缓冲区:字节类型是C++标准中用于原始内存的类型,不会触发任何对象初始化,适合作为C API的原始缓冲区。
  • 合法使用reinterpret_cast:将std::byte*转为IP_ADAPTER_ADDRESSES*是合法的,因为缓冲区内存足够且对齐符合要求,API会在其中隐式构造对象,配合std::launder告知编译器识别新对象。
  • 使用偏移量而非Next指针:结构中的XXXOffset是相对于缓冲区起始地址的偏移量,用它计算下一个结构地址,比直接用Next指针更安全,严格遵守严格别名规则。
  • std::launder的作用:当内存被重新用于构造不同类型对象时,std::launder避免编译器优化导致的未定义行为,确保正确识别内存中的新对象。

内容的提问来源于stack exchange,提问作者TheLast Rar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 00:05:59