C - 两个指针之间的转换行为

C - Conversion behavior between two pointers

2020-12-11更新:感谢@“某位程序员老兄”在评论中提出的建议。 我的潜在问题是我们的团队正在实施动态类型存储引擎。我们使用 16-aligned 分配多个 char array[PAGE_SIZE] 缓冲区来存储动态类型的数据(没有固定结构)。出于效率原因,我们无法执行字节编码或分配额外的 space 来使用 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 部分:

A pointer to an object type may be converted to a pointer to a different object type. If the resulting pointer is not correctly aligned 68) for the referenced type, the behavior is undefined.

68) In general, the concept ‘‘correctly aligned’’ is transitive: if a pointer to type A is correctly aligned for a pointer to type B, which in turn is correctly aligned for a pointer to type C, then a pointer to type A is correctly aligned for a pointer to type C.

这里的resulting pointer是指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 标准谈论的是 Pointer Value 引用类型而不是 Pointer Object。上面代码的对齐检查是没有意义的。 在程序中表达这个想法:

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

哪种解释是正确的?

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

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

更新:

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

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

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

6 The effective type of an object for an access to its stored value is the declared type of the object, if any.87) If a value is stored into an object having no declared type through an lvalue having a type that is not a character type, then the type of the lvalue becomes the effective type of the object for that access and for subsequent accesses that do not modify the stored value. If a value is copied into an object having no declared type using memcpy or memmove, or is copied as an array of character type, then the effective type of the modified object for that access and for subsequent accesses that do not modify the value is the effective type of the object from which the value is copied, if it has one. For all other accesses to an object having no declared type, the effective type of the object is simply the type of the lvalue used for the access.

7 An object shall have its stored value accessed only by an lvalue expression that has one of the following types:88)

  • a type compatible with the effective type of the object,
  • a qualified version of a type compatible with the effective type of the object,
  • a type that is the signed or unsigned type corresponding to the effective type of the object,
  • a type that is the signed or unsigned type corresponding to a qualified version of the effective type of the object,
  • an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union), or
  • a character type.

...

87 ) Allocated objects have no declared type.

88 ) The intent of this list is to specify those circumstances in which an object may or may not be aliased.

在您的示例中,您正在 char 对象之上编写一个 unsigned long 和一个 double。这两种类型都不满足第7段的条件。

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

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

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

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

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

标准不要求实现考虑代码在 T* 中观察到与类型 T 不对齐的值的可能性。例如,在 clang 中,当针对“更大”的平台时load/store 指令不支持未对齐访问,将指针转换为不满足其对齐方式的类型,然后在其上使用 memcpy 可能会导致编译器生成代码,如果指针不对齐,该代码将失败t 对齐,即使 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,它将生成更大更慢的代码,但这将支持源操作数和目标操作数的任意对齐。

可以肯定的是,即使在 clang 上,将未对齐的指针转换为 long long* 的行为本身通常不会导致任何奇怪的事情发生,但事实是这样的转换会产生 UB这免除了编译器处理 test1.

中未对齐指针情况的任何责任