为什么 C 标准(无论最新的是什么)会因为严格的别名而禁止该程序?

ano*_*ard 7 c strict-aliasing

我很清楚 C 标准禁止(没有定义)这个程序的行为,但不清楚为什么它必须这样。为什么别名规则使得人们无法编写这个程序?

#include<stdio.h>
#include<string.h>
#include<stdint.h>
#include<stdalign.h>

#define SIZE 512

unsigned char buffer[SIZE];
size_t free_slot = 0;

void* alloc(const size_t bytes, const size_t alignment)
{
   
     const uintptr_t start = (uintptr_t)(buffer+free_slot);
     const size_t adjust = (size_t)(start % alignment);
     const size_t placement = free_slot + adjust;
     const size_t next_free_slot = placement + bytes;

     printf("start=%ld\n",start);
     printf("adjust=%ld\n",adjust);
     printf("placement=%ld\n",placement);
     printf("next_free_slot=%ld\n",next_free_slot);
   

   if(SIZE < next_free_slot) return NULL;
   
   free_slot = next_free_slot;

   return buffer+placement;
   

}

struct thing {
  uint64_t x;
  uint64_t y;
};


int main()
{
  int* p1 = alloc(sizeof(int),alignof(int));
  printf("--------------\n");
  printf("alignof(struct thing)=%ld\n",alignof(struct thing));
  printf("--------------\n");
  struct thing* p2 = alloc(sizeof(struct thing),alignof(struct thing));

  *p1 = 143;
  memcpy(p2,&(struct thing){1,2},sizeof(struct thing));

  printf("%d\n",*p1);
  printf("%ld\n",p2->x);

  return 0;

}
Run Code Online (Sandbox Code Playgroud)

是否有可能修改标准以允许这样的计划,或者这是一个无望的努力?

Lun*_*din 8

我很清楚 C 标准禁止这个程序

不是真的,它没有涵盖如果您从字符数组中将双关语输入到结构中会发生什么 - 这是未定义的行为,因为它违反了 C17 6.5/7 中的“应”,但不是约束。

关于所有“严格的别名很糟糕,我说得对吗?” 咆哮...是和否。这些规则的最初目的是禁止疯狂的皈依。C99 基本原理 5.10 第 6.5/35 章显示了以下示例:

int a;
void f(int * b)
{
  a = 1;
  *b = 2;
  g(a);
}  
Run Code Online (Sandbox Code Playgroud)

很容易生成对 g 的调用,就好像源表达式是g(1),但b可能指向a,因此这种优化并不安全。另一方面,考虑

int a;
void f( double * b )
{
  a = 1;
  *b = 2.0;
  g(a);
}
Run Code Online (Sandbox Code Playgroud)

b同样,只有当指向 时,优化才是不正确的a。然而,只有当 a 的地址被强制转换为 时,才会发生这种情况double*。C89 委员会决定不允许这种可疑的可能性。

这是最初的基本原理,C99 通过引入effective type稍微扩展了 C89 的不明确规则,无论好坏。规则还很不清楚,但初衷是让编译器不必做出上述奇怪的假设。到目前为止,应该允许编译器做出一个完全合理的假设。

不幸的是,在 2000 年代初的某个时候,一些编译器(最著名的是 gcc)决定滥用这一点来执行优化。突然间你就不能做类似的事情了uint8_t arr[2]; ... *(uint16_t*)arr,因为严格来说这是严格的别名违规。直到 C99 编译器在没有此类优化的情况下生成了合理的代码,但在 C99 之后,有些编译器选择了失控。多年来,情况有所改善,但我们仍然不能依赖编译器在uint16_t*上面的小转换中生成“预期的”代码。

C17 6.5/7 中严格别名规则的例外情况数量还有很多不足之处。例如,在各种无符号整数类型之间输入双关语是完全明智的——任何做过硬件相关编程的人都明白这一点。但这是不允许的。

作为另一个例子,没有提到类型限定符会发生什么 - 全世界似乎没有人能够回答这个问题:有效类型限定符有什么规则?- 我自己也不知道有什么规则。

目前还不清楚如何使用与有效类型相关的数组......这样的例子不胜枚举。有许多关于这些规则的各种细节的缺陷报告,但它们还没有得到改进。


至于您的程序是否包含任何严格的别名违规以及如何修复它:

  • unsigned char buffer[SIZE];具有有效类型 (array of) unsigned char。
  • const uintptr_t start = (uintptr_t)(buffer+free_slot);假设您最终不会出现错位,那很好,但这是一个单独的问题。
  • 当您从调用方取消引用指针并进行左值访问int或结构类型等时,会出现严格的别名冲突,因为这不是列表 6.5/7 中允许的异常之一。相反,从更大的类型开始并使用字符类型指针逐字节访问就可以了。

