malloc-free-malloc和严格别名

Aar*_*aid 5 c strict-aliasing

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

更新:根据目前为止的答案,很明显我需要澄清这个问题.从某个角度来看,这里的第一个列表是"明显"定义的行为.真正的问题是遵循这个逻辑到自定义分配器和自定义内存池.如果我malloc在开始时有一大块内存,然后编写我自己的my_malloc并且my_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);
}
Run Code Online (Sandbox Code Playgroud)

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

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

如果不是UB,为什么?标准中是否有明确的例外,它说mallocfree(和calloc/ realloc/ ...)被允许"删除"与特定地址相关的类型,允许进一步访问在地址上"压印"一个新类型?

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

自定义分配器

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

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


(...除非,当然,除非有另一条规则以特殊的方式制作memcpymemmove特殊malloc.允许它们将类型更改为目标指针的类型.这与我的'证据一致'.)


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


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

n. *_* m. 5

以下是 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);
}
Run Code Online (Sandbox Code Playgroud)

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

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


Ser*_*sta 1

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

\n\n
    \n
  • 您分配一块内存,使用它并释放它
  • \n
  • 您分配另一个内存块,使用它并释放它。
  • \n
\n\n

未定义的是是否会收到与不同时间相同p16的值p32

\n\n

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

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

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

\n\n
\n\n

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

\n\n

所以你有一大块内存并且想要编写自定义的 free 和 malloc 函数而无需 UB。有可能的。在这里我不会深入讨论分配块和空闲块管理的困难部分,只是给出提示。

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

    \n\n
    intptr_t orig_addr = chunk;\nint delta = orig_addr % alignment;\nchar *pool = chunk + alignement - delta; /* pool in now aligned */\n
    Run Code Online (Sandbox Code Playgroud)
  6. \n
\n\n

现在,您只需从自己的块地址池返回并pool + n * alignement转换为void *: 6.3.2.3 \xc2\xa71 说:指向 void 的指针可以转换为指向任何不完整或对象类型的指针或从指向任何不完整或对象类型的指针转​​换。指向任何不完整或对象类型的指针可以转换为指向 void 的指针,然后再转换回来;结果应等于原始指针。

\n\n

使用 C11 会更干净,因为 C11 明确添加了_Alignasalignof关键字来显式处理它,并且它会比当前的 hack 更好。但它应该仍然有效

\n\n

限制:

\n\n

我必须承认,我对 6.3.2.3 \xc2\xa77 的解释是指向正确对齐的 char 数组的指针可以转换为另一种类型的指针,这并不是真正整洁和清晰。有人可能会说,这里所说的只是如果它本来指向其他类型,那么它可以用作 char 指针。但当我从 char 指针开始时,这是不明确允许的。确实如此,但这是可以做到的最好的,它没有明确标记为未定义的行为......这就是 malloc 在幕后所做的事情。

\n\n

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

\n