C 未定义的行为。严格的别名规则,还是不正确的对齐方式?

标签 c gcc memory-alignment strict-aliasing

<分区>

我无法解释这个程序的执行行为:

#include <string> 
#include <cstdlib> 
#include <stdio.h>

typedef char u8;
typedef unsigned short u16;

size_t f(u8 *keyc, size_t len)
{
    u16 *key2 = (u16 *) (keyc + 1);
    size_t hash = len;
    len = len / 2;

    for (size_t i = 0; i < len; ++i)
        hash += key2[i];
    return hash;
}

int main()
{
    srand(time(NULL));
    size_t len;
    scanf("%lu", &len);
    u8 x[len];
    for (size_t i = 0; i < len; i++)
        x[i] = rand();

    printf("out %lu\n", f(x, len));
}

因此,当它使用 gcc 的 -O3 编译并使用参数 25 运行时,它会引发段错误。没有优化它工作正常。我已经反汇编了它:它正在被矢量化,编译器假定 key2 数组以 16 字节对齐,因此它使用 movdqa。明明是UB,虽然我解释不了。我知道严格的别名规则,但不是这种情况(我希望如此),因为据我所知,严格的别名规则不适用于 char。为什么 gcc 假设这个指针是对齐的?即使经过优化,Clang 也能正常工作。

编辑

我把unsigned char改成了char,去掉了const,还是有段错误。

编辑2

我知道这段代码不好,但据我所知严格的别名规则,它应该可以正常工作。具体违规在哪里?

最佳答案

该代码确实违反了严格的别名规则。但是,不仅存在混叠违规,而且崩溃不会因为混叠违规而发生。这是因为 unsigned short 指针未正确对齐;如果结果没有适当对齐,甚至指针转换本身也是未定义的。

C11 (draft n1570) Appendix J.2 :

1 The behavior is undefined in the following circumstances:

....

  • Conversion between two pointer types produces a result that is incorrectly aligned (6.3.2.3).

6.3.2.3p7

[...] If the resulting pointer is not correctly aligned [68] for the referenced type, the behavior is undefined. [...]

unsigned short 对您的实现(x86-32 和 x86-64)有 2 的对齐要求,您可以使用它进行测试

_Static_assert(_Alignof(unsigned short) == 2, "alignof(unsigned short) == 2");

但是,您强制 u16 *key2 指向未对齐的地址:

u16 *key2 = (u16 *) (keyc + 1);  // we've already got undefined behaviour *here*!

有无数程序员坚持认为未对齐访问在实践中可以保证在 x86-32 和 x86-64 上无处不在,并且在实践中不会有任何问题 - 好吧,他们都错了。

基本上发生的事情是编译器注意到

for (size_t i = 0; i < len; ++i)
     hash += key2[i];

可以使用 SIMD instructions 更有效地执行如果适当对齐。使用 MOVDQA 将值加载到 SSE 寄存器中,这要求参数对齐到 16 字节:

When the source or destination operand is a memory operand, the operand must be aligned on a 16-byte boundary or a general-protection exception (#GP) will be generated.

对于指针在开始时未适当对齐的情况,编译器将生成代码,将前 1-7 个无符号短整数逐个求和,直到指针对齐到 16 字节。

当然,如果您从指向奇数 地址的指针开始,即使将 7 乘以 2 相加也不会使 1 到达与 16 字节对齐的地址。当然,编译器甚至不会生成检测这种情况的代码,因为“如果两种指针类型之间的转换产生的结果未正确对齐,则行为未定义”——并忽略 the situation completely with unpredictable results ,这意味着 MOVDQA 的操作数将不会正确对齐,这将导致程序崩溃。


可以很容易地证明,即使不违反任何严格的别名规则也可以发生这种情况。考虑以下由 2 个翻译单元组成的程序(如果 f 及其调用者都放在 一个 翻译单元中,我的 GCC 就足够聪明了注意我们在这里使用了一个压缩结构,并且没有使用MOVDQA生成代码):

翻译单元 1:

#include <stdlib.h>
#include <stdint.h>

size_t f(uint16_t *keyc, size_t len)
{
    size_t hash = len;
    len = len / 2;

    for (size_t i = 0; i < len; ++i)
        hash += keyc[i];
    return hash;
}

翻译单元 2

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
#include <inttypes.h>

size_t f(uint16_t *keyc, size_t len);

struct mystruct {
    uint8_t padding;
    uint16_t contents[100];
} __attribute__ ((packed));

int main(void)
{
    struct mystruct s;
    size_t len;

    srand(time(NULL));
    scanf("%zu", &len);

    char *initializer = (char *)s.contents;
    for (size_t i = 0; i < len; i++)
       initializer[i] = rand();

    printf("out %zu\n", f(s.contents, len));
}

现在编译并将它们链接在一起:

% gcc -O3 unit1.c unit2.c
% ./a.out
25
zsh: segmentation fault (core dumped)  ./a.out

请注意,这里没有违反别名的情况。唯一的问题是未对齐的 uint16_t *keyc

使用 -fsanitize=undefined 会产生以下错误:

unit1.c:10:21: runtime error: load of misaligned address 0x7ffefc2d54f1 for type 'uint16_t', which requires 2 byte alignment
0x7ffefc2d54f1: note: pointer points here
 00 00 00  01 4e 02 c4 e9 dd b9 00  83 d9 1f 35 0e 46 0f 59  85 9b a4 d7 26 95 94 06  15 bb ca b3 c7
              ^ 

关于C 未定义的行为。严格的别名规则,还是不正确的对齐方式?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/46790550/

相关文章:

c - 有没有办法获得 C 语言支持的波特率?

c++ - 如何调整 std::vector<std::queue<std::unique_ptr<int>>> 的大小?

ios - GCC 优化 : use of ARM conditional instructions?

c++ - 强制 clang 在 linux 中使用 llvm 而不是 gcc

C++/Linux 出于性能原因对齐字符数组?

c - 链表查询

更改指针数组数据的地址

c - Arm Cortex-M4 LDRD 指令导致硬故障

c - GNU GCC 编译器 - 对齐属性

c++ - 如何在 C++ 中模拟 5 级流水线?