使用void指针介入时是否出现严格别名

Does strict aliasing occur when using void pointer intervening

下面的例子是否违反了严格的别名规则?

在文件中a.c

extern func_takes_word(uint32_t word);

void func(void *obj, size_t size_in_words)
{
    for (int i = 0; i < size_in_words; i++)
        func_takes_word(*(((uint32_t *)obj)+i)); // <--- Here
}

在文件中 b.c

struct some_struct
{
    uint32_t num_0;
    uint32_t num_1;
    uint32_t num_2;
};

extern void func(void *obj, size_t size_in_words);

void some_func(void)
{
    struct some_struct stc = {0, 1, 59}; // assume no padding
    func((void *)&stc, sizeof(struct some_struct)/sizeof(uint32_t));
}

可以说存在违规,因为我将 struct some_struct 指针发送到 func,然后将其转换为 uint32_t 指针而不是访问该值。

但是,由于 func 接受了一个 void 指针,并且由于 func 与调用者在不同的编译单元中,编译器不能 "see" 这样的违规行为.


下面的例子呢,根据我的理解没有违规,它完全符合别名的要求:

extern func_takes_word(uint32_t word);

void func(void *obj, size_t size_in_words)
{
    uint32_t word;

    for (int i = 0; i < size_in_words; i++)
    {
        // instead of calling memcpy, (or using union type punning) 
        // for learning purpose
        *(char *)&word = *((char *)obj+ (sizeof(uint32_t) * i)); 
        *(((char *)&word) + 1) = *((char *)obj+ (sizeof(uint32_t) * i)+1);
        *(((char *)&word) + 2) = *((char *)obj+ (sizeof(uint32_t) * i)+2);
        *(((char *)&word) + 3) = *((char *)obj+ (sizeof(uint32_t) * i)+3);
        func_takes_word(word);
    }
}

我说得对吗?

这主要是一个重复的问题,但我还是要写一个答案,因为我找不到更早的答案来讨论通过 void * 和单独编译进行转换的具体问题.

首先,让我们想象一个更简单的代码版本:

#include <stddef.h>
#include <stdint.h>

extern void func_takes_word(uint32_t word);

struct __attribute__((packed, aligned(_Alignof(uint32_t)))) some_struct
{
    uint32_t num_0;
    uint32_t num_1;
    uint32_t num_2;
};

void some_func(void)
{
    struct some_struct stc = {0, 1, 59};

    for (size_t i = 0; i < sizeof(struct some_struct) / sizeof(uint32_t); i++)
        func_takes_word(*(((uint32_t *)&stc) + i));
}

(GCC __attribute__((packed, aligned(...))) 注释的存在只是为了排除由于填充或未对齐而引起的任何问题的可能性。如果你把它去掉,我下面所说的一切仍然是正确的。)

根据对 C2011 最直接的解释,这段代码 确实 违反了 "strict aliasing" 规则(N1570:6.2.7 and 6.5p6,7). The type struct some_struct is not compatible with the type uint32_t. Therefore, taking the address of an object with declared type struct some_struct, casting the resultant pointer to type uint32_t *, adding a nonzero offset, and dereferencing the cast pointer, has undefined behavior. It really is that simple. (EDIT: If the pointer is not offset, the dereference has well-defined behavior, because of a special case rule hiding in section 6.7.2p15 我完全忘记了. 感谢 dbush 指出这一点。)

许多人愤怒地反对对标准的这种解释,并坚持认为委员会一定有其他意思,因为有数百万甚至数十亿行的 "legacy" C 代码行完全符合以上,并期待它的工作。更不用说,在这种解释下,您如何使用 offsetof 做任何有用的事情还不清楚。但文本确实是这么说的,没有其他合理的解释,而且自 1989 年最初的 ANSI C 以来,标准相关部分的措辞基本没有变化。我认为我们必须假设委员会没有兴趣改变文字,三十年来,尽管有几次正式要求澄清或更正,但它表达了他们想要表达的意思。


