gcc 固定编译器的未定义行为的一致性

yfwxisqw  于 2022-11-12  发布在  其他
关注(0)|答案(2)|浏览(159)

我想知道编译器是如何处理未定义行为的。我将以x86架构和-O2 -std=c++03标志的GCC 10.4为例,但请随意评论其他编译器。如何改变带有UB的操作的结果?语言标准没有规定如果操作带有UB应该发生什么,但编译器会做一些事情。也就是说,我不是从C的Angular 而是从编译器的Angular 来问UB中发生了什么。我知道C标准没有对程序的行为施加任何限制。
例如,如果我有UB,因为某个内存位置中的对象值被表达式的求值多次修改,如下所示:

int i = 0;
i = ++i + i++; // UB pre-C++11

在该设置中所选择的编译器生成汇编代码,该汇编代码将计算减少为常数,在这种情况下为3,参见https://godbolt.org/z/MEEGT15dM
如果我不改变编译器、版本、标志或体系结构,什么会导致常量变成3以外的任何值?在错误语句之前编辑函数而不改变i的值会导致它吗?

lvjbypge

lvjbypge1#

C和C++语言标准将“未定义行为”定义为标准没有强制要求的行为。请注意强调的部分。特别是,这并不意味着 * 作为一个整体 * 对该行为没有要求,而只是从语言标准的Angular 来看。编译器可能会寻求符合其他规范的要求,包括自己的规范。
编译器通常支持许多语言标准意义上的“未定义行为”,例如:

  • 链接以多种编程语言编写的代码,
  • 调用显示图形或执行网络通信或执行其它操作系统服务的操作系统例程,
  • 提供用于特殊对准请求和其它可变属性的特征,
  • 允许将汇编语言插入到C或C++代码中,
  • 提供例程或操作来对字中的位进行计数、找到第一位组、执行具有溢出处理的算术
  • 提供对SIMD功能的支持,以及
  • 在函数中定义函数。

编译器支持的任何东西都应该是稳定的;它不应该受到改变优化开关、语言变量选择开关或其他开关的影响,除非编译器记录了这些开关。因此这些“未定义的行为”应该是一致的。
除此之外,有些东西既不是由适用的语言标准定义的,也不是由编译器定义的(直接在它自己的文档中,或者间接通过它试图遵循的规范)。在大多数情况下,你应该认为这些是不稳定的。当优化开关改变时,当其他代码改变时,当存储器使用的模式或存储器的内容改变时等等。
虽然你通常不能依赖这样的行为,但这并不意味着它们没有模式。它们是设计中产生的属性。有经验的程序员可能会将某些症状识别为程序中错误的线索。即使行为是未定义的(由语言标准和编译器定义),但由于我们设计软件的方式,它仍然可能落入一个模式。例如,溢出缓冲区可能会破坏堆栈上更高(更早)的数据。这并不保证打开;优化可以改变缓冲区溢出时发生的情况,但这仍然是一个常见的结果。此外,这是一些人所依赖的结果。恶意的人可能试图利用缓冲区溢出来攻击程序并窃取信息或金钱,控制系统,或崩溃或以其他方式造成拒绝服务。他们利用的行为不是随机的;它至少在一定程度上是可预测的,这就为他们提供了利用它的机会。优秀的程序员必须考虑未定义行为的后果并设法减轻它。
如果我不更改编译器、版本、标志或体系结构,什么会导致常量变为3以外的任何值?
在大多数情况下,如果你对编译不做任何改变,你每次都应该得到相同的结果,只有少数例外。如果编译器没有错误,那么它的行为应该由它的源代码定义(即使我们,用户,不知道定义是什么),这意味着,给定相同的输入和环境,它应该产生相同的输出。
一个例外是,编译器可能会将日期或时间信息注入到其输出中。类似地,执行环境中的其他变化可能会导致一些变化。另一个问题是,编译器的输出是目标代码,而目标代码不是完整的程序,因此最终的程序可能会受到其他因素的影响。一个例子是,现代多用户操作系统通常使用address space layout randomization,所以程序中的许多地址在不同的执行中会有所不同。这不太可能影响您的i = ++i + i++;示例,但这意味着其他导致未定义行为的bug可能会由于所涉及的地址而表现出一些随机性。

dgsult0t

dgsult0t2#

名称C用于描述共享一些核心语言特征的各种语言方言。该标准被特许以一种不可知论的方式描述那些方言的共同核心特征,而这些特征虽然共同,但并不通用。虽然该标准不要求实现以与“高级汇编程序”一致的方式行为,本标准的作者 * 明确 * 表示(引用如下),他们不希望排除为此目的使用该语言。
如果一个实现被设计成适合在特定平台上的低级编程,它将“以环境特征的文档化方式”处理许多构造,如果这样做对它的客户有用,而不管标准是否要求它这样做。
棘手的是,当启用优化时,一些编译器被设计为识别标准不会对特定函数的行为施加任何要求的情况,除非接收到特定的输入,然后用盲目地假设它们会被接收的机器代码来替换源代码中检查是否接收到这些输入的部分。
如果函数接收到的所有输入都与这些假设一致,则这种替换将是有用的,但是如果函数接收到的输入在“以环境特征的文档化方式”进行处理的情况下会产生可接受的--甚至是有用的--行为,但其行为不是标准所要求的,则这种替换将是灾难性的。
如果考虑到这样一个事实,事情就变得更加棘手了,即处理整数运算的实现方式可能并不总是产生可预测的值,但除了产生可能无意义的值之外,不可能有任何副作用,如果作者无法想象目标平台的编译器无法支持后一种保证 *,那么很少会记录这种保证 *。不幸的是,该标准没有提供区分支持后一种保证的实现与不支持后一种保证的实现的方法,因此允许程序员引入有用的优化,这些优化可能会使以一种方式运行的程序以一种 * 明显不同但同样可接受的 * 方式运行。
任何想了解未定义行为的人都应该做两件事:
1.阅读已发布的C标准基本原理文档,位于https://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf(最值得注意的是第11页,从第23行开始,但也包括第2页第32-36行;而且还包括第13页第5-8行;第44页第20行开始,以及第60页第17-19行)。
1.认识到,虽然基本原理文档描述了委员会被授权描述的语言,但一些编译器维护者积极地看待标准未能要求编译器正确处理代码的情况,或以与标准作者期望的“最新实现”一致的方式处理代码的情况,这意味着没有可能的处理这种情况的方法会比任何其他方法更糟糕。

相关问题