【问题标题】:Effective type rules with relation to strict aliasing与严格别名相关的有效类型规则
【发布时间】:2020-08-01 11:29:58
【问题描述】:

所以,在过去的几天里,我一直在反对严格的别名规则和有效的类型规则。虽然它的精神很清楚,但我想确定对规则的良好技术理解。请注意,我已经解决了许多关于 SO 的相关问题,但我认为这里提出的问题并没有以在任何其他地方真正与我同在的方式得到回答。

这个问题分为两部分。

在第一部分,我将有效的类型规则分成句子,并解释我自己对每个句子的理解。对于其中的每一个,如果它是正确的,请验证我的理解,或者如果它有缺陷,请纠正我并解释它的原因。对于最后一个“句子”,我还提出了两个问题,希望得到答案。

问题的第二部分涉及我对特区的理解。

第 1 部分:有效的类型规则

第 1 句

访问其存储值的对象的有效类型是对象的声明类型,如果有的话。

这很清楚 - 一个声明的对象,如 int x 具有永久有效类型,即声明它的类型(在本例中为 int)。

第二句

如果通过具有非字符类型类型的左值将值存储到没有声明类型的对象中,则左值的类型将成为该访问以及后续访问的有效类型不修改存储的值。

“没有声明类型的对象”通常是动态分配的对象。

当我们将数据存储在分配的对象中时(无论它是否已经具有有效类型),对象的有效类型将成为用于访问数据以进行存储的左值的类型(除非左值是字符类型)。比如:

int* x = malloc(sizeof(int)); // *x has no effective type yet
*x = 10; // *x has effective type int, because the type of lvalue *x is int

还可以更改已经具有有效类型的对象的有效类型。例如:

float* f = (float*) x;
*f = 20.5; // *x now has effective type float, because the type of lvalue *f is float.

第 3 句

如果使用 memcpy 或 memmove 将值复制到没有声明类型的对象中,或者复制为字符类型的数组,则该访问的修改对象的有效类型以及不修改该访问的后续访问value 是从中复制值的对象的有效类型(如果有的话)。

这意味着当我们给一个分配的对象设置一个值时,如果该值是通过一个与char*兼容类型的左值(或通过memcpymemmove)设置的,那么该对象的有效类型就变成了复制到其中的数据的有效类型。例如:

int* int_array = malloc(sizeof(int) * 5); // *int_array has no effective type yet
int other_int_array[] = {10, 20, 30, 40, 50};
char* other_as_char_array = (char*) other_int_array;
for (int i = 0; i < sizeof(int) * 5; i++) {
    *((char*) int_array + i) = other_as_char_array[i];
}
// *int_array now has effective type int

第 4 句

对于没有声明类型的对象的所有其他访问,对象的有效类型只是用于访问的左值的类型。

关于这部分我有两个问题:

A.通过“对于所有其他访问”,文本是否仅仅意味着“对于所有读取访问”?

在我看来,之前所有引用未声明类型对象的规则都只处理存储一个值。那么这仅仅是针对未声明类型的对象(可能已经或可能没有有效类型)的任何 read 操作的规则吗?

B.内存中的特定对象只有一种有效类型。那么 - “对于所有其他访问”的文本是什么意思......这不是访问的问题,而是对象的客观有效类型的问题。不是吗?请澄清文本的语言。

第 2 部分:关于严格别名的问题

严格的别名规则描述是这样开始的(强调我的):

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

当文本显示“已访问存储值”时 - 是指读写访问,还是仅读取?

作为问这个问题的另一种方式:以下代码是否构成严格别名违规或合法?

int* x = malloc(sizeof(int)); // *x - no effective type yet
*x = 8; // *x - effective type int
printf("%d \n", *x); // access the int object through lvalue *x

float* f = (float*) x; // casting itself is legal
*f = 12.5; // effective type of *x changes to float - *** is this a SAR violation? ***
printf("%g \n", *f); // access the float object through lvalue *f

