使用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
相关产品推荐
相关产品推荐