现在,关于通过 void * and/or 拆分操作的转换,以便对象的原始 "effective type" 对于执行取消引用的代码不可见:这些没有区别。你原来的一对翻译单元仍然有未定义的行为。

通过 void * 的转换没有区别,因为第 6.5.p6 节中的规则没有提及中间转换。他们只谈内存中实际对象的"effective type",以及访问对象的左值表达式的类型。因此,在获取对象地址的时间和取消引用指针的时间之间指针可能具有什么类型并不重要(只要强制转换的 none 破坏信息,即保证不会发生从对象类型到 void * 和返回)的转换。

拆分操作,以便对象的原始 "effective type" 对于执行取消引用的代码不可见(静态),没有区别,因为 C 标准对复杂性没有任何限制在决定是否允许访问之前允许编译器执行的分析。特别是,一个用 "effective type" 标记内存的每个字节并对每个取消引用执行 runtime 检查的实现已得到委员会的明确认可(不在标准,但在 DR 响应中,我不记得这是多久以前的事了,而且 WG14 的网站不是很容易搜索)。在翻译阶段 8 ("link-time optimization") 和阶段 7 期间,还允许实现进行任意激进的内联和过程间分析。将您的原始程序折叠到我的 "simpler version" 中完全在当前的能力范围内-一代全程序优化编译器。


正如对该问题的评论中所指出的,您可以依靠对特定实现的优化器的复杂程度或实现的公开扩展(例如 __attribute__((noinline)))的了解来控制是否或不是你得到的机器代码表现得像预期的那样尽管 未定义的行为。 C 标准甚至通过定义 "conforming program" 和 "strictly conforming program"(N1570:section 4)之间的区别,明确许可您这样做。依赖于特定实现对未定义行为的处理的程序仍然可以 符合 但不是 严格符合 ,其作者必须意识到当移植到不同的实现(可能包括同一编译器的更新版本)时,它可能会中断。

N1570 6.5p7分区访问的规则分为两类:

  1. 所有符合要求的实现在所有情况下都需要以与 "object" 和 "stored" 的定义和描述一致的方式进行处理。

  2. 那些符合标准的实现可能会或可能不会以这种方式处理,在他们有空的时候,大概考虑到他们客户的需求。

作者只希望这个分区在实现的客户可能不需要特定构造但他们可能想要使用确实需要它的代码的情况下相关。有许多几乎每个人都同意编译器应该支持的结构,但实际上属于上面的第二类。与 clang 和 gcc 的作者似乎相信的相反,标准未能强制支持构造不能合理地被视为通过任何判断大多数(如果不是全部)编译器是否应该支持它。

标准的编写方式,甚至是这样的:

struct S { int x[1]; } s;
int test1(void)
{
  s.x[0] = 1;
}

与您的示例相比完全驯服属于上面的第二类。此外,相对较少的代码将依赖于编译器给出一些接近您的示例的东西,例如:

struct S { int x[1], y[1], z[1]; } s;
int test2(int index)
{
  s.y[0] = 1;
  s.x[index] = 2;
  return s.y[0];
}

考虑到访问 s.x[index] 可能会影响 s.y[0] 的可能性。需要以 "interesting" 方式访问内存的代码通常使用可以被任何关心寻找它们的编译器轻松识别的构造,例如给出更接近你的例子的东西:

struct S { int x[1], y[1], z[1]; } s;
int test3(int index)
{
  s.y[0] = 1;
  ((int*)&s)[index] = 2;
  return s.y[0];
}

程序员似乎不太可能将 struct S* 转换为 int* 而无意以 "unusual" 方式访问它,因此编译器将此类转换视为表明它应该允许此类事情的指示不太可能阻止有用的优化。

标准在 test2test3 之间没有区别,而且我不知道 clang 和 gcc 文档中的任何内容都会这样做。尽管当前版本的 gcc 似乎做出了这样的区分,并且尽管当前的 clang 错过了对这两个函数的优化,但我不会依赖任何一个编译器来支持 test3 或其他有意义地需要类似语义的函数,除非或直到它们明确记录这样的支持。