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



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

#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;
}

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

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总是足够大。

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

否。C 2018 6.5 7中的规则规定,定义为char数组的对象可以访问为:

  1. 是一种与CCD_ 4阵列兼容的类型
  2. 与CCD_ 5的阵列兼容的类型的合格版本
  3. 是与CCD_ 6的阵列相对应的有符号或无符号类型的类型
  4. 是与CCD_ 7的数组相对应的有符号或无符号类型的类型
  5. 在其成员中包括char数组的聚合或并集类型,或者
  6. 字符类型

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

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

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

该代码安全吗?

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

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

没有不良后果的推论是脆弱的,应该谨慎使用。例如,在实现存储器池的转换单元中,通过将指针存储在释放的存储器块中或者通过将大小信息存储在分配的块之前的隐藏报头中,很容易意外地违反6.57。

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

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标志以适合低级别编程的方式扩展语言,并且商业编译器通常可以支持低级别概念,如内存池,即使在使用基于类型的别名时也是如此,因为它们将指针类型转换识别为可能错误的基于类型的混叠假设的障碍。

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

相关内容

最新更新