【问题讨论】:

    标签: c language-lawyer strict-aliasing


    【解决方案1】:

    “访问”表示读或写。 “对于所有其他访问”是指该段落中尚未涵盖的任何访问。回顾一下,对未声明类型的对象的访问是:

    • 一个值通过 具有非字符类型的左值,
    • 不修改存储值的后续访问
    • 一个值被复制到一个没有声明类型的对象中 memcpy 或 memmove
    • 或被复制为字符类型数组

    所以剩下的“全部读写”的情况是:

    • 通过具有字符类型类型的左值将值存储到没有声明类型的对象中,
    • 其他我们没有想到的写法

    在第 2 部分中,根据 C11 的文本,代码是正确的:

    如果一个值通过 左值的类型不是字符类型,则左值的类型变为 该访问的对象的有效类型

    *x = 8; 通过具有非字符类型类型的左值将值存储到没有声明类型的对象中。所以这个访问的对象的有效类型是int,然后在6.5/7我们有有效类型int的对象被int类型的左值访问。同样的道理也适用于*f = 20.5,使用float 而不是int

    脚注:有很多理由相信 6.5/6 和 /7 的文本有缺陷,正如您在搜索有关该主题的其他问题中所看到的那样。人们(和编译器编写者)形成了他们自己对规则的解释。

    【讨论】:

    • 我明白了。让我验证我的理解:我认为我们同意 SAR 处理对通过特定左值访问存储值的行为的限制(“一个对象的存储值只能由具有以下条件之一的左值表达式访问)以下类型:...")。而且,我对您在实践中的写作的理解是,通过我们想要的任何左值设置存储值始终是合法的,就其本身而言。这种访问总是将对象的有效类型设置为左值的类型,或者设置为被复制数据的类型,无论它是否[...]
    • [...] 已经有一个有效的类型。这将是所有不修改值的后续访问(即读取访问)的有效类型。因此,如果我理解正确,任何存储访问,即赋值、复制等都会碰巧将底层对象的有效类型设置为恰好与当前使用的左值匹配的类型,只要特区很担心。如果是这样 - 您是否同意 SAR 仅与读取特定对象的访问相关? IE。 - 通过进行特定的分配或复制是不可能违反 SAR 的。只有 [...] 才有可能违反 SAR
    • [...] 通过与底层有效类型不兼容的左值来解除对 reading 内容的引用。我的理解正确吗?
    • @AvivCohn 对于没有声明类型的对象,是的
    • Strict Aliasing 上的 SO 标签引用 Mike Acton 的一篇文章,定义 Strict Aliasing 如下:“Strict aliasing 是由 C(或 C++)编译器做出的一个假设,即取消引用指向不同类型对象的指针永远不会引用相同的内存位置(即互为别名)。”。我现在的理解是,根据 SAR,两个不同类型的指针相互别名肯定是可以的,即使在代码的同一区域,如上所示。这只是一个问题,如果我错了,请纠正我,[...]
    【解决方案2】:

    据我所知,委员会成员之间从未就“有效类型”规则在所有极端情况下的含义达成共识;任何合理的解释要么禁止应该是有用的优化,要么不能适应应该是可用的构造,或者两者兼而有之。据我所知,几乎没有像 clang 和 gcc 那样“严格”的编译器能够以与标准的任何合理解释一致的方式正确处理规则提出的所有极端情况。

    struct s1 { char x[1]; };
    struct s2 { char x[1]; };
    
    void convert_p_to_s1(void *p)
    {
        int q = ((struct s2*)p)->x[0]+1;
        ((struct s1*)p)->x[0] = q-1;
    }
    
    int test(struct s1 *p1, struct s2 *p2)
    {
        p1->x[0] = 1;
        p2->x[0] = 2;
        convert_p_to_s1(p1);
        return p1->x[0];
    }
    

    clang 和 gcc 都不允许 test 可能将 struct s1 的成员 x[0] 写入某个位置,然后使用 struct s2 的成员 x[0] 写入同一位置,然后使用读取x[0]struct s2,使用 x[0]struct s1 写入,然后使用 struct s1x[0] 读取,所有读取和写入都通过取消引用指针类型 char* 执行,并且每个读取从结构指针派生的左值,然后通过以相同方式从相同类型的指针派生的左值写入该存储。

    在 C99 之前,几乎普遍认为质量实现应避免以对客户有害的方式应用类型访问规则,而不考虑标准是否需要此类限制。因为一些实现用于需要能够以奇怪的方式访问对象但不需要花哨优化的目的,而另一些实现用于不需要以棘手的方式访问存储但需要更多优化的目的,所以问题实现应该认识到对一个对象的访问可能会影响另一个对象的确切时间被留作实现质量问题。

    然而,一些 C99 的作者可能反对这样一个事实,即规则实际上并不要求实现支持所有实现都应该支持的构造,事实上几乎所有实现都已经支持。为了解决他们认为的缺陷,他们添加了一些额外的规则,这些规则将强制支持他们认为所有实现都应该支持的一些构造,并且故意不强制支持一些不应该需要通用支持的构造。然而,他们似乎没有做出任何重大努力来考虑极端情况以及规则是否会明智地处理它们。

    如果作者愿意承认某些任务比其他任务需要更强的保证,并且应该期望针对不同类型任务的实现应该支持适当的不同保证那些任务。否则,应将 C 视为两个方言家族——其中一个要求使用特定类型访问的任何存储在其生命周期内永远不会被任何其他类型访问,其中一个识别对目标的操作从另一种类型的指针新派生的指针可能会影响由原始指针标识的对象。

    【讨论】:

      猜你喜欢
      • 2013-10-01
      • 2019-01-29
      • 2015-10-15
      • 2017-02-25
      • 1970-01-01
      • 2020-02-08
      • 1970-01-01
      • 2021-08-08
      • 2013-03-11
      相关资源
      最近更新 更多