【问题标题】:Is a memcpy between differently aligned pointers UB?不同对齐的指针UB之间是memcpy吗?
【发布时间】:2016-09-07 22:32:18
【问题描述】:

据我了解,以下代码在 C11 中表现出未定义的行为:

#include <string.h>

struct aaaa { char bbbb; int cccc; };

int main(void) {
    unsigned char buffer[sizeof(struct aaaa)] = { 0 };
    struct aaaa *pointer = &buffer[0];

    return (*pointer).cccc;
}

根据N1570 第 6.5.3.2 节第 4 条,

如果已为指针分配了无效值,则一元 * 运算符的行为未定义。

附有一个脚注,说明了这一点

一元 * 操作符解除对指针的引用的无效值包括空指针、与指向的对象类型不恰当对齐的地址以及对象在其生命周期结束后的地址。

struct aaaa *unsigned char * 不太可能具有相同的对齐方式,因此我们为 pointer 分配了无效值,因此使用 *pointer 会导致 UB。

但是,我可以复制结构吗?

#include <string.h>

struct aaaa { char bbbb; int cccc; };

int main(void) {
    unsigned char buffer[sizeof(struct aaaa)] = { 0 };
    struct aaaa target;

    memcpy(&target, buffer, sizeof(struct aaaa));

    return target.cccc;
}

在这里,我们将struct aaaa *unsigned char * 传递给memcpy。虽然这看起来和第一段代码一样糟糕,但我在 C11 中找不到任何规定该代码显示 UB 的措辞。 memcpy 的这种用法会导致未定义的行为吗?

【问题讨论】:

    标签: c language-lawyer memcpy c11


    【解决方案1】:

    不,memcpy 不对对齐做出任何假设。它在功能上等同于逐字节复制。

    顺便说一句,通过非字符类型的不同类型的左值访问auto 对象会导致未定义的行为,无论对齐方式如何。这违反了有效类型规则,C11 6.5 p6 和 p7。

    【讨论】:

    • 但是在 C11 中“逐字节复制”是什么意思呢?也许第一个字节会很好,但是你将如何将buffer[1] 复制到*(((unsigned char *)&amp;target)+1)?当然,后一个表达式有 UB...
    • @Rhymoid,相当于将地址分别转换为char*char const*,然后进行for循环进行复制。始终允许通过字符指针访问单个字节。
    • 虽然memcpy 本身通常不会对对齐做出假设,但它可能会利用编译器在其他地方做出的关于对齐的假设。例如,如果编译器发现memcpy 的参数是uint32_t 类型,它可能会生成memcpy 代码,如果指针未针对该类型对齐,则会失败。编译器是否通常会关心转换为 uint32_t* 并返回到 void* 而不被取消引用为 uint32_t 的指针的对齐,如果指针转换为 uint32_t* 将调用未定义行为未对齐...
    • ...对于该类型(即使它从未被取消引用为该类型)。由于memcpy 接收未对齐指针的唯一方法是如果 UB 已经发生,则编译器不需要处理这种情况。
    【解决方案2】:

    据我了解,这两种情况都是 UB(但不是因为调用了memcpy),因为编译器没有正确强制变量的起始偏移对齐。您可以确保与特定于编译器的属性保持对齐,但这当然是特定于平台的解决方案。

    假设起始偏移量是对齐的(这是实践中的假设),就像编译器通常这样做是为了提高性能:

    在您的第一个示例中,您在第一个缓冲区索引 0 处分配。buffer 通常正确对齐。 cccc 也将对齐,因为 struct 未打包。在这种情况下应该不会造成问题。

    在第二个示例中,当使用memcpy 时,所有内容都会正确复制,因为(在内部)它会尽最大努力进行对齐复制以提高性能,而当不可能时,它会按字节复制。同样,所有结构和缓冲区都符合我上面提到的限制。

    这里的实际问题是什么?

    如果您分配&amp;buffer[1](假设它通常不对齐),您将冒险(在实践中明显)。对cccc 的访问将从未对齐的地址加载一个字。在某些架构上,它会导致可怕的 SIGBUS。 x86 检测到未对齐的寻址并减慢一点(可能),但不会崩溃。

    【讨论】:

    • 没有理由假设bufferstruct aaaa 类型正确对齐。
    • 是的,这通常是由编译器特定的对齐属性强制执行的,我忘了。当buffer[0]对齐时,仍然没有问题。
    • 您的答案仍然假定buffer 是对齐的。
    猜你喜欢
    • 2019-11-18
    • 2020-08-12
    • 1970-01-01
    • 2018-12-16
    • 2022-01-20
    • 1970-01-01
    • 2023-03-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多