C/C++ 中的 strict aliasing
source link: http://www.kongjunblog.xyz/2021/04/cc-strict-aliasing.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
C/C++ 中的 strict aliasing 跳至主要内容
C/C++ 中的 strict aliasing
C/C++ 中的变量占有一块内存,这时这个变量就是这块内存的别名,指针也可以指向内存,因此同一块内存可能会有多个别名。
int main()
{
int i = 0;
int *ip = &i;
}
其中i
和ip
是同一块内存,都是它的别名。
内存别名的存在会影响编译器生成的代码的行为。
考虑以下代码块(来自 CSAPP 5.1 节):
void twiddle1(long *xp, long *yp)
{
*xp += *yp;
*xp += yp;
}
void tiwddle2(long *xp, long *yp)
{
*xp *= 2 * *yp;
}
这两个函数的功能看起来是相同的,但其实不然。加入,xp
和yp
指向同一块内存,twiddle1()
将*xp
写为原来的四倍,而twiddle2()
将xp
写为原来的两倍。
编译器在进行优化时,要确保优化是安全的,即优化的程序和未优化的程序行为是一致的。在上面的例子中,编译器无法判断xp
和yp
是同一块内存的别名(指向同一块内存),只能保守地认为两个指针指向同一块内存,因此twiddle()
要老老实实的进行两次+=
。
而当指针指向的对象类型不同时,编译器可以放心地认为指针指向不同的内存,互相不为别名,这就是所谓的 strict aliasing:不同类型的指针指向不同的内存块。在这种情况下,编译器可以使用激进的优化策略。
但是 C/C++ 经常使用类型转换和指针直接操作内存,有时就会破坏 strict aliasing 规则,导致未定义行为。
xxxxxxxxxx
#include <stdio.h>
int global = 2;
int test_strict_aliasing(int *arg)
{
global = 1;
*reinterpret_cast<float*>(arg) = 0;
return global;
}
int
main()
{
printf("%d\n", test_strict_aliasing(&global));
printf("global: %d\n", global);
return 0;
}
----------------------------------------------------------------------
g++ -Wall -Wstrict-aliasing=1 -o strict-aliasing strict-aliasing.cpp
----------------------------------------------------------------------
0
global: 0
------------------------------------------------------------------------
g++ -Wall -Wstrict-aliasing=1 -O2 -o strict-aliasing strict-aliasing.cpp
------------------------------------------------------------------------
0
global: 1
在上面的程序中reinterpret_cast<float *>(arg)
创建了一个临时的float *
指针,并且指向的内存块和int *
类型指针arg
相同,这是非法的内存别名,这反了 strict aliasing 规则,产生未定义行为。在 GCC O2 以下的优化级别,不假设 strict aliasing, 在 O2 及以上优化级别假设 strict aliasing,因此两编译选项下程序的行为不同。
xxxxxxxxxx
g++ -Wall -Wstrict-aliasing=1 -O2 -o strict-aliasing strict-aliasing.cpp 生成的汇编代码
401170: c7 05 aa 2e 00 00 01 movl $0x1,0x2eaa(%rip) # 404024 <global>
401177: 00 00 00
40117a: b8 01 00 00 00 mov $0x1,%eax
40117f: c7 07 00 00 00 00 movl $0x0,(%rdi)
401185: c3 retq
401186: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
40118d: 00 00 00
g++ -Wall -Wstrict-aliasing=1 -o strict-aliasing strict-aliasing.cpp 生成的汇编代码
401126: 55 push %rbp
401127: 48 89 e5 mov %rsp,%rbp
40112a: 48 89 7d f8 mov %rdi,-0x8(%rbp)
40112e: c7 05 ec 2e 00 00 01 movl $0x1,0x2eec(%rip) # 404024 <global>
401135: 00 00 00
401138: 48 8b 45 f8 mov -0x8(%rbp),%rax
40113c: 66 0f ef c0 pxor %xmm0,%xmm0
401140: f3 0f 11 00 movss %xmm0,(%rax)
401144: 8b 05 da 2e 00 00 mov 0x2eda(%rip),%eax # 404024 <global>
40114a: 5d pop %rbp
40114b: c3 retq
可以发现 O2 编译选项下生成的代码更少,性能更强。O2 下的代码直接将数字1
当成返回值返回,而不是将global
当成返回值返回,因此程序出现了错误的行为。
将指针转型为不相容(imcompatible的指针类型,并进行读写违反了 strict aliasing,是严重的未定义行为。如果确实需要进行 type punning,必须将指针转换为相容的指针类型,即通过合法的内存别名访问内存。C/C++ 标准规定了以下类型的指针是合法的别名:
- 指针指向的类型相差
unsigned
、signed
、volatile
char *
和void *
是所有指针的合法别名- 指向包含指针指向对象类型的聚合类或 union 的指针是合法别名
使用char *
创建合法别名
char
在 C/C++ 中实际上是字节类型,使用非常频繁,因此在标准中为它开了“后门”。
以下程序避免了未定义行为:
xxxxxxxxxx
int test_strict_aliasing(int *arg)
{
global = 1;
*reinterpret_cast<float*>(arg) = 0;
*reinterpret_cast<char *>(arg) = 0;
*(reinterpret_cast<char *>(arg) + 1) = 0;
*(reinterpret_cast<char *>(arg) + 2) = 0;
*(reinterpret_cast<char *>(arg) + 3) = 0;
return global;
}
使用union *
创建合法别名
xxxxxxxxxx
union int2float
{
int i;
float f;
};
int test_strict_aliasing(int *arg)
{
global = 1;
*reinterpret_cast<float*>(arg) = 0;
return global;
}
上面的程序定义了一个包含我们要修改的指针指向的对象类型(int
)的联合体,然后将arg
转型为union int2float *
再通过 union 修改*arg
。这种方法是 GCC 推荐的方法。
禁止编译器假设 strict aliasing
上面提到,在 O2 及以上优化等级才会 假设 strict aliasing ,有大量的 C/C++ 程序必须违反 strict aliasing,因此 GCC 提供了-fstrict-aliasing
和-fno-strict-aliasing
选项开启和关闭 strict aliasing。
GCC 还提供了-Wstrict-aliasing
来警告违反 strict-aliaisng 的行为,这个选项被-Wall
默认开启。GCC 虽然提供了警告选项,但该功能工作的并不好。
GCC 对 strict aliasing 的处理让很多 C/C++ 程序不加上-no-strict-aliasing
选项就无法正确运行,这引起了很多人的愤怒,Linus 还专门喷过 GCC,但是经过我通过实验发现,Clang 也和 GCC 一样烂。
总之,要避免直接对指针进行转型并读写,这是未定义的!!!
此博客中的热门博文
使用 Vim 搭建 C/C++ 开发环境
Ibex 架构介绍
UNIX 进程关系
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK