前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >GCC -O2 踩坑指南:严格别名(Strict Aliasing)与整数环绕(Integer Wrap-around)

GCC -O2 踩坑指南:严格别名(Strict Aliasing)与整数环绕(Integer Wrap-around)

作者头像
Flowlet
发布2024-02-26 21:19:19
3040
发布2024-02-26 21:19:19
举报
文章被收录于专栏:FlowletFlowlet

关于作者:

作者:张帅,云网络从业人员

博客:www.flowlet.net

GCC 在开启 -O2 编译优化后,会遇到编译器领域的两个著名问题:严格别名(Strict Aliasing)与整数环绕(Integer Wrap-around)。

本次笔者就为大家详细讲解下这两个经典的编译优化问题。由于作者水平有限,本文不免存在遗漏或错误之处,欢迎指正交流。

1、什么是别名(alias)

在 C 和 C++ 中,当多个左值 lvalue 指向同一个内存区域时,就会出现别名(alias)。

例 1:

代码语言:javascript
复制
int a = 42;int *ptr = &a;

*ptr 改变 a 的值也会改变。这里 *ptr 就被称为 a 的别名。

1.1 类型双关(type punning)


别名(alias)最常见的用途就是类型双关(type punning)。有时我们想绕过类型系统,将一个对象解释为不同的类型,这就是所谓的类型双关。类型双关经常应用在编译器、序列化、网络传输等领域。

类型双关一般做法是通过别名(alias)来实现,通过获取对象的地址,将其转换为我们想要重新解释的类型的指针,然后访问该值。

以下就是类型双关的例子,在标准定义中,这种类型双关属于未定义的行为。

代码语言:javascript
复制
int a;float *ptr = (float *)&a;printf("%f\n", *ptr);

2、什么是严格别名

严格别名就是编译器当看到多个别名(alias)时,会在一定规则下默认它们指向不同的内存区域(即使它们实际上指向相同的内存区域),并以此进行优化,这可能会生成与我们期望不同的代码。

符合 strict aliasing,编译器认为 argv1,argv2 指向同一内存区域:

代码语言:javascript
复制
int a;void foo(char *argv1, int *argv2)foo((char *)(&a), &a);

违背 strict aliasing,编译器认为 argv1,argv2 指向不同的内存区域 ,为未定义的行为(UB,Undefined Behavior)。

代码语言:javascript
复制
int a;void foo(float *argv1, int *argv2)foo((float *)(&a), &a);

2.1 C11 (N1570)标准严格别名下规则


由于笔者主要从事网络领域编程,DPDK 采用 C11 标准的内存模型,因此这里只介绍 C11 标准。在 N1570 第 6.5 节的第 7 段:

对象的存储值只能由具有以下类型之一的左值表达式访问:

2.1.1 与对象的有效类型兼容的类型

代码语言:javascript
复制
int x = 1;int *ptr = &x;printf("%d\n", *ptr); // *ptr 是 int 类型的左值表达式,与 int 类型兼容(相同)
2.1.2 与对象的有效类型兼容类型的限定版本

代码语言:javascript
复制
int x = 1;const int *ptr = &x;printf("%d\n", *ptr); // *ptr 是 const int 类型的左值表达式,与 int 类型兼容
2.1.2 与对象的有效类型相对应的有符号或无符号类型的类型

例如,使用 signed int * ,或者 unsigned int * 作为 int 类型的别名。

代码语言:javascript
复制
int x = 1;unsigned int *ptr = (unsigned int*)&x;printf("%u\n", *ptr); // *ptr 是 unsigned int 类型的左值表达式,是 int 类型对应的无符号类型

注意, 使用 int * 作为 unsigned int 的别名,不符合标准,但 gcc 和 clang 都做了拓展,因此没有问题。参见:Why does gcc and clang allow assigning an unsigned int * to int * since they are not compatible types, although they may alias.

代码语言:javascript
复制
unsigned int x = 1;int *ptr = (int *)&x;printf("%d\n", *ptr); // No Warnning, No Error
2.1.3 类型是与对象的有效类型相对应的限定版本有符号或无符号类型

代码语言:javascript
复制
int x = 1;const unsigned int *ptr = (const unsigned int*)&x;printf("%u\n", *ptr );
2.1.4 struct 或 union 类型,其成员中包括上述类型之一(递归地包含 struct 或包含 union 的成员)

代码语言:javascript
复制
struct foo {    int x;};void foobar(struct foo *foo_ptr, int *int_ptr);  // f 是一个 struct 类型,并包含 int 类型,因此 *int_ptr 可以是 f.x 的别名。struct foo f;foobar(&f, &f.x);
2.1.5 字符类型

代码语言:javascript
复制
int x = 65; // ASCII 值 65 对应的字符为 Achar *ptr = (char *)&x;printf("%c\n", *ptr);  // *ptr 是 char 类型的左值表达式, char 类型可以作为任何类型的别名。

char 类型是严格别名规则下的银弹,可以作为任何类型的别名。不只是 char 类型,unsigned char,uint8_t, int8_t 也满足这条规则。

3、GCC 编译优化选项

GCC -O0, -O1 编译优化选项下开启严格别名(strict aliasing)规则的编译选项为:-fstrict-aliasing。

GCC -O2, -O3, -Os 编译优化选项下,严格别名(strict aliasing)规则默认开启。

具体的各个编译优化等级的优化参数,参考如下 GCC 手册:Options That Control Optimization

默认情况下无论是在 GCC -O0, -O1 优化下开启 -fstrict-aliasing,还是开启 GCC -O2, -O3, -Os 优化,如果想让违反严格别名规则代码在编译的时候产生告警需要增加 -Wstrict-aliasing 编译选项。

