【问题标题】:C - Conversion behavior between two pointersC - 两个指针之间的转换行为
【发布时间】:2021-03-22 05:12:33
【问题描述】:

2020 年 12 月 11 日更新:感谢@“Some程序员老兄”在评论中提出的建议。 我的根本问题是我们的团队正在实现一个动态类型的存储引擎。我们分配了多个 16-aligned 的 char array[PAGE_SIZE] 缓冲区来存储动态类型的数据(没有固定的结构)。出于效率原因,我们无法执行字节编码或分配额外空间来使用memcpy

既然已经确定了对齐方式(即16),剩下的就是使用指针的强制转换来访问指定类型的对象,例如:

int main() {
    // simulate our 16-aligned malloc
    _Alignas(16) char buf[4096];

    // store some dynamic data:
    *((unsigned long *) buf) = 0xff07;
    *(((double *) buf) + 2) = 1.618;
}

但是我们的团队对这个操作是否是未定义的行为存在争议。


看过很多类似的问题,比如

但是这些和我对C标准的理解不同,我想知道是不是我的误解。

主要的困惑是关于 C11 的 6.3.2.3 #7 部分:

指向对象类型的指针可以转换为指向不同对象类型的指针。如果 结果指针 未正确对齐 68) 引用类型,则行为未定义。

68) 通常,“正确对齐”的概念是可传递的:如果指向类型 A 的指针正确对齐指向类型 B 的指针,而指向类型 B 的指针又正确对齐指向类型 C 的指针,则指向类型 A 的指针与指向类型 C 的指针正确对齐。

这里的结果指针是指指针对象还是指针值

在我看来,我认为答案是Pointer Object,但更多的答案似乎表明Pointer Value


解释A:指针对象

我的想法如下:指针本身就是一个对象。根据6.2.5 #28,不同的指针可能有不同的表示和对齐要求。因此,根据6.3.2.3 #7,只要两个指针具有相同的对齐方式,就可以安全地转换它们而没有未定义的行为,但不能保证它们可以被取消引用。 在程序中表达这个想法:

#include <stdio.h>

int main() {
    char buf[4096];

    char *pc = buf;
    if (_Alignof(char *) == _Alignof(int *)) {
        // cast safely, because they have the same alignment requirement?
        int *pi = (int *) pc; 
        printf("pi: %p\n", pi);
    } else {
        printf("char * and int * don't have the same alignment.\n");
    }
}

解释B:指针值

但是,如果 C11 标准讨论的是引用类型的 指针值 而不是 指针对象。上面代码的对齐检查是没有意义的。 在程序中表达这个想法:

#include <stdio.h>

int main() {
    char buf[4096];

    char *pc = buf;
    
    /*
     * undefined behavior, because:
     * align of char is 1
     * align of int is 4
     * 
     * and we don't know whether the `value` of pc is 4-aligned.
     */
    int *pi = (int *) pc;
    printf("pi: %p\n", pi);
}

哪种解释是正确的?

【问题讨论】:

  • 所有普通指针大小相同。 sizeof(int*) == sizeof(char*)。因此,所有指针都将具有相同的对齐方式:_Alignof(int*) == _Alignof(char*),因为对齐方式取决于大小。
  • @MartinYork 不正确。数组的对齐方式是基本对象类型的对齐方式,因此buf 的对齐要求仅为 1,而与数组的大小无关。
  • @MartinYork “所有普通指针的大小都相同。” --> 对象指针和函数指针通常可以有不同的大小。各种对象指针的大小可能不同,但如今这种情况很少见。
  • @ryyker 我指的是 OP 引用的措辞。我曾经和dbush的回答类似,但是再看引用的部分我觉得不是很明显。
  • @RichardBryant:不。我指出检查是没有意义的(因为它总是正确的)。 char* x = 0x01; 有效。 int* y = (int*)x; 无效。指针 x/y 具有相同的对齐方式,您可以分配给它们。但是分配非法值是UB。解释B是正确的。

标签: c pointers language-lawyer strict-aliasing


【解决方案1】:

解释 B 是正确的。该标准谈论的是指向对象的指针,而不是对象本身。 “结果指针”是指强制转换的结果,并且强制转换不会产生左值,所以它是指强制转换后的指针值。

以您的示例中的代码为例,假设int 必须在 4 字节边界上对齐,即它的地址必须是 4 的倍数。如果buf 的地址是0x1001,则转换该地址到int * 无效,因为指针值未正确对齐。如果buf的地址为0x1000,则转换为int *有效。

更新:

您添加的代码解决了对齐问题,因此在这方面没问题。然而,它有一个不同的问题:它违反了严格的别名。

