简体   繁体   English

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

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

Context 语境
I was asked the following puzzle by one of my friends: 我的一位朋友问我以下难题:

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;
}

I know there can be multiple solutions, some involving macro and some assuming something about the implementation and violating C. 我知道可以有多种解决方案,有些涉及宏,有些假设有关实现并违反C。

One particular solution I was interested in is to make certain assumptions about stack and write following code: (I understand it is undefined behavior, but may work as expected on many implementations) 我感兴趣的一种特定解决方案是对堆栈进行某些假设并编写以下代码:(我知道这是未定义的行为,但在许多实现上都可以按预期工作)

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 */
}

Problem 问题
This program worked fine in MSVC and gcc without optimization. 该程序在MSVC和gcc中无需优化即可正常运行。 But when I compiled it with gcc -O2 flag or tried on ideone , it loops infinitely in function fn . 但是当我用gcc -O2标志编译它或尝试使用ideone时 ,它在函数fn无限循环。

My Observation 我的观察
When I compiled the file with gcc -S vs gcc -S -O2 and compared, it clearly shows gcc kept an infinite loop in function fn . 当我用gcc -S vs gcc -S -O2编译文件并进行比较时,它清楚地表明gcc在函数fn保持了无限循环。

Question
I understand because the code invokes undefined behavior, one can not call it a bug. 我了解,因为代码调用了未定义的行为,所以不能称其为错误。 But why and how does compiler analyze the behavior and leave an infinite loop at O2 ? 但是,为什么以及如何编译器分析行为并在O2处留下无限循环?


Many people commented to know the behavior if some of the variables are changed to volatile. 如果将某些变量更改为volatile,许多人表示要了解行为。 The result as expected is: 预期结果是:

  • If i or j is changed to volatile , program behavior remains same. 如果ij更改为volatile ,则程序行为保持不变。
  • If array a is made volatile , program does not suffer infinite loop. 如果将数组avolatile ,则程序不会遭受无限循环。
  • Moreover if I apply the following patch 此外,如果我应用以下补丁
-  int a[1] = {0};
+  int aa[1] = {0};
+  int *a = aa;

The program behavior remains same (infinite loop) 程序行为保持不变(无限循环)

If I compile the code with gcc -O2 -fdump-tree-optimized , I get the following intermediate file: 如果使用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

This verifies the assertions made after the answers below. 这将验证在以下答案之后做出的断言。

This is undefined behavior so the compiler can really do anything at all, we can find a similar example in GCC pre-4.8 Breaks Broken SPEC 2006 Benchmarks , where gcc takes a loop with undefined behavior and optimizes it to: 这是未定义的行为,因此编译器实际上可以执行任何操作,我们可以在GCC 4.8之前的Breaks Broken SPEC 2006 Benchmarks中找到一个类似的示例,其中gcc采取具有未定义行为的循环并将其优化为:

L2:
    jmp .L2

The article says ( emphasis mine ): 文章说( 重点是我的 ):

