c++ - 在内存映射中使用 reinterpret_cast 时处理未定义的行为
问题描述
为了避免复制大量数据,最好是mmap
二进制文件并直接处理原始数据。这种方法有几个优点,包括将分页委托给操作系统。不幸的是,我的理解是明显的实现会导致未定义的行为(UB)。
我的用例如下:创建一个二进制文件,其中包含一些标识格式的标头并提供元数据(在这种情况下只是double
值的数量)。文件的其余部分包含我希望处理的原始二进制值,而不必先将文件复制到本地缓冲区(这就是我首先对文件进行内存映射的原因)。下面的程序是一个完整的(如果简单的话)示例(我相信所有标记为UB[X]
通向UB的地方):
// C++ Standard Library
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <fstream>
#include <iostream>
#include <numeric>
// POSIX Library (for mmap)
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
constexpr char MAGIC[8] = {"1234567"};
struct Header {
char magic[sizeof(MAGIC)] = {'\0'};
std::uint64_t size = {0};
};
static_assert(sizeof(Header) == 16, "Header size should be 16 bytes");
static_assert(alignof(Header) == 8, "Header alignment should be 8 bytes");
void write_binary_data(const char* filename) {
Header header;
std::copy_n(MAGIC, sizeof(MAGIC), header.magic);
header.size = 100u;
std::ofstream fp(filename, std::ios::out | std::ios::binary);
fp.write(reinterpret_cast<const char*>(&header), sizeof(Header));
for (auto k = 0u; k < header.size; ++k) {
double value = static_cast<double>(k);
fp.write(reinterpret_cast<const char*>(&value), sizeof(double));
}
}
double read_binary_data(const char* filename) {
// POSIX mmap API
auto fp = ::open(filename, O_RDONLY);
struct stat sb;
::fstat(fp, &sb);
auto data = static_cast<char*>(
::mmap(nullptr, sb.st_size, PROT_READ, MAP_PRIVATE, fp, 0));
::close(fp);
// end of POSIX mmap API (all error handling ommitted)
// UB1
const auto header = reinterpret_cast<const Header*>(data);
// UB2
if (!std::equal(MAGIC, MAGIC + sizeof(MAGIC), header->magic)) {
throw std::runtime_error("Magic word mismatch");
}
// UB3
auto beg = reinterpret_cast<const double*>(data + sizeof(Header));
// UB4
auto end = std::next(beg, header->size);
// UB5
auto sum = std::accumulate(beg, end, double{0});
::munmap(data, sb.st_size);
return sum;
}
int main() {
const double expected = 4950.0;
write_binary_data("test-data.bin");
if (auto sum = read_binary_data("test-data.bin"); sum == expected) {
std::cout << "as expected, sum is: " << sum << "\n";
} else {
std::cout << "error\n";
}
}
编译并运行为:
$ clang++ example.cpp -std=c++17 -Wall -Wextra -O3 -march=native
$ ./a.out
$ as expected, sum is: 4950
在现实生活中,实际的二进制格式要复杂得多,但保留了相同的属性:基本类型存储在具有适当对齐的二进制文件中。
我的问题是:你如何处理这个用例?
我发现了许多我认为相互矛盾的答案。
一些答案明确指出应该在本地构建对象。这很可能是这种情况,但会使任何面向数组的操作变得非常复杂。
其他地方的评论似乎同意这种结构的 UB 性质,但存在一些分歧。
cppreference中的措辞至少对我来说是令人困惑的。我会将其解释为“我正在做的事情是完全合法的”。特别是这一段:
每当尝试通过 AliasedType 类型的 glvalue 读取或修改 DynamicType 类型的对象的存储值时,除非满足以下条件之一,否则该行为是未定义的:
- AliasedType 和 DynamicType 类似。
- AliasedType 是 DynamicType 的(可能是 cv 限定的)有符号或无符号变体。
- AliasedType 是 std::byte、(C++17 起)char 或 unsigned char:这允许将任何对象的对象表示检查为字节数组。
可能是 C++17 提供了一些希望,std::launder
或者我必须等到 C++20 才能获得类似std::bit_cast
.
同时,您如何处理这个问题?
在线演示链接:https ://onlinegdb.com/rk_xnlRUV
C中的简化示例
我的理解是正确的,以下 C 程序没有表现出未定义的行为?我知道通过char
缓冲区的指针转换不参与严格的别名规则。
#include <stdint.h>
#include <stdio.h>
struct Header {
char magic[8];
uint64_t size;
};
static void process(const char* buffer) {
const struct Header* h = (const struct Header*)(buffer);
printf("reading %llu values from buffer\n", h->size);
}
int main(int argc, char* argv[]) {
if (argc != 2) {
return 1;
}
// In practice, I'd pass the buffer through mmap
FILE* fp = fopen(argv[1], "rb");
char buffer[sizeof(struct Header)];
fread(buffer, sizeof(struct Header), 1, fp);
fclose(fp);
process(buffer);
}
我可以通过传递由原始 C++ 程序创建的文件来编译和运行此 C 代码,并按预期工作:
$ clang struct.c -std=c11 -Wall -Wextra -O3 -march=native
$ ./a.out test-data.bin
reading 100 values from buffer
解决方案
std::launder
解决了严格别名的问题,但不是对象生命周期的问题。
std::bit_cast
制作一个副本(它基本上是 的包装器std::memcpy
)并且不适用于从一系列字节进行复制。
标准 C++ 中没有工具可以在不复制的情况下重新解释映射内存。已经提出了这样的工具:std::bless。直到/除非此类更改被采纳到标准中,您必须要么希望 UB 不会破坏任何东西†</sup>,采取潜在的††</sup> 性能冲击并复制,或者将程序写入C。
†</sup> 虽然并不理想,但这并不一定像听起来那么糟糕。您已经通过 using 限制了可移植性mmap
,并且如果您的目标系统/编译器承诺可以重新解释mmap
ped 内存(可能通过洗钱),那么应该没有问题。也就是说,我不知道是否说,Linux 上的 GCC 提供了这样的保证。
††</sup> 编译器可能会优化std::memcpy
掉。可能不会对性能造成任何影响。在这个SO 答案中有一个方便的功能,它被观察到被优化掉,但确实按照语言规则启动对象生命周期。它确实有一个限制,映射内存必须是可写的(因为它在内存中创建对象,并且在非优化构建中它可能会执行实际复制)。
推荐阅读
- azure - 在 Azure 中使用异地复制时,如何在应用服务中设置配置字符串?
- java - 使用环境变量进行 Spring Boot 集成测试
- excel - 作者姓名转换的Excel公式
- javascript - Express.js 应用程序在请求 url 中添加一个额外的斜杠
- php - 试图将字符串更改为 Date PHP
- c# - 解析 Azure 搜索查询筛选器
- javascript - 如何通过单击从循环的 js 对象中获取的导航栏项来使用 useState 来切换下拉菜单
- java - 二进制 XML 文件第 17 行:膨胀类 com.camerakit.CameraKitView 时出错
- sql - BigQuery:为表中的缺失列填充空值
- docker - Gunicorn Docker 容器仅监听 `0.0.0.0`