有时Stack溢出的问题和答案会将指针强制转换为类型惩罚的有效方式。它经常被声称拒绝严格别名并因此调用未定义的行为。
现代目标真的关心严格别名吗?在这种情况下,是否存在会显示意外行为的程序实例?
答案 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都无法正确处理test1
或test2
,arr[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