malloc-free-malloc和strict-aliasing

我最近一直试图理解严格别名的一个特定方面,我想我已经制作了尽可能小的有趣代码。 (对我来说很有趣,就是!)

更新:根据目前为止的答案,很明显我需要澄清这个问题。 从某个角度来看,这里的第一个列表是“明显”定义的行为。 真正的问题是遵循这个逻辑到自定义分配器和自定义内存池。 如果我在开始时malloc一大块内存,然后编写我自己的my_mallocmy_free使用那个单个大块,那么UB是否因为它没有使用官方free

我会坚持使用C,有点随意。 我得到的印象是更容易谈论,C标准更清晰一点。

 int main() { uint32_t *p32 = malloc(4); *p32 = 0; free(p32); uint16_t *p16 = malloc(4); p16[0] = 7; p16[1] = 7; free(p16); } 

第二个malloc可能会返回与第一个malloc相同的地址(因为它之间是free )。 这意味着它正在访问具有两种不同类型的相同内存,这违反了严格的别名。 那么上面肯定是未定义的行为(UB)?

(为简单起见,我们假设malloc总是成功的。我可以添加对malloc的返回值的检查,但这会使问题混乱)

如果它不是UB,为什么? 标准中是否有明确的exception,它表示允许mallocfree (以及calloc / realloc / …)“删除”与特定地址关联的类型,允许进一步访问“压印”新类型地址?

如果malloc / free是特殊的,那么这是否意味着我不能合法地编写我自己的分配器来克隆malloc的行为? 我确信有很多项目都有自定义分配器 – 它们都是UB吗?

自定义分配器

因此,如果我们决定必须定义这样的自定义分配器行为,则意味着严格别名规则本质上是“不正确的”。 我会更新它,说只要你不再使用类型的指针,就可以通过不同(’new’)类型的指针写入 (不读取 )。 如果确认所有编译器基本上都遵守了这个新规则,那么这个措辞可能会悄然改变。

我得到的印象是, gccclang基本上尊重我的(激进的)重新解释。 如果是这样,也许应该相应地编辑标准? 关于gccclang ‘证据’很难描述,它使用memmove具有相同的源和目标(因此被优化),以便它阻止任何不需要的优化,因为它告诉编译器将来通过目标读取指针将对先前通过源指针写入的位模式进行别名。 我能够相应地阻止不受欢迎的解释。 但我想这不是真正的’证据’,也许我只是幸运。 UB显然意味着编译器也被允许给我误导结果!


(…除非,当然,除非有另一个规则使memcpymemmove特殊,就像malloc可能是特殊的一样。允许它们将类型更改为目标指针的类型。这与我的’证据’。)


无论如何,我在漫无边际。 我想一个非常简短的答案是:“是的, malloc (和朋友)是特殊的。自定义分配器不是特殊的,因此是UB,除非它们为每种类型维护单独的内存池。而且,进一步,参见示例X的极端因为编译器Y在这方面是非常严格的并且与这种重新解释相矛盾,所以编译器Y做了不需要的东西的代码片段。


跟进:非malloc内存怎么样? 是否同样适用。 (局部变量,静态变量……)

注意:这只回答了初始问题,而不是关于自定义分配器的部分。


不,它不是UB,因为p16现在拥有不同的对象,前者在你调用free(p32)之后就消失了。

请注意, malloc()返回为每个对象预先对齐的指针,因此这避免了实际术语中严格别名的破坏。 来自C11(N1570)7.22.3 / p1 内存管理function (强调我的):

如果分配成功,则返回指针,以便可以将其分配给指向具有基本对齐要求的任何类型对象的指针,然后用于在分配的空间中访问此类对象或此类对象的数组(直到空间明确解除分配)。 分配对象的生命周期从分配延伸到解除分配

以下是C99严格的别名规则(我希望是)他们的整体:

6.5
(6)访问其存储值的对象的有效类型是对象的声明类型(如果有)。 如果通过具有非字符类型的类型的左值将值存储到没有声明类型的对象中,则左值的类型将成为该访问的对象的有效类型以及不修改该值的后续访问的有效类型储值。 如果使用memcpy或memmove将值复制到没有声明类型的对象中,或者将其复制为字符类型数组,则该访问的修改对象的有效类型以及不修改该值的后续访问的有效类型是复制值的对象的有效类型(如果有)。 对于没有声明类型的对象的所有其他访问,对象的有效类型只是用于访问的左值的类型。

(7)对象的存储值只能由具有以下类型之一的左值表达式访问:
– 与对象的有效类型兼容的类型,
– 与对象的有效类型兼容的类型的限定版本,
– 对应于对象的有效类型的有符号或无符号类型,
– 对应于对象有效类型的限定版本的有符号或无符号类型,
– 聚合或联合类型,其成员中包含上述类型之一(包括递归地,子聚合或包含联合的成员),或者
– 一个字符类型。

