【问题标题】:How does malloc work with strict aliasing - can it only be violated within a single compilation unit?malloc 如何使用严格的别名 - 它只能在单个编译单元中被违反吗?
【发布时间】:2020-07-25 09:28:51
【问题描述】:

阅读this 后,我有一个类似this one 的问题,想知道内存分配器如何在不违反严格的别名规则的情况下工作。但我并不想重新使用释放的内存,我想知道如何在不违反严格别名的情况下将分配的对象定位在线性内存中。

到目前为止,我看到的所有堆内存分配器都将它们的内存划分为某种块,前面有一个标题。然而,malloc 返回一个void * 并且通常指向紧接在头部之后的内存。这是一个非常缩小的示例来说明这一点。

#include <stddef.h>

struct block_header {
  size_t size;
};

struct block_header *request_space(size_t size);

void *malloc(size_t size) {
    struct block_header *block = request_space(size);

    // I guess this violates strict aliasing, because the caller will 
    // convert the pointer to something other than struct block_header?
    // Or why wouldn't it?
    return block + 1;
}

我已经研究了一段时间,但我看不出分配器如何在不违反严格别名的情况下将其指针定位在内存区域中。我错过了什么?

【问题讨论】:

  • 这不是分配器提供对齐的内存的原因之一,比如 16 字节吗?违规与 C 本身有关。
  • 语言实现本身不必是可移植的。它可以依赖于实现细节。
  • 位于block + 1的内存还没有被malloc使用。所以严格的别名不适用。别名规则说“一个对象应该访问其存储的值 ...”,并且那里没有存储的对象。一旦客户端代码获取到指针并向其中写入一些东西,它就变成了存储的值,它的类型就变成了有效的类型。
  • @rici 谢谢,这确实可以解释很多!
  • 许多标准库的内部结构依赖于 C 标准列出的定义不明确的行为或非标准扩展。例如,诸如 memcpy 之类的函数的各种优化通过读取对齐 uint32_t 的数据来处理例如 32 位块,如果由普通 C 应用程序完成,这将是明显的严格别名违规。

标签: c pointers language-lawyer c11 strict-aliasing


【解决方案1】:

根据标准,这些东西永远不会违反严格的别名:

  • 投射指针。
  • 做指针运算。
  • 写入 malloc 的空间。

你不能在 malloc'd 空间中做的事情是 读取 一些内存作为不同于它所写的类型(当然,允许的别名类型列表除外)。

规则文本在 C11 6.5/7 中:

一个对象的存储值只能被 [...] 访问

并且 6.5/6 中的文本解释说,如果我们在 malloc'd 空间中,那么写入会将写入的类型印在目标上(因此不会出现类型不匹配)。

到目前为止,您发布的代码从未做过禁止的事情,因此没有明显的问题。如果有人使用了你的分配器然后读取内存而不写它,只会有问题。

脚注 1: 根据committee response to DR236,6.5/6 显然是有缺陷的,但从未修复过,所以谁知道它会离开我们。

脚注 2: 正如 Eric 指出的那样,该标准不适用于实现内部,但在您链接到的其他问题中考虑我的 cmets 在一些用户编写的分配器的上下文中。

【讨论】:

  • 委员会似乎不同意您关于写入 malloc 空间的意见。请参阅 示例 1DR236 中的委员会回复
  • @LanguageLawyer 6.5/6 中的文字很清楚,示例 1 定义明确,因此委员会的回应必须解释为 6.5/6 有缺陷。在document log 中有几个更改 6.5/6 的建议。然而 20 年来,这个缺陷并没有得到纠正,那我们该怎么办?
  • @MM:尽管 6.5p6-7 完全按照书面形式没有意义,但修复它所需要做的就是说它仅适用于 (1) 多个左值用于在某些上下文中以冲突的方式访问相同的存储[其中上下文可以在编译器的闲暇时广泛或狭义地绘制],并且(2)左值在该上下文中不是从标识相同对象的指针或左值明显地新派生的或同一数组的成员。我怀疑标准的作者认为编译器作者会以标准为借口......
  • ...忽略可见指针派生。请注意,如果以这种方式修复 6.5p7,则可以消除对 6.5p6 以及“字符类型”异常的需要,但会允许许多有用的优化,这些优化在标准的 clang/gcc 解释下是不可能的。
  • “严格别名”/有效类型规则总是不稳定且不清楚,但我相信至少在如何处理 malloc 内存方面存在某种共识。从 malloc 返回的数据将被视为没有声明的类型,直到完成第一个左值写入访问时——从那里访问的内存具有用于第一次访问的有效类型。否则就无法理解有效的类型规则。
【解决方案2】:

malloc 的源代码不需要像普通源代码那样符合 C 标准。它是 C 实现的一部分。

malloc、编译器和 C 实现的其他部分的工作人员负责确保他们一起工作。这可能包括编译器特别对待mallocmalloc 使用C 编译器保证但C 标准不保证的行为。

【讨论】:

    【解决方案3】:

    C 标准故意避免要求所有实现都适合所有目的。相反,它旨在允许旨在用于各种目的的实现,作为“符合语言扩展”的一种形式,以对这些目的有用的方式有意义地处理构造,即使标准没有强加任何要求。因此,该标准允许旨在用于需要手动内存管理的任务的实现支持促进此类任务的“流行扩展”,即使它故意避免要求不适合此类任务的实现提供此类支持。

    许多类型的内存分配器在语义受限于标准规定的实现上是不切实际的。然而,维护委员会描述的“不要阻止程序员做需要做的事情”的 C 精神原则的实现,并且被设计和配置为适合构建这样的分配器,但是,将认识到以下迹象:存储将用作不止一种类型。编译器识别此类指示的确切情况范围被视为标准管辖范围之外的“实施质量”问题。从实际的角度来看,clang 和 gcc 的作者选择以最低允许质量的方式行事,除非使用 -fno-strict-aliasing,但这意味着程序员想要对这些编译器做任何“有趣”的事情必须使用它选项。没有任何证据表明该标准的作者打算让程序员跳过障碍来适应低质量实现的限制;相反,他们希望市场能够比委员会更好地判断编译器应该如何最有效地完成各种任务。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2015-01-16
      • 2015-05-30
      • 1970-01-01
      • 1970-01-01
      • 2010-10-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多