【问题标题】:Is it possible to use a character array as a memory pool without violating strict aliasing?是否可以在不违反严格别名的情况下将字符数组用作内存池?
【发布时间】:2021-08-05 04:12:27
【问题描述】:

我有一个静态分配的字符数组。我可以在不违反严格的别名规则的情况下重用这个数组来存储不同的类型吗?我不太了解严格的别名,但这是我想做的代码示例:

#include <stdio.h>

static char memory_pool[256 * 1024];

struct m1
{
    int f1;
    int f2;
};

struct m2
{
    long f1;
    long f2;
};

struct m3
{
    float f1;
    float f2;
    float f3;
};

int main()
{
    void *at;
    struct m1 *m1;
    struct m2 *m2;
    struct m3 *m3;

    at = &memory_pool[0];
    
    m1 = (struct m1 *)at;
    m1->f1 = 10;
    m1->f2 = 20;

    printf("m1->f1 = %d, m1->f2 = %d;\n", m1->f1, m1->f2);

    m2 = (struct m2 *)at;
    m2->f1 = 30L;
    m2->f2 = 40L;

    printf("m2->f1 = %ld, m2->f2 = %ld;\n", m2->f1, m2->f2);

    m3 = (struct m3 *)at;
    m3->f1 = 5.0;
    m3->f2 = 6.0;
    m3->f3 = 7.0;

    printf("m3->f1 = %f, m3->f2 = %f, m3->f3 = %f;\n", m3->f1, m3->f2, m3->f3);

    return 0;
}

我使用 gcc 和 -Wstrict-aliasing=3 -fstrict-aliasing 编译了这段代码,它按预期工作:

m1->f1 = 10, m1->f2 = 20;
m2->f1 = 30, m2->f2 = 40;
m3->f1 = 5.000000, m3->f2 = 6.000000, m3->f3 = 7.000000;

该代码安全吗?假设memory_pool 总是足够大。

【问题讨论】:

  • 你为什么不直接使用联合?
  • 我不是专家也不是语言律师,但我相信这是 UB,因为别名涉及多个名称相同类型被用来指代相同的内存位置- 因为您使用不同的类型,这使得它更像 union - 在这种情况下,您应该特别使用 union
  • @BillLynch 这样做和使用union 有什么区别吗?
  • @valentinsp 实际上,如果您使用一些翻译单元隔离,大多数编译器将无法知道void *myalloc(void); 或某些extern char *myblock; 是否会返回具有声明类型的指针或不是,所以他们必须保守地将通过它的访问视为对没有声明类型的内存的访问(即使该类型是在某些不可访问的翻译单元中声明的)。
  • @PSkocik -- 是的,这就是我提供的链接的确切部分..."An object shall have its stored value accessed only by an lvalue expression that has one of the following types: --- a character type"。是的,对此有很多讨论,但都以有限的不确定性告终。在这里,您有一个内存块,用于存储char 类型的块。它可以通过对象类型或char 类型访问。将其放入 char 块将需要 memcpy(),这在 (P6) 中进行了讨论。还有关于访问是否会修改对象的警告。

标签: c language-lawyer strict-aliasing memory-pool


【解决方案1】:

是否可以在不违反严格别名的情况下将字符数组用作内存池?

没有。 C 2018 6.5 7 中的规则说,定义为 char 数组的对象可以按以下方式访问:

  1. char数组兼容的类型,
  2. char 数组兼容的类型的合格版本,
  3. 对应char数组的有符号或无符号类型,
  4. 对应char数组的有符号或无符号类型,
  5. 在其成员中包含 char 数组的聚合或联合类型,或
  6. 一种字符类型。

char 的数组不能使用 3 和 4;它们仅适用于原始类型是整数类型的情况。在您使用结构的各种示例中,结构不是与char 数组兼容的类型(它们的成员也不兼容),排除了 1 和 2。它们的成员中不包括 char 数组,排除了 5。它们不是字符类型,排除 6.

我使用带有 -Wstrict-aliasing=3 -fstrict-aliasing 的 gcc 编译了这段代码,它按预期工作:

示例输出表明代码在一次测试中产生了所需的输出。这并不等同于显示它按预期工作。

那个代码安全吗?

没有。在某些情况下可以使代码安全。首先,以适当的对齐方式声明它,例如static _Alignas(max_align_t) memory_pool[256 * 1024];。 (max_align_t&lt;stddef.h&gt; 中定义。)这使得指针转换部分定义。

其次,如果您使用 GCC 或 Clang 并请求 -fno-strict-aliasing,编译器会提供对 C 语言的扩展,以放宽 C 2018 6.5 7。或者,在某些情况下,可以从对编译器和链接器设计,即使违反 6.5 7,您的程序也能正常工作:如果程序在单独的翻译单元中编译,并且目标模块不包含类型信息或未使用花哨的链接时优化,并且在实现内存池的翻译单元,则违反 6.5 7 不会产生不利后果,因为 C 实现无法区分内存池方面违反 6.5 7 的代码与不违反 6.5 7 的代码。此外,您必须知道指针转换按预期工作,它们有效地生成指向相同地址的指针(而不仅仅是可以转换回原始指针值但不能直接用作指向同一内存的指针的中间数据) .

没有不良后果的推论是脆弱的,应谨慎使用。例如,在实现内存池的翻译单元中很容易意外违反 6.5 7,例如将指针存储在已释放的内存块中或将大小信息存储在已分配块之前的隐藏标头中。

【讨论】:

  • 很遗憾,标准的作者没有明确表示他们没有努力系统地确保标准定义了委员会不仅期望编译器以某种方式工作的每种情况下的行为, 但无法想象他们会这样做,并且在现有编译器会一致地处理构造的情况下,应该期望质量实现遵循先例,而没有记录和令人信服的理由偏离它。跨度>
【解决方案2】:

该标准有意避免要求所有实现都适合低级编程,但允许旨在用于低级编程的实现扩展语言以支持这种用法,方法是在比标准规定的更多情况下指定它们的行为。然而,即使使用为低级编程设计的编译器,使用字符数组作为内存池通常也不是一个好主意。然而,为了与最广泛的编译器和平台兼容,应该将内存池对象声明为具有最广泛对齐的类型的数组,或者包含具有最广泛对齐的类型的 long 字符数组的联合,例如

 static uint64_t my_memory_pool_allocation[(MY_MEMORY_POOL_SIZE+7)/8];
 void *my_memory_pool_start = my_memory_pool_allocation;

 union
 {
   unsigned char bytes[MY_MEMORY_POOL_SIZE];
   double alignment_force;
 } my_memory_pool_allocation;
 void *my_memory_pool_start = my_memory_pool_allocation.bytes;

请注意,clang 和 gcc 可以配置为通过使用-fno-strict-aliasing 标志以适合低级编程的方式扩展语言,并且商业编译器通常可以支持低级概念,例如内存池,即使使用 type-基于别名,因为他们将指针类型转换视为可能错误的基于类型的别名假设的障碍。

如果void* 被初始化为一个静态对象的地址,该对象的符号没有在其他上下文中使用,我认为任何普通的编译器都不会关心用于初始化的类型。在这里跳过箍来遵循标准是愚蠢的差事。当不使用-fno-strict-aliasing 时,clang 和 gcc 都不会处理标准规定的所有极端情况,而使用-fno-strict-aliasing,它们将扩展语言的语义以允许无论标准是否使用内存池都可以方便地使用是否要求他们这样做。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-04-11
    相关资源
    最近更新 更多