C未定义的行为。严格的别名规则,还是不正确的对齐? [英] C undefined behavior. Strict aliasing rule, or incorrect alignment?

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

问题描述

我无法解释此程式的执行行为:

  #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];
返回散列;
}

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





$ b因此,当它使用gcc编译-O3并运行参数25,它提出了一个段错误。没有优化,它可以正常工作。我已经对它进行了反汇编:它正在被矢量化,并且编译器假定 key2 数组以16字节对齐,所以它使用 movdqa 。显然这是UB,尽管我无法解释它。我知道严格的别名规则,并不是这种情况(我希望),因为据我所知,严格的别名规则不适用于 char s 。为什么gcc认为这个指针是一致的?即使进行了优化,Clang也可以正常工作。



编辑



我将 unsigned char 更改为 char ,并且删除了 const ,它仍然是段错误。



EDIT2



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

解决方案

代码确实打破了严格的别名规则。但是,不仅存在别名冲突,并且崩溃不会因为别名冲突而发生。发生这种情况的原因是 unsigned short 指针不正确对齐;即使转换本身在未适当对齐的情况下也具有未定义的行为。




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

....


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

使用 6.3.2.3p7 说: $ b


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

无符号短的对齐要求 2 在您的实现(x86-32和x86-64)上,您可以使用

  _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< len; ++ i)
hash + = key2 [i];

可以使用 SIMD说明(如果适当对齐)。这些值使用 MOVDQA ,它要求参数与 16字节


当源操作数或目标操作数是内存操作数时,操作数必须在16字节边界或一般保护异常(#GP)。

对于指针在启动时没有适当对齐的情况,编译器会生成一个代码,将前面1-7个无符号短裤一个接一个地加起来,直到指针对齐到16个字节为止。



当然,如果你从一个指向一个 odd 地址的指针,甚至不会添加7次2将把一个地址映射到一个与16个字节对齐的地址。当然,编译器甚至不会生成检测这种情况的代码,因为行为未定义,如果两个指针类型之间的转换产生不正确对齐的结果 - 并忽略这种情况完全有不可预知的结果,这意味着 MOVDQA <


$ b

可以很容易地证明,code>不会被正确对齐,然后程序崩溃。即使不违反任何严格的别名规则,也可能发生这种情况。考虑下面这个由 2 翻译单元组成的程序(如果 f 及其调用者被放入一个翻译单元,我的GCC很聪明,足以注意到我们在这里使用打包结构,不会生成代码, MOVDQA ):

翻译单元1


  #include  
#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];
返回散列;
}

翻译单元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));
}

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

 %gcc -O3 unit1.c unit2.c 
%./a.out
25
zsh:段错误转储)./a.out

请注意,在那里没有锯齿违规。唯一的问题是未对齐 uint16_t * keyc



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

  unit1.c:10:21:运行时错误:未对齐的加载地址0x7ffefc2d54f1,类型为'uint16_t',需要2个字节对​​齐
0x7ffefc2d54f1:注意:指针指向
00 00 00 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\n", 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 conversion itself has undefined behaviour 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\n", 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天全站免登陆