4、违反严格别名规则

下面我们举几个例子,在 GCC 开启 -O2 优化时,违反严格别名规则导致的未定义行为。

4.1 违反严格别名规则示例 1


4.1.1 开启 GCC -O2 导致示例 1 未定义的行为

代码语言:javascript
复制
#include <stdio.h>

int foo( float *f, int *i ) {
    *i = 1;               
    *f = 0.0f;            
   
   return *i;
}

int main() {

    int x = 0;
    
    printf("%d\n", x);

    x = foo((float*)(&x), &x);

    printf("%d\n", x);
}

在 GCC 开启 -O1编译优化时,输出结果为:

代码语言:javascript
复制
0
0

我们可以通过 godbolt 这个网站实时查看 C/C++ 代码的汇编代码:

在 GCC 开启 -O2编译优化时,输出结果为:

代码语言:javascript
复制
0
1
4.1.2 开启 -Wstrict-aliasing 编译参数

在本例中即使开启 -Wstrict-aliasing 严格别名告警编译参数,本例虽然违反了严格别名规则,在 x86-64 gcc 13.2 下也未收到任何编译告警提示。

4.1.3 开启 -fno-strict-aliasing 编译参数

开启 -fno-strict-aliasing 取消严格别名优化,修改 GCC -O2 导致的严格别名 Bug。

4.1.4 GCC 开启 -O2编译优化,避免严格别名 Bug 的方法

推荐处理顺序为从左到右:

代码语言:javascript
复制
改代码 > -fno-strict-aliasing > 不开 GCC -O2 优化 > -Wno-strict-aliasing (掩耳盗铃,强烈不建议)

Linux 内核的做法是:

代码语言:javascript
复制
在开启 GCC -O2 编译优化的同时开启 `-fno-strict-aliasing` 编译参数。

其实如果按照 GCC 那帮人的严格别名(Strict Aliasing)标准,Linux 代码有一半都跑不起来。2018 年 Linus Torvalds 就针对 Strict Aliasing 对 GCC 进行了开喷:device property: Get rid of union aliasing

5、整数环绕

在开启 GCC -O2 编译优化时,对于有符号整数的溢出,编译器认为其是未定义行为。

在 C11 标准的 3.4.3 小结对未定义行为进行了明确定义:

未定义行为:当使用不可移植或者错误的程序/错误的数据时,将导致不可预期的结果。典型例子就是整数溢出时的行为。

下文,我们通过几个整数溢出的示例进行说明:

5.1 整数溢出示例 1


代码语言:javascript
复制
#include <stdio.h>
#include <limits.h>

int f(int i) {
    return i+1 > i;
}

int main() {

    int x = INT_MAX;
    
    printf("%d\n", x);

    printf("%d\n", f(x));
}

在 GCC 开启 -O2 编译优化时,默认开启 -fstrict-overflow 编译优化,有符号整数的溢出行为为未定义行为,输出结果为:

代码语言:javascript
复制
2147483647
1

此时 GCC 编译器认为 i+1 恒大于 i,因此该函数永远返回 true

在 GCC 开启 -O2 -fwrapv-O2 -fno-strict-overflow 编译参数后,输出结果为:

代码语言:javascript
复制
2147483647
0

-fwrapv 编译选项指示 GCC 编译器假定加法、减法和乘法的有符号算术溢出使用二进制补码表示进行环绕。

#include <limits.h> 头文件中有两个宏定义,INT_MAX:2147483647(整形最大值),INT_MIN:-2147483648(整形最小值),x 初始化为:INT_MAX(2147483647/0x7FFFFFFF),x + 1 后发生溢出,导致新值回绕,变为 INT_MIN(-2147483648/0x80000000)。

因此最终表达式为:-2147483648 > 2147483647,因此返回 false 即 0。

5.1 整数溢出示例 2


代码语言:javascript
复制
#include <stdio.h>

int main() {
    
    for (int i=0; i>=0; i++) {
        printf("%d\n", i);
    }
}

在 GCC 开启 -O2 编译优化时,默认开启 -fstrict-overflow 编译优化,有符号整数的溢出行为为未定义行为,在 i 到达值 INT_MAX 后,评估 i++ 经常生未定义的行为,编译器会产生死循环。

而在 GCC 开启 -O2 -fwrapv 编译参数时,循环将在执行 INT_MAX 次后停止。

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-02-18,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 Flowlet 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.1 类型双关(type punning)
  • 2.1 C11 (N1570)标准严格别名下规则
    • 2.1.1 与对象的有效类型兼容的类型
      • 2.1.2 与对象的有效类型兼容类型的限定版本
        • 2.1.2 与对象的有效类型相对应的有符号或无符号类型的类型
          • 2.1.3 类型是与对象的有效类型相对应的限定版本有符号或无符号类型
            • 2.1.4 struct 或 union 类型,其成员中包括上述类型之一(递归地包含 struct 或包含 union 的成员)
              • 2.1.5 字符类型
              • 4.1 违反严格别名规则示例 1
                • 4.1.1 开启 GCC -O2 导致示例 1 未定义的行为
                  • 4.1.2 开启 -Wstrict-aliasing 编译参数
                    • 4.1.3 开启 -fno-strict-aliasing 编译参数
                      • 4.1.4 GCC 开启 -O2编译优化,避免严格别名 Bug 的方法
                      • 5.1 整数溢出示例 1
                      • 5.1 整数溢出示例 2
                      领券
                      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
                      http://www.vxiaotou.com