这两个子句一起禁止一个特定情况,通过类型X的左值存储值,然后通过与X不兼容的Y类型的左值检索值。

因此,当我阅读标准时,即使这种用法也完全正常(假设4个字节足以存储uint32_t或两个uint16_t )。

 int main() { uint32_t *p32 = malloc(4); *p32 = 0; /* do not do this: free(p32); */ /* do not do this: uint16_t *p16 = malloc(4); */ /* do this instead: */ uint16_t *p16 = (uint16_t *)p32; p16[0] = 7; p16[1] = 7; free(p16); } 

没有规则禁止存储uint32_t然后随后将uint16_t存储在同一地址,所以我们完全没问题。

因此,没有什么能阻止编写完全兼容的池分配器。

您的代码是正确的C并且不会调用未定义的行为(除了您不测试malloc返回值),因为:

  • 你分配了一块内存,使用它并释放它
  • 你分配另一个内存块,使用它并释放它。

未定义的是p16是否将获得与p32 在不同时间具有的相同值

什么是未定义的行为,即使值相同,也可以在释放后访问p32 。 例子 :

 int main() { uint32_t *p32 = malloc(4); *p32 = 0; free(p32); uint16_t *p16 = malloc(4); p16[0] = 7; p16[1] = 7; if (p16 == p32) { // whether p16 and p32 are equal is undefined uint32_t x = *p32; // accessing *p32 is explicitely UB } free(p16); } 

它是UB,因为您在释放后尝试访问内存块。 即使它确实指向内存块,该内存块也已初始化为uint16_t数组,使用它作为指向另一种类型的指针是正式未定义的行为。


自定义分配(假设符合C99的编译器):

所以你有一大块内存,想要编写自定义的free和malloc函数而不需要UB。 有可能的。 在这里,我不会深入探讨分配和自由集团管理的难点,只是给出提示。

  1. 你需要知道它对实施的最严格的对等。 stdlib malloc知道它,因为C99语言规范(草案n1256)的7.20.3§1说: 如果分配成功,则返回的指针被适当地对齐,以便可以将其指定给指向任何类型对象的指针 。 它通常是32位系统上的4位和64位系统上的8位,但可能更大或更小……
  2. 内存池必须是char数组,因为6.3.2.3§7说: 指向对象或不完整类型的指针可能会转换为指向不同对象或不完整类型的指针。 如果结果指针未针对指向类型正确对齐,则行为未定义。 否则,当再次转换回来时,结果应该等于原始指针。 当指向对象的指针转换为指向字符类型的指针时,结果指向对象的最低寻址字节。 结果的连续递增(直到对象的大小)产生指向对象的剩余字节的指针。 :这意味着只要您可以处理对齐,正确大小的字符数组可以转换为指向任意类型的指针(并且是malloc实现的基础)
  3. 您必须使您的内存池从与系统对齐兼容的地址开始:

     intptr_t orig_addr = chunk; int delta = orig_addr % alignment; char *pool = chunk + alignement - delta; /* pool in now aligned */ 

你现在只需要从你自己的池集合地址返回作为pool + n * alignement并转换为void * :6.3.2.3§1说: 指向void的指针可以转换为指向任何不完整或对象的指针类型。 指向任何不完整或对象类型的指针可能会转换为指向void的指针并再次返回; 结果应该等于原始指针。

C11会更干净,因为C11明确地添加了_Alignasalignof关键字来明确地处理它,它会比当前的黑客更好。 但它应该工作

限制:

我必须承认,我对6.3.2.3§7的解释是指向正确对齐的char数组的指针可以转换为另一种类型的指针并不是很整洁。 有人可能会说,所说的只是如果它最初指向另一种类型,它可以用作char指针。 但是当我从一个char指针开始时,它没有明确允许。 这是真的,但它是最好的,可以做到的,它没有被明确标记为未定义的行为……而且它正是malloc所做的。

由于对象明确依赖于实现,因此无法创建可用于任何实现的通用库。

关于混叠的实际规则在标准的第6.5节第7段中列出。注意措辞:

对象的存储值只能由具有以下类型之一的左值表达式访问:

(强调我的)

别名包括对象的概念,而不仅仅是一般的记忆。 对于malloc在第二次使用时返回相同地址,要求原始对象已被释放。 即使它具有相同的地址,也不会被视为相同的对象。 由于完全不同的原因,任何尝试通过悬空之后的剩余指针访问第一个对象都是UB,因此没有别名,因为任何继续使用第一个指针p32无论如何都是无效的。