【问题标题】:Is void** an exception to strict aliasing rules?void** 是严格别名规则的例外吗?
【发布时间】:2021-12-22 01:04:18
【问题描述】:

基本上,启用严格别名后这段代码合法吗?

void f(int *pi) {
    void **pv = (void **) π
    *pv = NULL;
}

这里,我们通过另一种类型的指针(指向void *的指针)访问一种类型的对象(int*),所以我想说这确实是一个严格的别名违规。

但是试图突出未定义行为的示例让我怀疑(即使它不能证明它是合法的)。

首先,如果我们给int *char *加上别名,我​​们可以根据优化级别得到不同的值(所以肯定是违反严格别名的):

#include <stdio.h>

static int v = 100;

void f(int **a, char **b) {
    *a = &v;
    *b = NULL;
    if (*a)
        // *b == *a (NULL)
        printf("Should never be printed: %i\n", **a);
}

int main() {
    int data = 5;
    int *a = &data;
    f(&a, (char **) &a);
    return 0;
}
$ gcc a.c && ./a.out
$ gcc -O2 -fno-strict-aliasing a.c && ./a.out
$ gcc -O2 a.c && ./a.out
Should never be printed: 100

但是使用void ** 而不是char ** 的相同样本不会表现出未定义的行为:

#include <stdio.h>

static int v = 100;

void f(int **a, void **b) {
    *a = &v;
    *b = NULL;
    if (*a)
        // *b == *a (NULL)
        printf("Should never be printed: %i\n", **a);
}

int main() {
    int data = 5;
    int *a = &data;
    f(&a, (void **) &a);
    return 0;
}
$ gcc a.c && ./a.out
$ gcc -O2 -fno-strict-aliasing a.c && ./a.out
$ gcc -O2 a.c && ./a.out

只是偶然吗?还是void ** 的标准中有明确的例外?

或者也许只是编译器处理void **,因为实际上(void **) &amp;a 在野外太常见了?

【问题讨论】:

  • 没有什么可以打破严格的别名规则。您分配的指针不通过不兼容的引用引用数据。 NULL 适用于任何指针类型。顺便说一句,您在通过应用强制转换调用函数时使警告静音
  • 但是如果我们使用char ** 而不是void **,我们会表现出未定义的行为。这里,数据是int *(不是int),我们通过指向void *的指针访问它,所以我们通过不兼容的(?)指针访问它。
  • @0___________ 这些是指针变量本身的严格别名违规行为,而不是指向的数据。

标签: c language-lawyer undefined-behavior strict-aliasing


【解决方案1】:

基本上,启用严格别名后这段代码是否合法?

没有。 pi 的有效类型是int*,但您通过void* 左值访问指针变量。取消引用指针以提供与对象的有效类型不对应的访问权限是严格的别名违规 - 除了某些例外,这不是一个。

在您的第二个示例中,函数的两个参数都设置为指向有效类型 int* 的对象,这是在此处完成的:f(&amp;a, (char **) &amp;a);。因此,函数内部的*b 确实是一个严格的别名违规,因为您使用char* 类型进行访问。

在您的第三个示例中,您执行相同操作,但使用 void*。这也是一个严格的混叠违规。在这种情况下,void*void** 没有什么特别之处。

为什么您的编译器在某些情况下会表现出某种形式的未定义行为,推测起来意义不大。尽管void* 根据定义必须可以与任何其他对象指针类型相互转换,因此它们很可能在内部具有表示形式,即使这不是标准中的明确要求。

您还使用了-fno-strict-aliasing,它会关闭 各种基于指针别名的优化。如果您希望引起奇怪和意外的结果,则不应使用该选项。

【讨论】:

  • 我认为将指针转换为 (char *) 并不违反严格的别名规则; char ** 有什么不同吗?这真的是一个严格的别名规则问题吗?或者更多,不能保证的指令顺序?
  • @GuillaumePetitjean 在此代码中的任何地方都没有对char* 的强制转换。如果是这样,则与char* 相关的严格别名规则的例外仅适用于您取消引用它们并将左值作为字符类型访问,以获得原始二进制表示。仅仅因为字符类型左值有一个例外,所以假设取消引用一些不相关的char** 类型会有一个例外是没有意义的。
【解决方案2】:

是的,void *char * 是特殊的。

void** 是严格别名规则的例外吗?

您没有通过void ** 类型进行别名;您通过void * 别名。在*pv = NULL中,*pv的类型是void *

