为什么编译器会生成一遍又一遍地在同一个内存位置写入相同内容的代码?

时间:2012-08-15 13:42:37

标签: c optimization gcc llvm-clang

我使用clanggcc编译了以下代码,并使用-O3调用了这两个代码:

#include <stdio.h>
#include <stdlib.h>

static void a(int n) {
  if (n == 0) return;

  printf("descending; a=%i\n", n);
  a(n-1);
}

int main() {
  a(5);
  return 0;
}

这是生成的主要函数gcc(最后没有NOP):

08048310 <main>:
 8048310:   55                      push   %ebp
 8048311:   89 e5                   mov    %esp,%ebp
 8048313:   83 e4 f0                and    $0xfffffff0,%esp
 8048316:   83 ec 10                sub    $0x10,%esp
 8048319:   c7 44 24 04 05 00 00    movl   $0x5,0x4(%esp)
 8048320:   00 
 8048321:   c7 04 24 14 85 04 08    movl   $0x8048514,(%esp)
 8048328:   e8 c7 ff ff ff          call   80482f4 <printf@plt>
 804832d:   c7 44 24 04 04 00 00    movl   $0x4,0x4(%esp)
 8048334:   00 
 8048335:   c7 04 24 14 85 04 08    movl   $0x8048514,(%esp)
 804833c:   e8 b3 ff ff ff          call   80482f4 <printf@plt>
 8048341:   c7 44 24 04 03 00 00    movl   $0x3,0x4(%esp)
 8048348:   00 
 8048349:   c7 04 24 14 85 04 08    movl   $0x8048514,(%esp)
 8048350:   e8 9f ff ff ff          call   80482f4 <printf@plt>
 8048355:   c7 44 24 04 02 00 00    movl   $0x2,0x4(%esp)
 804835c:   00 
 804835d:   c7 04 24 14 85 04 08    movl   $0x8048514,(%esp)
 8048364:   e8 8b ff ff ff          call   80482f4 <printf@plt>
 8048369:   c7 44 24 04 01 00 00    movl   $0x1,0x4(%esp)
 8048370:   00 
 8048371:   c7 04 24 14 85 04 08    movl   $0x8048514,(%esp)
 8048378:   e8 77 ff ff ff          call   80482f4 <printf@plt>
 804837d:   31 c0                   xor    %eax,%eax
 804837f:   c9                      leave  
 8048380:   c3                      ret    

这是来自clang的那个:

080483d0 <main>:
 80483d0:   55                      push   %ebp
 80483d1:   89 e5                   mov    %esp,%ebp
 80483d3:   56                      push   %esi
 80483d4:   83 ec 0c                sub    $0xc,%esp
 80483d7:   be 05 00 00 00          mov    $0x5,%esi
 80483dc:   8d 74 26 00             lea    0x0(%esi,%eiz,1),%esi
 80483e0:   89 74 24 04             mov    %esi,0x4(%esp)
 80483e4:   c7 04 24 e0 84 04 08    movl   $0x80484e0,(%esp)
 80483eb:   e8 04 ff ff ff          call   80482f4 <printf@plt>
 80483f0:   4e                      dec    %esi
 80483f1:   75 ed                   jne    80483e0 <main+0x10>
 80483f3:   31 c0                   xor    %eax,%eax
 80483f5:   83 c4 0c                add    $0xc,%esp
 80483f8:   5e                      pop    %esi
 80483f9:   5d                      pop    %ebp
 80483fa:   c3                      ret    

我的问题是:是否有充分的理由说明为什么它们都会生成一遍又一遍地在堆栈中写入静态字符串地址的代码?例如,为什么代码clang生成的代码不是这样的呢?