您定义的数组包含char 类型的对象。通过将地址转换为不同的类型并随后取消引用转换后的类型,您可以将一种类型的对象作为另一种类型的对象来访问。这是 C 标准所不允许的。

虽然标准中没有使用术语“严格别名”,但该概念在第 6.5 节第 6 和第 7 段中有所描述:

6对象的有效类型是为了访问其存储的值是对象的声明类型,如果有的话。87) 如果一个 值通过 lvalue 的类型不是字符类型,然后是 左值成为该访问的对象的有效类型 以及不修改存储值的后续访问。如果一个 使用memcpy 将值复制到没有声明类型的对象中 或memmove,或复制为字符类型数组,则 被修改对象的有效类型,用于该访问和 不修改值的后续访问是有效类型 从中复制值的对象的名称(如果有的话)。对所有人 对没有声明类型的对象的其他访问,有效 对象的类型只是用于 访问。

7 对象的存储值只能由具有以下类型之一的左值表达式访问:88)

  • 与对象的有效类型兼容的类型,
  • 与对象的有效类型兼容的类型的限定版本,
  • 对应于对象有效类型的有符号或无符号类型,
  • 一种有符号或无符号类型,对应于对象有效类型的限定版本,
  • 在其成员中包含上述类型之一的聚合或联合类型(递归地,包括 子聚合或包含联合),或
  • 一种字符类型。

...

87 ) 分配的对象没有声明类型。

88 ) 此列表的目的是指定在哪些情况下, 对象可能有别名,也可能没有别名。

在您的示例中,您在 char 对象之上编写了 unsigned longdouble。这两种类型都不满足第 7 段的条件。

除此之外,这里的指针算术无效:

 *(((double *) buf) + 2) = 1.618;

当您将buf 视为double 的数组时,它不是。至少,您需要直接对buf 执行必要的算术运算并在最后转换结果。

那么为什么这是char 数组而不是malloc 返回的缓冲区的问题?因为从malloc 返回的内存没有有效类型,直到你在其中存储一些东西,这就是第 6 段和脚注 87 所描述的。

所以从严格的标准角度来看,您所做的是未定义的行为。但是根据您的编译器,您可能能够禁用严格别名,因此这将起作用。如果您使用 gcc,则需要传递 -fno-strict-aliasing 标志

【讨论】:

  • 感谢更新!我了解基本的严格别名,因此我在代码中注释 simulate our 16-aligned malloc 以指出它是分配对象。但是我想问一下,一旦分配的对象有了有效的类型,把它转换成另一种类型是违法的吗(我们想在不需要旧数据的情况下重用缓冲区)?
  • @RichardBryant 根据第 6 段,我认为这是可以接受的。关键短语是,一旦存储了一个值,“左值的类型将成为该访问的对象的有效类型,对于不修改存储值的后续访问”。所以存储一个新值可以改变类型。
  • @dbush:存储一个 new 值(存储从未保存的位模式)会改变有效类型,但在由 clang 和 gcc 处理的-fstrict-aliasing 方言中, 如果一个存储区域用于保存具有特定位模式的 T1,则使用该位模式写入 T2 可能会导致在编译器空闲时将有效类型设置为 T1 或 T2。
【解决方案2】:

该标准不要求实现考虑代码将观察到 T* 中的值与类型 T 不对齐的可能性。例如,在 clang 中,当针对具有“更大”加载/存储指令的平台时不支持未对齐访问,将指针转换为不满足对齐的类型,然后在其上使用memcpy 可能会导致编译器生成代码,如果指针未对齐,即使memcpy 也会失败否则它本身不会强加任何对齐要求。

以 ARM Cortex-M0 或 Cortex-M3 为目标时,例如,给定:

void test1(long long *dest, long long *src)
{
    memcpy(dest, src, sizeof (long long));
}
void test2(char *dest, char *src)
{
    memcpy(dest, src, sizeof (long long));
}
void test3(long long *dest, long long *src)
{
    *dest = *src;
}

clang 将为 test1 和 test3 生成代码,如果 srcdest 未对齐,则会失败,但对于 test2,它将生成更大更慢的代码,但支持任意对齐源和目标操作数。

可以肯定的是,即使在铿锵声中,将未对齐的指针转换为long long* 本身通常也不会导致任何奇怪的事情发生,但事实上这样的转换会产生免除编译器的 UB处理test1 中未对齐的指针大小写的任何责任。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-10-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-24
    • 1970-01-01
    • 1970-01-01
    • 2012-12-17
    相关资源
    最近更新 更多