首页 > 解决方案 > 使用 void 指针干预时是否发生严格别名

问题描述

以下示例是否违反了严格的别名规则?

在文件

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
}

在文件bc

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与调用者在不同的编译单元中,编译器无法“看到”这样的违规行为。


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

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

我对么?

标签: clanguage-lawyerstrict-aliasing

解决方案


这主要是一个重复的问题,但无论如何我都会写出一个答案,因为我找不到更早的答案来讨论通过转换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 最直接的解释,这段代码确实违反了“严格别名”规则(N1570:6.2.76.5p6,7)。该类型struct some_struct与该类型不兼容uint32_t。因此,获取具有声明 type 的对象的地址,struct some_struct将结果指针转换为 type uint32_t *,添加非零偏移量并取消引用转换指针,具有未定义的行为。真的就是这么简单。(编辑:如果指针没有偏移,则取消引用具有明确定义的行为,因为我完全忘记了第 6.7.2p15 节中隐藏的特殊情况规则。感谢 dbush 指出这一点。)

许多人愤怒地反对对标准的这种解释,并坚持认为委员会一定有别的意思,因为那里有数百万(如果不是数十亿)行的“遗留”C 代码完全符合上述要求并期望它能够工作。更不用说offsetof在这种解释下你如何做任何有用的事情都不清楚。但是文本确实是这样说的,没有其他合理的解释,而且标准相关部分的措辞自 1989 年 ANSI C 以来几乎没有变化。我认为我们必须假设委员会对改变标准缺乏兴趣文本,三十年来,尽管有几次正式的澄清或更正要求,它意味着它表达了他们想要表达的意思。


现在,关于强制转换void *和/或拆分操作,以便执行取消引用的代码看不到对象的原始“有效类型”:这些没有区别。 您原来的一对翻译单元仍有未定义的行为。

强制转换没有任何区别,因为6.5.p6void *节中的规则没有说明中间强制转换。他们只谈论内存中实际对象的“有效类型”,以及用于访问该对象的左值表达式的类型。因此,在获取对象地址的时间和取消引用指针的时间之间指针可能具有什么类型并不重要(只要没有强制转换破坏信息,这保证不会发生用于对象类型的void *来回转换)。

拆分操作,使对象的原始“有效类型”对执行取消引用的代码不可见(静态),这没有什么区别,因为 C 标准对编译器的分析复杂性没有任何限制。在决定是否允许访问之前允许执行。特别是,一种用其“有效类型”标记内存的每个字节并执行运行时的实现检查每一个取消引用,已得到委员会的明确认可(不是在标准的文本中,而是在 DR 的回复中,我不记得这是多久以前了,而且 WG14 的网站不是很容易搜索)。在翻译阶段 8(“链接时间优化”)和阶段 7 期间,还允许实现任意积极的内联和过程间分析。将您的原始程序折叠成我的“更简单版本”完全在当前的能力范围内 -一代全程序优化编译器。


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


推荐阅读