跨 DLL 边界的严格别名
Strict aliasing accross DLL boundary
我一直在审查 C++ 的严格别名规则,这让我想到了我之前工作中的一些代码。我相信上述代码违反了严格的别名规则,但很好奇为什么我们没有 运行 解决任何问题或编译器警告。我们使用核心 .DLL 来接收传递给服务器应用程序的网络消息。已完成的(非常)简化的示例:
#include <iostream>
#include <cstring>
using namespace std;
// These enums/structs lived in a shared .h file consumed by the DLL and server application
enum NetworkMessageId : int
{
NETWORK_MESSAGE_LOGIN
// ...
};
struct NetworkMessageBase
{
NetworkMessageId type;
size_t size;
};
struct LoginNetworkMessage : NetworkMessageBase
{
static constexpr size_t MaxUsernameLength = 25;
static constexpr size_t MaxPasswordLength = 50;
char username[MaxUsernameLength];
char password[MaxUsernameLength];
};
// This buffer and function was created/exported by the DLL
char* receiveBuffer = new char[sizeof(LoginNetworkMessage)];
NetworkMessageBase* receiveNetworkMessage()
{
// Simulate receiving data from network, actual production code provided additional safety checks
LoginNetworkMessage msg;
msg.type = NETWORK_MESSAGE_LOGIN;
msg.size = sizeof(msg);
strcpy(msg.username, "username1");
strcpy(msg.password, "qwerty");
memcpy(receiveBuffer, &msg, sizeof(msg));
return (NetworkMessageBase*)&receiveBuffer[0]; // I believe this line invokes undefined behavior (strict aliasing)
}
// Pretend main is the server application
int main()
{
NetworkMessageBase* msg = receiveNetworkMessage();
switch (msg->type)
{
case NETWORK_MESSAGE_LOGIN:
{
LoginNetworkMessage* loginMsg = (LoginNetworkMessage*)msg;
cout << "Username: " << loginMsg->username << " Password: " << loginMsg->password << endl;
}
break;
}
delete [] receiveBuffer; // A cleanup function defined in the DLL actually did this
return 0;
}
据我了解,receiveNetworkMessage()
调用了未定义的行为。我读过严格别名 U.B。通常与编译器 optimizations/assumptions 有关。我认为这些优化与这种情况无关,因为 .DLL 和服务器应用程序是单独编译的。对吗?
最后,客户端应用程序还共享了示例 .h,它利用它创建了一个 LoginNetworkMessage
,该文件逐字节流式传输到服务器。这是便携式的吗? Packing/endianness 抛开问题不谈,我相信不是因为 LoginNetworkMessage
的布局是非标准的,所以成员顺序可能不同。
From what I understand, receiveNetworkMessage() invokes undefined behavior
正确。
LoginNetworkMessage which was streamed byte-for-byte to the server. Is this portable?
不,依赖二进制兼容性的网络通信不可移植。
Packing/endianness issues aside, I believe it's not since LoginNetworkMessage's layout is non-standard, so member ordering may be different.
即使是非标准布局也能保证成员的顺序类。问题(除了字节顺序)是用于对齐目的的填充量和位置、基本类型的大小、字节中的位数(尽管公平地说,非 8 位字节的网络连接硬件可能不是你需要支持的东西)。
I'm thinking these optimizations are not relevant in this case since the .DLL and server application are compiled separately. Is that correct?
是的。这使得代码在实践中是安全的(即使标准不知道任何 DLL 并且无论如何都认为它是未定义的)。
不同的翻译单元也是如此,除非启用了全程序优化。
正如另一个答案所说,这里唯一的潜在问题是结构布局的可移植性。
C 标准未尝试考虑其整个源代码不可同时用于 C 实现的程序的语义,因此对实现如何处理此类程序没有强加要求。由于并非所有实现都可以支持此类概念,因此任何依赖它们的程序本质上都是“不可移植的”,并且由于标准对它们的处理方式没有强加要求,因此它们在技术上会调用未定义的行为。
另一方面,该标准认识到未定义行为可能发生在不可移植但正确的程序中,并指定实现可以“以环境特征的文档化方式”处理调用未定义行为的程序。允许调用单独构建的代码片段的实现通常指定如何在机器级别处理对此类函数的调用,因此应该期望以记录的方式处理此类调用,而不管标准是否要求它们这样做.
我一直在审查 C++ 的严格别名规则,这让我想到了我之前工作中的一些代码。我相信上述代码违反了严格的别名规则,但很好奇为什么我们没有 运行 解决任何问题或编译器警告。我们使用核心 .DLL 来接收传递给服务器应用程序的网络消息。已完成的(非常)简化的示例:
#include <iostream>
#include <cstring>
using namespace std;
// These enums/structs lived in a shared .h file consumed by the DLL and server application
enum NetworkMessageId : int
{
NETWORK_MESSAGE_LOGIN
// ...
};
struct NetworkMessageBase
{
NetworkMessageId type;
size_t size;
};
struct LoginNetworkMessage : NetworkMessageBase
{
static constexpr size_t MaxUsernameLength = 25;
static constexpr size_t MaxPasswordLength = 50;
char username[MaxUsernameLength];
char password[MaxUsernameLength];
};
// This buffer and function was created/exported by the DLL
char* receiveBuffer = new char[sizeof(LoginNetworkMessage)];
NetworkMessageBase* receiveNetworkMessage()
{
// Simulate receiving data from network, actual production code provided additional safety checks
LoginNetworkMessage msg;
msg.type = NETWORK_MESSAGE_LOGIN;
msg.size = sizeof(msg);
strcpy(msg.username, "username1");
strcpy(msg.password, "qwerty");
memcpy(receiveBuffer, &msg, sizeof(msg));
return (NetworkMessageBase*)&receiveBuffer[0]; // I believe this line invokes undefined behavior (strict aliasing)
}
// Pretend main is the server application
int main()
{
NetworkMessageBase* msg = receiveNetworkMessage();
switch (msg->type)
{
case NETWORK_MESSAGE_LOGIN:
{
LoginNetworkMessage* loginMsg = (LoginNetworkMessage*)msg;
cout << "Username: " << loginMsg->username << " Password: " << loginMsg->password << endl;
}
break;
}
delete [] receiveBuffer; // A cleanup function defined in the DLL actually did this
return 0;
}
据我了解,receiveNetworkMessage()
调用了未定义的行为。我读过严格别名 U.B。通常与编译器 optimizations/assumptions 有关。我认为这些优化与这种情况无关,因为 .DLL 和服务器应用程序是单独编译的。对吗?
最后,客户端应用程序还共享了示例 .h,它利用它创建了一个 LoginNetworkMessage
,该文件逐字节流式传输到服务器。这是便携式的吗? Packing/endianness 抛开问题不谈,我相信不是因为 LoginNetworkMessage
的布局是非标准的,所以成员顺序可能不同。
From what I understand, receiveNetworkMessage() invokes undefined behavior
正确。
LoginNetworkMessage which was streamed byte-for-byte to the server. Is this portable?
不,依赖二进制兼容性的网络通信不可移植。
Packing/endianness issues aside, I believe it's not since LoginNetworkMessage's layout is non-standard, so member ordering may be different.
即使是非标准布局也能保证成员的顺序类。问题(除了字节顺序)是用于对齐目的的填充量和位置、基本类型的大小、字节中的位数(尽管公平地说,非 8 位字节的网络连接硬件可能不是你需要支持的东西)。
I'm thinking these optimizations are not relevant in this case since the .DLL and server application are compiled separately. Is that correct?
是的。这使得代码在实践中是安全的(即使标准不知道任何 DLL 并且无论如何都认为它是未定义的)。
不同的翻译单元也是如此,除非启用了全程序优化。
正如另一个答案所说,这里唯一的潜在问题是结构布局的可移植性。
C 标准未尝试考虑其整个源代码不可同时用于 C 实现的程序的语义,因此对实现如何处理此类程序没有强加要求。由于并非所有实现都可以支持此类概念,因此任何依赖它们的程序本质上都是“不可移植的”,并且由于标准对它们的处理方式没有强加要求,因此它们在技术上会调用未定义的行为。
另一方面,该标准认识到未定义行为可能发生在不可移植但正确的程序中,并指定实现可以“以环境特征的文档化方式”处理调用未定义行为的程序。允许调用单独构建的代码片段的实现通常指定如何在机器级别处理对此类函数的调用,因此应该期望以记录的方式处理此类调用,而不管标准是否要求它们这样做.