是否存在现代目标/编译器的任何示例,其中在C中破坏严格别名会影响程序结果?

时间:2018-05-17 12:52:56

标签: c strict-aliasing

有时Stack溢出的问题和答案会将指针强制转换为类型惩罚的有效方式。它经常被声称拒绝严格别名并因此调用未定义的行为。

现代目标真的关心严格别名吗?在这种情况下,是否存在会显示意外行为的程序实例?

3 个答案:

答案 0 :(得分:3)

是的,严格别名是非常真实的现象,并且现代编译器经常利用它来执行优化。

考虑以下代码 -

typedef struct
{
    char a;
} my_struct;

void foo( int * a, my_struct * b, int count )
{
    int i;

    for (i=0;i<count;i++)
    {
        a[i] += b->a;
    }
}

当使用clang 3.8.0-2(这是一个非常现代的编译器)编译X64目标(也是一个非常现代的目标)时,使用命令 -

clang -m64 -S -O2 -std=c11 foo.c -fno-vectorize -fno-unroll-loops

生成以下程序集(简化并在 AT&amp; T 语法中) -

foo:
    testl   %edx, %edx
    jle     .LBB0_3
    movsbl  (%rsi), %eax   # Value loaded only once before start of loop
    .align  16, 0x90
.LBB0_2:
    addl    %eax, (%rdi)
    addq    $4, %rdi
    decl    %edx
    jne     .LBB0_2
.LBB0_3:
    retq

可以看到来自b->a的值仅在循环开始之前加载一次并添加到a中的所有整数。

但是如果这个函数被称为 -

my_struct a[100];
// initializaion of values in a;
...

foo((int*)a, a, 2);  // Breaking strict aliasing

现在很容易看出结果如何不是预期的结果。

在较低的优化级别-fno-strict-aliasing一起,编译器会在循环体内添加指令movsbl (%rsi), %eax

因此可以得出结论,现代编译器的现代架构确实利用了严格的别名,因此不能将指针转换用作类型惩罚的方式。

<强>参考: 这个例子来自于这个blog post

答案 1 :(得分:1)

像gcc和clang这样的编译器要么太原始,要么太聪明&#34;聪明&#34; [或者可能两者 - 取决于你要求的人]识别出已经被用作一种类型的存储需要作为另一种存储的情况,即使在除了最原始的编译器之外的任何其他事物都应该容易识别的情况下。例如:

struct s1 { int x; };
struct s2 { int x; };
union s1s2 { struct s1 v1; struct s2 v2; };

int read_S1(struct s1 *p) { return p->x; }
void write_S2(struct s2 volatile *p, int v) { p->x = v; }

int test1(union s1s2 arr[], int i, int j)
{
    int temp;
    if (read_S1(&arr[i].v1))
    {
        temp = arr[i].v1.x;
        arr[i].v2.x = temp;
        write_S2(&arr[j].v2, 1);
        temp = arr[i].v2.x;
        arr[i].v1.x = temp;
    }
    return read_S1(&arr[i].v1);
}

int test2(union s1s2 arr[], int i, int j)
{
    int temp;
    { struct s1 *p = &arr[i].v1; temp = p->x; }
    if (temp)
        { struct s2 volatile *p = &arr[j].v2; p->x = 1; }
    { struct s1 *p = &arr[i].v1; temp = p->x; }
    return temp;
}

clang和gcc都无法正确处理test1test2arr[i]arr[j]可能识别相同存储空间的可能性,即使它没有实际上上面代码中的任何别名都是写的。通过任何合理的别名定义,arr[i]arr[j]别名i==j - 它们只是将相同的索引应用于同一个数组。此外,使用每个指针访问的所有存储或从其中派生的任何存储都将在该指针的生命周期内以这种方式排他地访问,因此没有任何指针别名。

不幸的是,clang和gcc都太原始了,不能注意到在arr[i].v1的地址形成的第一个和第二个指针的生命周期之间,代码写入了arr[j],这可能是合法的与arr[i]相同的左值。相反,他们盲目地假设,因为arr[i].v1的物理地址两次都是相同的,并且因为没有实际改变与该位置相关联的任何存储位的操作使用类型struct s1这样做,所以#39;允许忽略其他一切。

答案 2 :(得分:0)

  

在较低的优化级别上或使用-fno-strict-aliasing编译器   在循环体内添加指令movsbl(%rsi),%eax。

它不适用于较低的优化级别。

你的例子是错误的,并且函数以这种方式构造来调用UB,因为你已经忘记了这种惩罚的副作用。在这种情况下 - 如果您知道可以通过编译器不可见的程序更改被处罚对象的值,则应使用volatile关键字。你刚刚犯了一个典型的不稳定错误。

这个功能差别很大。作者知道它容易产生副作用 - 只是忽略了它。无论是否有指针处罚都是错误的。

typedef struct
{
    char a;
} my_struct;

my_struct a, b[100];

void foo( int * a, my_struct * b, int count )
{
    int i;

    for (i=0;i<count;i++)
    {
        a[i] += b->a;
    }
}

void foo1( int * a, volatile my_struct * b, int count )
{
    int i;

    for (i=0;i<count;i++)
    {
        a[i] += b->a;
    }
}


void call1(void)
{
    int *v = (int *)b;

    foo(v, &b[0], 100);
}

void call2(void)
{


    foo((int *)b, b, 100);
}

void call3(void)
{
    int *v = (int *)b;

    foo1(v, &b[0], 100);
}

void call4(void)
{


    foo1((int *)b, b, 100);
}

编译代码。

foo: # @foo
  test edx, edx
  jle .LBB0_3
  movsx eax, byte ptr [rsi]
  mov ecx, edx
.LBB0_2: # =>This Inner Loop Header: Depth=1
  add dword ptr [rdi], eax
  add rdi, 4
  add rcx, -1
  jne .LBB0_2
.LBB0_3:
  ret
foo1: # @foo1
  test edx, edx
  jle .LBB1_3
  mov eax, edx
.LBB1_2: # =>This Inner Loop Header: Depth=1
  movsx ecx, byte ptr [rsi]
  add dword ptr [rdi], ecx
  add rdi, 4
  add rax, -1
  jne .LBB1_2
.LBB1_3:
  ret
call1: # @call1
  mov rax, -400
  movsx ecx, byte ptr [rip + b]
.LBB2_1: # =>This Inner Loop Header: Depth=1
  add dword ptr [rax + b+400], ecx
  add rax, 4
  jne .LBB2_1
  ret
call2: # @call2
  mov rax, -400
  movsx ecx, byte ptr [rip + b]
.LBB3_1: # =>This Inner Loop Header: Depth=1
  add dword ptr [rax + b+400], ecx
  add rax, 4
  jne .LBB3_1
  ret
call3: # @call3
  mov rax, -400
.LBB4_1: # =>This Inner Loop Header: Depth=1
  movsx ecx, byte ptr [rip + b]
  add dword ptr [rax + b+400], ecx
  add rax, 4
  jne .LBB4_1
  ret
call4: # @call4
  mov rax, -400
.LBB5_1: # =>This Inner Loop Header: Depth=1
  movsx ecx, byte ptr [rip + b]
  add dword ptr [rax + b+400], ecx
  add rax, 4
  jne .LBB5_1
  ret