C 未定义的行为.严格的混叠规则,或不正确的对齐方式? [英] C undefined behavior. Strict aliasing rule, or incorrect alignment?

查看:19
本文介绍了C 未定义的行为.严格的混叠规则,或不正确的对齐方式?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

#include #include #include typedef 字符 u8;typedef unsigned short u16;size_t f(u8 *keyc, size_t len){u16 *key2 = (u16 *) (keyc + 1);size_t 哈希 = len;len = len/2;for (size_t i = 0; i < len; ++i)哈希 += key2[i];返回哈希值;}int main(){srand(时间(空));size_t len;scanf("%lu", &len);u8 x[len];for (size_t i = 0; i < len; i++)x[i] = rand();printf("输出%lu
", f(x, len));}

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

编辑

我把unsigned char改成了char,去掉了const,还是segfaults.

编辑2

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

解决方案

该代码确实违反了严格的别名规则.但是,不仅存在别名冲突,而且崩溃不会因为别名冲突而发生.发生这种情况是因为 unsigned short 指针未正确对齐;如果结果未适当对齐,即使 指针转换 本身也是未定义的.

C11(n1570 草案)附录 J.2:

<块引用>

1 在以下情况下行为未定义:

....

  • 两种指针类型之间的转换会产生不正确对齐的结果 (6.3.2.3).

6.3.2.3p7说><块引用>

[...] 如果引用类型的结果指针没有正确对齐 [68],则行为未定义.[...]

unsigned short 在您的实现(x86-32 和 x86-64)上有 2 的对齐要求,您可以使用

进行测试

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

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

u16 *key2 = (u16 *) (keyc + 1);//我们已经有未定义的行为*这里*!

有无数的程序员坚持认为非对齐访问在实践中可以保证在 x86-32 和 x86-64 上的任何地方都有效,而且实践中不会有任何问题 - 好吧,他们都错了.

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

for (size_t i = 0; i 

如果适当对齐,

可以使用 SIMD 指令更有效地执行.这些值使用 MOVDQA,这要求参数对齐到 16 字节:

<块引用>

当源或目标操作数是内存操作数时,操作数必须在 16 字节边界上对齐,否则将产生通用保护异常 (#GP).

对于在开始时指针没有适当对齐的情况,编译器将生成代码,将前 1-7 个无符号短整型相加,直到指针对齐到 16 个字节.

当然,如果您从一个指向奇数地址的指针开始,即使将 7 乘以 2 也不会使 1 到达与 16 字节对齐的地址.当然,编译器甚至不会生成检测这种情况的代码,因为行为未定义,如果两种指针类型之间的转换产生不正确对齐的结果" - 并忽略 情况完全不可预知,这意味着MOVDQA的操作数将没有正确对齐,这将导致程序崩溃.

<小时>

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

翻译单元 1:

#include #include size_t f(uint16_t *keyc, size_t len){size_t 哈希 = len;len = len/2;for (size_t i = 0; i < len; ++i)哈希 += keyc[i];返回哈希值;}

翻译单元 2

#include #include #include #include #include size_t f(uint16_t *keyc, size_t len);结构我的结构{uint8_t 填充;uint16_t 内容[100];} __attribute__ ((打包));int main(void){结构 mystruct s;size_t len;srand(时间(空));scanf("%zu", &len);char *initializer = (char *)s.contents;for (size_t i = 0; i < len; i++)初始值设定项[i] = rand();printf("out %zu
", f(s.contents, len));}

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

% gcc -O3 unit1.c unit2.c% ./a.out25zsh:分段错误(核心转储)./a.out

请注意,那里没有别名违规.唯一的问题是未对齐的 uint16_t *keyc.

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

unit1.c:10:21: 运行时错误:加载未对齐的地址 0x7ffefc2d54f1 用于类型 'uint16_t',需要 2 字节对齐0x7ffefc2d54f1:注意:指针指向这里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^

I can't explain the execution behavior of this program:

#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
", f(x, len));
}

So, when it is compiled with -O3 with gcc, and run with argument 25, it raises a segfault. Without optimizations it works fine. I've disassembled it: it is being vectorized, and the compiler assumes that the key2 array is aligned at 16 bytes, so it uses movdqa. Obviously it is UB, although I can't explain it. I know about the strict aliasing rule and it is not this case (I hope), because, as far as I know, the strict aliasing rule doesn't work with chars. Why does gcc assume that this pointer is aligned? Clang works fine too, even with optimizations.

EDIT

I changed unsigned char to char, and removed const, it still segfaults.

EDIT2

I know that this code is not good, but it should work ok, as far as I know about the strict aliasing rule. Where exactly is the violation?

解决方案

The code indeed breaks the strict aliasing rule. However, there is not only an aliasing violation, and the crash doesn't happen because of the aliasing violation. It happens because the unsigned short pointer is incorrectly aligned; even the pointer conversion itself is undefined if the result is not suitably aligned.

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).

With 6.3.2.3p7 saying

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

unsigned short has alignment requirement of 2 on your implementation (x86-32 and x86-64), which you can test with

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

However, you're forcing the u16 *key2 to point to an unaligned address:

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

There are countless programmers that insist that unaligned access is guaranteed to work in practice on x86-32 and x86-64 everywhere, and there wouldn't be any problems in practice - well, they're all wrong.

Basically what happens is that the compiler notices that

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

can be executed more efficiently using the SIMD instructions if suitably aligned. The values are loaded into the SSE registers using MOVDQA, which requires that the argument is aligned to 16 bytes:

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.

For cases where the pointer is not suitably aligned at start, the compiler will generate code that will sum the first 1-7 unsigned shorts one by one, until the pointer is aligned to 16 bytes.

Of course if you start with a pointer that points to an odd address, not even adding 7 times 2 will land one to an address that is aligned to 16 bytes. Of course the compiler will not even generate code that will detect this case, as "the behaviour is undefined, if conversion between two pointer types produces a result that is incorrectly aligned" - and ignores the situation completely with unpredictable results, which here means that the operand to MOVDQA will not be properly aligned, which will then crash the program.


It can be easily proven that this can happen even without violating any strict aliasing rules. Consider the following program that consists of 2 translation units (if both f and its caller are placed into one translation unit, my GCC is smart enough to notice that we're using a packed structure here, and doesn't generate code with MOVDQA):

translation unit 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;
}

translation unit 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
", f(s.contents, len));
}

Now compile and link them together:

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

Notice that there is no aliasing violation there. The only problem is the unaligned uint16_t *keyc.

With -fsanitize=undefined the following error is produced:

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 未定义的行为.严格的混叠规则,或不正确的对齐方式?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