繁体   English   中英

功能已优化为'gcc -O2'的无限循环

[英]Function optimized to infinite loop at 'gcc -O2'

语境
我的一位朋友问我以下难题:

void fn(void)
{
  /* write something after this comment so that the program output is 10 */
  /* write something before this comment */
}

int main()
{
  int i = 5;
  fn();
  printf("%d\n", i);
  return 0;
}

我知道可以有多种解决方案,有些涉及宏,有些假设有关实现并违反C。

我感兴趣的一种特定解决方案是对堆栈进行某些假设并编写以下代码:(我知道这是未定义的行为,但在许多实现上都可以按预期工作)

void fn(void)
{
  /* write something after this comment so that the program output is 10 */
  int a[1] = {0};
  int j = 0;
  while(a[j] != 5) ++j;  /* Search stack until you find 5 */
  a[j] = 10;             /* Overwrite it with 10 */
  /* write something before this comment */
}

问题
该程序在MSVC和gcc中无需优化即可正常运行。 但是当我用gcc -O2标志编译它或尝试使用ideone时 ,它在函数fn无限循环。

我的观察
当我用gcc -S vs gcc -S -O2编译文件并进行比较时,它清楚地表明gcc在函数fn保持了无限循环。


我了解,因为代码调用了未定义的行为,所以不能称其为错误。 但是,为什么以及如何编译器分析行为并在O2处留下无限循环?


如果将某些变量更改为volatile,许多人表示要了解行为。 预期结果是:

  • 如果ij更改为volatile ,则程序行为保持不变。
  • 如果将数组avolatile ,则程序不会遭受无限循环。
  • 此外,如果我应用以下补丁
-  int a[1] = {0};
+  int aa[1] = {0};
+  int *a = aa;

程序行为保持不变(无限循环)

如果使用gcc -O2 -fdump-tree-optimized编译代码, gcc -O2 -fdump-tree-optimized得到以下中间文件:

;; Function fn (fn) (executed once)

Removing basic block 3
fn ()
{
<bb 2>:

<bb 3>:
  goto <bb 3>;

}



;; Function main (main) (executed once)

main ()
{
<bb 2>:
  fn ();

}
Invalid sum of incoming frequencies 0, should be 10000

这将验证在以下答案之后做出的断言。

这是未定义的行为,因此编译器实际上可以执行任何操作,我们可以在GCC 4.8之前的Breaks Broken SPEC 2006 Benchmarks中找到一个类似的示例,其中gcc采取具有未定义行为的循环并将其优化为:

L2:
    jmp .L2

文章说( 重点是我的 ):

当然,这是一个无限循环。 由于SATD()无条件执行未定义的行为(这是3型函数),因此对于正确的C编译器任何转换(或根本没有转换)都是完全可以接受的行为 未定义的行为是在退出循环之前访问d [16]。 在C99中,创建指向数组末尾一个位置的元素的指针是合法的,但是不得取消引用该指针 同样,不得访问数组末尾的一个数组元素。

如果我们使用Godbolt检查您的程序, 将会看到:

fn:
.L2:
    jmp .L2

优化器使用的逻辑可能类似于以下内容:

  • 所有的元素a初始化为零
  • a永远不会在循环之前或循环内进行修改
  • 因此a[j] != 5始终为true->无限循环
  • 由于无穷大, a[j] = 10; 是不可访问的,因此可以进行优化, aj也可以,因为不再需要它们来确定循环条件。

这与以下文章中的情况类似:

int d[16];

分析以下循环:

for (dd=d[k=0]; k<16; dd=d[++k]) 

像这样:

一旦看到d [++ k],就可以假定k的增量值在数组范围内,因为否则会发生未定义的行为。 对于此处的代码,GCC可以推断出k在0..15范围内。 稍后,当GCC看到k <16时,它自言自语:“啊哈-这个表达式总是正确的,所以我们有一个无限循环。”

也许有趣的第二点是,是否将无限循环视为可观察到的行为( 即按规则 ),这影响了是否也可以优化无限循环。 C编译器反驳Fermat的最后定理可以看出,在C11之前至少存在一些解释的空间:

许多有知识的人(包括我在内)都读这句话,说不得更改程序的终止行为。 显然,有些编译器作者不同意,或者不认为这很重要。 理性的人不同意这种解释的事实似乎表明C标准是有缺陷的。

C11在第6.8.5节“ 迭代语句”中添加了说明,并且在此答案中进行了更详细的介绍。

在优化版本中,编译器已经决定了一些事项:

  1. 在测试之前,数组a不会改变。
  2. 数组a不包含5

因此,我们可以将代码重写为:

void fn(void) {
  int a[1] = {0};
  int j = 0;
  while(true) ++j;
  a[j] = 10;
}

现在,我们可以做出进一步的决定:

  1. while循环之后的所有代码均为无效代码(无法访问)。
  2. j已写入但从未读取。 这样我们就可以摆脱它。
  3. a从未读过。

此时,您的代码已减少为:

void fn(void) {
  int a[1] = {0};
  while(true);
}

我们可以记下a现在不再被读取,因此我们也将其删除:

void fn(void) {
  while(true);
}

现在,未优化的代码:

在未优化的生成代码中,阵列将保留在内存中。 您实际上将在运行时遍历它。 一旦您经过数组的末尾,它后面可能会有一个5可读性。

这就是为什么未优化的版本有时不会崩溃和燃烧的原因。

如果循环确实被优化成无限循环,则可能是由于静态代码分析看到您的数组是

  1. 不易volatile

  2. 仅包含0

  3. 永远不会被写入

因此不可能包含数字5 这意味着无限循环。

即使没有做到这一点,您的方法也很容易失败。 例如,某些编译器有可能在不使循环变为无限的情况下优化代码,但会将i的内容填充到寄存器中,从而使其无法从堆栈中使用。

作为附带说明,我敢打赌,您的朋友实际期望的是:

void fn(void)
{
  /* write something after this comment so that the program output is 10 */
  printf("10\n"); /* Output 10 */
  while(1); /* Endless loop, function won't return, i won't be output */
  /* write something before this comment */
}

或这个(如果包括stdlib.h ):

void fn(void)
{
  /* write something after this comment so that the program output is 10 */
  printf("10\n"); /* Output 10 */
  exit(0); /* Exit gracefully */
  /* write something before this comment */
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM