【问题标题】:mmap and C++ strict aliasing rulesmmap 和 C++ 严格的别名规则
【发布时间】:2017-07-25 18:14:26
【问题描述】:

考虑一个 POSIX.1-2008 兼容的操作系统,并让 fd 是一个有效的文件描述符(对于一个打开的文件、读取模式、足够的数据......)。以下代码符合 C++11 标准*(忽略错误检查):

void* map = mmap(NULL, sizeof(int)*10, PROT_READ, MAP_PRIVATE, fd, 0);
int* foo = static_cast<int*>(map);

现在,以下指令是否违反了严格的别名规则?

int bar = *foo;

按照标准:

如果程序尝试通过以下类型之一以外的左值访问对象的存储值,则行为未定义:

  • 对象的动态类型,
  • 对象动态类型的 cv 限定版本,
  • 与对象的动态类型类似(如 4.4 中定义)的类型,
  • 对应于对象动态类型的有符号或无符号类型,
  • 一种有符号或无符号类型,对应于对象动态类型的 cv 限定版本,
  • 一种聚合或联合类型,在其元素或非静态数据成员(递归地包括子聚合或包含联合的元素或非静态数据成员)中包含上述类型之一,
  • 一种类型,它是对象动态类型的(可能是 cv 限定的)基类类型,
  • char 或 unsigned char 类型。

map / foo 指向的对象的动态类型是什么?那还是一个对象吗?标准说:

类型 T 的对象的生命周期开始于:获得类型 T 的正确对齐和大小的存储,并且如果对象具有非平凡初始化,则其初始化完成。

这是否意味着映射的内存包含 10 个 int 对象(假设初始地址对齐)? 但如果是真的,这是否也适用于这段代码(这显然打破了严格的别名)?

char baz[sizeof(int)];
int* p=reinterpret_cast<int*>(&baz);
*p=5;

奇怪的是,这是否意味着声明 baz 会启动任何(正确对齐的)大小为 4 的对象的生命周期?


一些上下文:我正在映射一个包含我希望直接访问的数据块的文件。由于这个块很大,我想避免 memcpy-ing 到一个临时对象。


*这里可以用 nullptr 代替 NULL,它是否隐式转换为 NULL?标准中的任何参考资料?

【问题讨论】:

  • 你的意思是int* foo = static_cast&lt;int*&gt;(map);(不是fd
  • 不幸的是它确实破坏了严格的别名,但另一方面,编译器无法知道该内存区域中存储的内容,因此它必须假设它可以是任何类型的对象并且因此你不应该看到任何奇怪形式的 UB。
  • @MikeMB 传递-fstrict-aliasing 标志,它将导致未定义的行为,甚至可能优化为NOP。将需要一个 memcpy 或至少一个执行未对齐读取的辅助函数来确保定义的行为。
  • @Yakk:The compiler could know that doesn't create any C++ objects。如何?除了 malloc 或 memcpy,mmap 不是 c++ 标准库的一部分。并且没有理由相信 c 函数不能返回指向包含 c++ 对象的内存的指针。
  • @Yakk:Afaik,posix 规范中没有任何内容表明 mmap 返回指向原始内存的指针,与 malloc 相反,它保证被初始化,在这种情况下,内存甚至包含可能具有的实际数据以前是通过不同的映射编写的。那么你有什么权利附加这样的属性呢?最后,c 和 c++ 函数之间的交互是无论如何定义的 afaik 实现(更不用说 mmap 是一个 posix 函数),所以单独的 c++ 标准可能不会在这里给出最终答案。

标签: c++ c++11 posix


【解决方案1】:

我相信简单的强制转换确实违反了严格的别名。令人信服地争论这超出了我的工资等级,所以这里是一个解决方法的尝试:

template<class T>
T* launder_raw_pod_at( void* ptr ) {
  static_assert( std::is_pod<T>::value, "this only works with plain old data" );
  char buff[sizeof(T)];
  std::memcpy( buff, ptr, sizeof(T) );
  T* r = ::new(ptr) T;
  std::memcpy( ptr, buff, sizeof(T) );
  return r;
}

我相信上述代码对内存的可观察到的副作用为零,并返回指向位于 ptr 的合法 T* 的指针。

检查您的编译器是否将上述代码优化为 noop。为此,它必须理解 memcpy 在真正基础的层面上,并且构造一个T 对那里的内存没有任何作用。

At least clang 4.0.0 can optimize this operation away.

我们要做的是首先复制字节离开。然后我们使用placement new 在那里创建一个T。最后,我们将字节复制回来。

我们有一个合法创建的T,其中包含我们想要的字节。

但是复制回来和复制到本地缓冲区,所以它没有明显的效果。

对象的构造,如果是 pod,也不必触及字节;从技术上讲,字节是未定义的。但是聪明的编译器会说“什么都不做”。

因此编译器可以计算出所有这些操作都可以在运行时跳过。同时,我们在抽象机器中在那个位置正确地创建了一个具有正确字节的对象。 (假设它具有有效的对齐方式!但这不是这段代码的问题。)

【讨论】:

  • 仅供参考:自 4.6 以来的所有 gcc 版本也对此进行了优化。
  • 整洁,但在我的情况下,我不允许写入 ptr 指向的内存。如果代码被优化掉可能没问题,但如果不优化它会导致麻烦。
  • 我认为对象是琐碎的溺爱和琐碎的破坏就足够了
  • @Yakk:Urgs,我应该在提交之前检查自动完成功能;)。我的观点:它不必是可简单构造的(只是默认可构造的),并且在考虑它时:可简单地破坏可能也没有必要。当然,在某些时候,编译器将不再能够将其优化为无操作。
  • @Yakk 我不认为简单的强制转换违反了严格的别名。为了违反严格的别名,需要“尝试访问对象的存储值”,而强制转换没有这样做。但是,一旦指针被取消引用(并读取),那就是违规了。可能取消引用和分配仍然可以(请参阅here)无论如何,您对我的用例有任何解决方法吗?我不允许通过 ptr 写入内存指针。
猜你喜欢
  • 2019-01-29
  • 2013-03-11
  • 2015-10-15
  • 2015-05-31
  • 2017-02-25
  • 2018-12-14
  • 1970-01-01
  • 2011-02-15
  • 2014-07-13
相关资源
最近更新 更多