因此,要修复它,您必须制作类似的东西,例如int:

typedef union
{
  int i;
  unsigned char bytes[sizeof(int)];
} intalias_t;
Run Code Online (Sandbox Code Playgroud)

现在你可以这样做:

intalias_t* p1 = alloc(sizeof(int),alignof(int));
(*p1).i = 143; // well-defined
Run Code Online (Sandbox Code Playgroud)

因为(*p1).i“左值表达式”是“包含”“与对象的有效类型兼容的类型”的聚合或联合类型。也就是说,联合包含一个字符类型数组,该数组(据说)与也是字符类型的有效类型兼容。“据说”是因为在数组访问方面规则很混乱。如果您的原始数组或联合中的数组包含类型限定符,则没有人知道(?)会发生什么。

如有疑问/根据经验,请使用-fno-strict-aliasing.


Joh*_*ger 5

为什么别名规则使得人们无法编写这个程序?

迄今为止发布的 ISO C 的每个版本中都存在严格别名规则,它并不是说您不能编写该程序,甚至 C 实现也不能接受该程序并按照您想要的效果执行该程序。相反,这是规范认为程序虽然在语法上正确并满足(我认为)所有语言约束的相对较多的地方之一,但具有未定义的行为。

在某些情况下,规范会出于多种原因使程序行为未定义,或者在本例中明确指定其未定义。在严格别名规则的情况下,C99 的基本原理文档(该规范的最新版本没有此类文档)说明了这一决定:

可用于访问对象的左值类型已受到限制,因此优化器不需要做出最坏情况的别名假设

(第 59 页)

完整的讨论内容太多,无法在此处引用(它比文档的整页多一点),但您可能会发现它很有趣。

是否有可能修改标准以允许这样的计划,或者这是一个无望的努力?

ISO 有一个工作组致力于维护语言规范,并且不时发布修订版本。那么原则上来说,这样的改变是可能的。在实践中,这种特殊的改变是否会被接受值得怀疑,因为它会以相对较小的收益产生广泛的影响。


dbu*_*ush 5

正如其他人所提到的,严格的别名允许进行某些优化。鉴于这些优化很有用,标准委员会不太可能删除它。

话虽这么说,特定的实现确实有解决这个问题的方法。特别是,gcc 具有该malloc属性。来自海湾合作委员会文档:

malloc
malloc (deallocator)
malloc (deallocator, ptr-index)
Run Code Online (Sandbox Code Playgroud)

属性 malloc 表明函数是类似 malloc 的,即函数返回的指针 P 不能与函数返回时任何其他有效指针别名,而且在 P 寻址的任何存储中都不会出现指向有效对象的指针。此外, GCC 预测具有该属性的函数在大多数情况下返回非空。

独立地,具有一个或两个参数的属性的形式将 deallocator 作为从类似 malloc 的函数返回的指针的合适的释放函数相关联。ptr-index 表示位置参数,当在调用deallocator 中将指针传递到该位置参数时,会产生释放它的效果。

因此,如果您使用 gcc 进行编译并像这样声明您的函数:

void* alloc(const size_t bytes, const size_t alignment) __attribute__((malloc))
Run Code Online (Sandbox Code Playgroud)

然后您可以安全地使用返回的内存,就像它是由 所返回一样malloc,并且严格别名仍然可以在程序的其他地方使用。


经过进一步思考,考虑到多个编译器支持某种类型的属性,标准化其中许多属性以使应用程序开发人员能够更好地控制代码的编译方式是有意义的。鉴于 C 在历史上一直被视为“便携式汇编程序”,但标准已导致与此的分歧,因此在标准中提供对此类低级行为的支持可能会受到欢迎。


And*_*rew 3

是否有可能修改标准以允许这样的计划,或者这是一个无望的努力?

C 标准 (ISO/IEC 9899) 由 ISO/IEC JTC1/SC22/WG14 维护 - 任何人都可以参与;提名是通过适当的国家机构(例如英国的 BSI)进行的

一旦成为工作组的参与者,您就可以自由提交更改标准的提案...如果您能在工作组内获得足够的支持,它将包含在下一个草案中。

但任何此类提案都需要解决所有后果、副作用或任何不需要的结果 - 请记住 WG14 的首要指令是“不要破坏现有代码”(无论该代码已经被破坏得多么严重)。

然后,草案将经历正式批准流程——在委员会草案上进行正式投票,然后是国际标准草案,最后是国际标准最终草案。

因此,如果你有充分的理由支持变革,那么就有可能改变标准;但是,我对改变这一点不抱太大希望!

免责声明:我是 WG14 的英国代表(以及 MISRA 联络人)