Of course this is an infinite loop. 当然,这是一个无限循环。 Since SATD() unconditionally executes undefined behavior (it's a type 3 function), any translation (or none at all) is perfectly acceptable behavior for a correct C compiler . 由于SATD()无条件执行未定义的行为(这是3型函数),因此对于正确的C编译器任何转换(或根本没有转换)都是完全可以接受的行为 The undefined behavior is accessing d[16] just before exiting the loop. 未定义的行为是在退出循环之前访问d [16]。 In C99 it is legal to create a pointer to an element one position past the end of the array, but that pointer must not be dereferenced . 在C99中,创建指向数组末尾一个位置的元素的指针是合法的,但是不得取消引用该指针 Similarly, the array cell one element past the end of the array must not be accessed. 同样,不得访问数组末尾的一个数组元素。

which if we examine your program with godbolt we see: 如果我们使用Godbolt检查您的程序, 将会看到:

fn:
.L2:
    jmp .L2

The logic being used by the optimizer probably goes something like this: 优化器使用的逻辑可能类似于以下内容:

  • All the elements of a are initialized to zero 所有的元素a初始化为零
  • a is never modified before or within the loop a永远不会在循环之前或循环内进行修改
  • So a[j] != 5 is always true -> infinite loop 因此a[j] != 5始终为true->无限循环
  • Because of the infinite, the a[j] = 10; 由于无穷大, a[j] = 10; is unreachable and so that can be optimized away, so can a and j since they are no longer needed to determine the loop condition. 是不可访问的,因此可以进行优化, aj也可以,因为不再需要它们来确定循环条件。

which is similar to the case in the article which given: 这与以下文章中的情况类似:

int d[16];

analyzes the following loop: 分析以下循环:

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

like this: 像这样:

upon seeing d[++k], is permitted to assume that the incremented value of k is within the array bounds, since otherwise undefined behavior occurs. 一旦看到d [++ k],就可以假定k的增量值在数组范围内,因为否则会发生未定义的行为。 For the code here, GCC can infer that k is in the range 0..15. 对于此处的代码,GCC可以推断出k在0..15范围内。 A bit later, when GCC sees k<16, it says to itself: “Aha– that expression is always true, so we have an infinite loop.” 稍后,当GCC看到k <16时,它自言自语:“啊哈-这个表达式总是正确的,所以我们有一个无限循环。”

Perhaps an interesting secondary point, is whether an infinite loop is considered observable behavior( wrt to the as-if rule ) or not, which effects whether an infinite loop can also be optimized away. 也许有趣的第二点是,是否将无限循环视为可观察到的行为( 即按规则 ),这影响了是否也可以优化无限循环。 We can see from C Compilers Disprove Fermat's Last Theorem that before C11 there was at least some room for interpretation: C编译器反驳Fermat的最后定理可以看出,在C11之前至少存在一些解释的空间:

Many knowledgeable people (including me) read this as saying that the termination behavior of a program must not be changed. 许多有知识的人(包括我在内)都读这句话,说不得更改程序的终止行为。 Obviously some compiler writers disagree, or else don't believe that it matters. 显然,有些编译器作者不同意,或者不认为这很重要。 The fact that reasonable people disagree on the interpretation would seem to indicate that the C standard is flawed. 理性的人不同意这种解释的事实似乎表明C标准是有缺陷的。

C11 adds clarification to section 6.8.5 Iteration statements and is covered in more detail in this answer . C11在第6.8.5节“ 迭代语句”中添加了说明,并且在此答案中进行了更详细的介绍。

In the optimized version, the compiler has decided a few things: 在优化版本中,编译器已经决定了一些事项:

  1. The array a doesn't change before that test. 在测试之前,数组a不会改变。
  2. The array a doesn't contain a 5 . 数组a不包含5

Therefore, we can rewrite the code as: 因此,我们可以将代码重写为:

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

Now, we can make further decisions: 现在,我们可以做出进一步的决定:

  1. All the code after the while loop is dead code (unreachable). while循环之后的所有代码均为无效代码(无法访问)。
  2. j is written but never read. j已写入但从未读取。 So we can get rid of it. 这样我们就可以摆脱它。
  3. a is never read. a从未读过。

At this point, your code has been reduced to: 此时,您的代码已减少为:

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

And we can make the note that a is now never read, so let's get rid of it as well: 我们可以记下a现在不再被读取,因此我们也将其删除:

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

Now, the unoptimized code: 现在,未优化的代码:

In unoptimized generated code, the array will remain in memory. 在未优化的生成代码中,阵列将保留在内存中。 And you'll literally walk it at runtime. 您实际上将在运行时遍历它。 And it's possible that there will be a 5 thats readable after it once you walk past the end of the array. 一旦您经过数组的末尾,它后面可能会有一个5可读性。

Which is why the unoptimized version sometimes doesn't crash and burn. 这就是为什么未优化的版本有时不会崩溃和燃烧的原因。

If the loop does get optimized out into an infinite loop, it could be due to static code analyzis seeing that your array is 如果循环确实被优化成无限循环,则可能是由于静态代码分析看到您的数组是

  1. not volatile 不易volatile

  2. contains only 0 仅包含0

  3. never gets written to 永远不会被写入

and thus it is not possible for it to contain the number 5 . 因此不可能包含数字5 Which means an infinite loop. 这意味着无限循环。

Even if it didn't do this, your approach could fail easily. 即使没有做到这一点,您的方法也很容易失败。 For example, it's possible that some compiler would optimize your code without making your loop infinite, but would stuff the contents of i into a register, making it unavailable from the stack. 例如,某些编译器有可能在不使循环变为无限的情况下优化代码,但会将i的内容填充到寄存器中,从而使其无法从堆栈中使用。

As a side note, I bet what your friend actually expected was this: 作为附带说明,我敢打赌,您的朋友实际期望的是:

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 */
}

or this (if stdlib.h is included): 或这个(如果包括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