通常,C 标准允许不同类型的指针具有不同的表示形式。它们甚至可以有不同的尺寸。但是,它需要一些指针类型具有相同的表示。 C 2018 6.2.5 28 说[为清楚起见,我将其分成项目符号]:

  • 指向void 的指针应具有与指向字符类型的指针相同的表示和对齐要求。49)
  • 同样,指向兼容类型的合格或不合格版本的指针应具有相同的表示和对齐要求。
  • 所有指向结构类型的指针都应具有相同的表示和对齐要求。
  • 所有指向联合类型的指针都应具有相同的表示和对齐要求。
  • 指向其他类型的指针不必具有相同的表示或对齐要求。

脚注 49 说:

相同的表示和对齐要求意味着作为函数的参数、函数的返回值和联合成员的可互换性。

注释不是标准规范部分的一部分。也就是说,它没有形成实现必须遵守的规则。但是,该注释似乎告诉我们,无论正式规则如何,您都应该能够在某些地方使用void * 代替char *,反之亦然。说两件事应该可以互换看起来像是一条规则。我的解释是,本文的作者打算将void *char * 至少在某种程度上可以互换,但没有适合放入C 标准规范部分的正式措辞。事实上C标准对别名的处理存在缺陷,比如this one,所以C标准确实需要重写规则。

因此,尽管这不是标准的规范部分,但编译器开发人员可能会尊重它并支持将 char *void * 别名化,反之亦然。这可以解释为什么您看到 char * 的别名表现得好像它受支持,而 int * 的别名不支持。

【讨论】:

  • 二进制兼容性与类型兼容性不同。 6.2.7 compatible types 将我们带到 6.7.6.1,这很清楚:“对于两个兼容的指针类型,两者都应具有相同的限定,并且都应是指向兼容类型的指针。” charvoid 不兼容。
  • @Lundin:是的,二进制兼容性与类型兼容性不同,但注释告诉我们不仅仅是二进制兼容性。规范文本说存在二进制兼容性,注释更进一步告诉我们存在可互换性:在某些情况下,void * 可以替代char *,反之亦然。该说明旨在提供一些规范性文本未能表达的可互换性。
【解决方案3】:

虽然char*void* 需要具有匹配的表示,但某些平台对int* 使用不同的表示。因此,任何依赖于使用取消引用的void** 来互换访问所有指针类型的代码都不能移植到此类机器上,并且从标准的角度来看是“不可移植的”。因此,该标准放弃了对任何特定实现是否应支持此类构造的管辖权。这样做的实现将比那些不这样做的更适合低级编程,因此设计和配置为适合该目的的高质量实现将这样做。但是请注意,clang 和 gcc 都不是特别适合低级编程,除非使用 -fno-strict-aliasing 标志。

为了阐明为什么平台可能对int*char* 使用不同的表示,一些硬件平台不允许以小于16 位的块直接寻址内存。该标准将允许此类平台的编译器以多种方式存储事物,在性能、存储效率和与期望 char 为 8 位的代码的兼容性之间进行不同的权衡:

  1. 只需使char 与最小的直接存储单元的大小相匹配(例如,使charint 均为16 位)。我使用了一个可以做到这一点的编译器。这种方法可能会提供最佳性能,但使用 unsigned char 的大型数组来保存八位字节的代码会浪费其中一半的存储空间。

  2. 在每个char 中存储 8 位有用数据,其余 8 位未使用。存储分成两个字的 16 位值,以及分成四个字的 32 位值。这将提供出色的兼容性,但性能和存储效率都很差。

  3. char* 实现为指向 16 位字的指针、指示应识别字的哪一半的位和 15 个填充位的组合,但将 int* 实现为简单的指向 16 位的指针-位字。

  4. 如上实现char*,但在int* 中添加一个填充字节。这会提高兼容性,但会浪费一些存储空间。

没有一种方法最适合所有应用程序,但该标准允许实现选择对其客户最有用的一种或多种方法(可能通过命令行开关选择)。

【讨论】:

  • "某些平台对 int* 使用不同的表示形式。"有什么例子吗?为什么?有什么好处?
  • @rom1v:见编辑。
猜你喜欢
  • 2015-10-15
  • 2017-02-25
  • 2014-08-27
  • 2016-09-12
  • 2015-01-16
  • 2013-03-11
  • 2018-12-14
  • 2015-05-31
相关资源
最近更新 更多