080483d0 <main>:
 80483d0:   55                      push   %ebp
 80483d1:   89 e5                   mov    %esp,%ebp
 80483d3:   56                      push   %esi
 80483d4:   83 ec 0c                sub    $0xc,%esp
 80483d7:   be 05 00 00 00          mov    $0x5,%esi
 80483dc:   8d 74 26 00             lea    0x0(%esi,%eiz,1),%esi
 80483e0:   c7 04 24 e0 84 04 08    movl   $0x80484e0,(%esp)
 80483e7:   89 74 24 04             mov    %esi,0x4(%esp)
 80483eb:   e8 04 ff ff ff          call   80482f4 <printf@plt>
 80483f0:   4e                      dec    %esi
 80483f1:   xx xx                   jne    80483e7 <main+0x17>
 80483f3:   31 c0                   xor    %eax,%eax
 80483f5:   83 c4 0c                add    $0xc,%esp
 80483f8:   5e                      pop    %esi
 80483f9:   5d                      pop    %ebp
 80483fa:   c3                      ret    

3 个答案:

答案 0 :(得分:2)

主要原因是ABI如何定义“堆栈框架”的所有权。简而言之,只要调用一个函数,该函数就会获得当前堆栈帧的所有权(包括参数,返回地址和任何局部变量)。

它可以用它做任何。无法保证printf()保持堆栈帧中的任何内容不变。虽然这样做是不好的做法,但函数可以做到这一点,我编写的代码与堆栈混淆(例如在C语言中,用C语言编写,可以调用任何已编译的函数,尤其是printf())。 / p>

因此,当printf()返回时,无法保证指向静态字符串的指针仍在堆栈中。请注意,编译器不能假定printf()将表现为行为,因为您可以使用printf()的自定义实现链接到不同的C运行时。

尝试调用GCC可以看到源代码的函数。对于这些,它可能会进一步优化堆栈。

[编辑] 也许一个不同的例子让这更容易理解。在某些情况下,GCC优化函数参数传递。它们将被传递到CPU寄存器中,而不是将它们推入堆栈。我们假设它使用%eax。如果参数由调用函数拥有,则不允许被调用函数修改%eax

答案 1 :(得分:2)

在Mac OS X应用程序二进制接口中,可以通过被调用例程修改堆栈上传递的参数。因此,在调用例程将格式字符串的地址放在堆栈上的地方,允许被调用例程写入堆栈中的那个位置。 (通常不允许写入堆栈中较高的[较早]位置。)因此,调用例程无法知道在调用例程返回后,调用例程写入堆栈的参数不变。

这对于可能在使用它们执行计算时修改其参数的例程很方便。例如,您可以使用以下代码:

int foo(int x)
{
    x *= x;
    return x+3;
}

显然,对于这么简单的代码,编译器实际上不需要将产品存储在x中以完成计算返回值。但是,对于更复杂的例程,您可以看到编译器可能决定将值存储到x的位置。

作为ABI设计问题,您可能想知道禁止被调用例程使用此空间是否更好,以便调用者可以依赖不更改的值。但是,以这种方式重复使用参数并不是很常见,编写新副本的成本很小。

另请注意,只有写入堆栈的格式字符串的地址可能会更改。 C语义要求格式字符串的内容保持不变,因为它是const char。

答案 2 :(得分:-1)

短暂的问题是函数调用。 C编译器实际上无法保证函数调用中发生的事情(除非它是C内置函数之一,例如strlen)。所以,它必须不围绕它们进行优化。考虑:

pthread_mutex_lock(&mutex);
val = 5;
pthread_mutex_unlock(&mutex);

如果编译器推断对val的写入与pthread_mutex_lock的调用无关,那么它可以在锁定之前推送它,并且随后会发生质量混乱。

编译器必须假定必须在函数调用之前完成最后一次写操作。如果写入发生在代码块中,它确切地知道发生了什么(即,没有函数调用或只有内置函数),那么它可以随心所欲地进行优化并拉出不需要的写入。

编译器无法推断函数是否可以访问变量(至少在没有解决暂停问题的情况下)。另一个函数可能在之前设置了地址,或使用类似dlsym之类的东西来计算它自己的二进制文件的地址,即使该符号未被导出。