汇编代码 - GCC 优化与未优化

逆向工程 拆卸 部件
2021-06-24 20:58:33

我想更多地了解 GCC 如何优化 C 程序。我对优化和未优化的随机函数做了一个 disas,我想看看其中的一些差异。在我的脑海中,优化的程序集跳跃较少,并且似乎主要使用寄存器,而未优化的程序集更频繁地使用内存。这两者还有什么其他不同需要注意的?

C代码

uint countPairsUpTo(int index, int* intArray, int first, int second)
{
  uint i;
  uint sum = 0;

  for (i = 0; i < index; i++)
    if ((first == intArray[i]) && (second == intArray[i+2]))
      sum++;

  return sum ;
}

未优化

0x080485b1 <countPairsUpTo+0>:  push   %ebp
0x080485b2 <countPairsUpTo+1>:  mov    %esp,%ebp
0x080485b4 <countPairsUpTo+3>:  sub    $0x10,%esp
0x080485b7 <countPairsUpTo+6>:  call   0x8048418 <mcount@plt>
0x080485bc <countPairsUpTo+11>: movl   $0x0,-0x4(%ebp)
0x080485c3 <countPairsUpTo+18>: movl   $0x0,-0x8(%ebp)
0x080485ca <countPairsUpTo+25>: jmp    0x80485fa <countPairsUpTo+73>
0x080485cc <countPairsUpTo+27>: mov    -0x8(%ebp),%eax
0x080485cf <countPairsUpTo+30>: shl    $0x2,%eax
0x080485d2 <countPairsUpTo+33>: add    0xc(%ebp),%eax
0x080485d5 <countPairsUpTo+36>: mov    (%eax),%eax
0x080485d7 <countPairsUpTo+38>: cmp    0x10(%ebp),%eax
0x080485da <countPairsUpTo+41>: jne    0x80485f6 <countPairsUpTo+69>
0x080485dc <countPairsUpTo+43>: mov    0xc(%ebp),%edx
0x080485df <countPairsUpTo+46>: add    $0x8,%edx
0x080485e2 <countPairsUpTo+49>: mov    -0x8(%ebp),%eax
0x080485e5 <countPairsUpTo+52>: shl    $0x2,%eax
0x080485e8 <countPairsUpTo+55>: lea    (%edx,%eax,1),%eax
0x080485eb <countPairsUpTo+58>: mov    (%eax),%eax
0x080485ed <countPairsUpTo+60>: cmp    0x14(%ebp),%eax
0x080485f0 <countPairsUpTo+63>: jne    0x80485f6 <countPairsUpTo+69>
0x080485f2 <countPairsUpTo+65>: addl   $0x1,-0x4(%ebp)
0x080485f6 <countPairsUpTo+69>: addl   $0x1,-0x8(%ebp)
0x080485fa <countPairsUpTo+73>: mov    0x8(%ebp),%eax
0x080485fd <countPairsUpTo+76>: cmp    -0x8(%ebp),%eax
0x08048600 <countPairsUpTo+79>: ja     0x80485cc <countPairsUpTo+27>
0x08048602 <countPairsUpTo+81>: mov    -0x4(%ebp),%eax
0x08048605 <countPairsUpTo+84>: leave  
0x08048606 <countPairsUpTo+85>: ret    

优化

0x08048570 <countPairsUpTo+0>:  push   %ebp
0x08048571 <countPairsUpTo+1>:  mov    %esp,%ebp
0x08048573 <countPairsUpTo+3>:  push   %edi
0x08048574 <countPairsUpTo+4>:  push   %esi
0x08048575 <countPairsUpTo+5>:  push   %ebx
0x08048576 <countPairsUpTo+6>:  call   0x8048418 <mcount@plt>
0x0804857b <countPairsUpTo+11>: mov    0xc(%ebp),%ebx
0x0804857e <countPairsUpTo+14>: mov    0x10(%ebp),%esi
0x08048581 <countPairsUpTo+17>: mov    0x8(%ebp),%ecx
0x08048584 <countPairsUpTo+20>: mov    $0x0,%edi
0x08048589 <countPairsUpTo+25>: test   %ecx,%ecx
0x0804858b <countPairsUpTo+27>: je     0x80485b2 <countPairsUpTo+66>
0x0804858d <countPairsUpTo+29>: mov    $0x0,%edi
0x08048592 <countPairsUpTo+34>: mov    $0x0,%edx
0x08048597 <countPairsUpTo+39>: cmp    %esi,(%ebx,%edx,4)
0x0804859a <countPairsUpTo+42>: jne    0x80485ab <countPairsUpTo+59>
0x0804859c <countPairsUpTo+44>: mov    0x14(%ebp),%eax
0x0804859f <countPairsUpTo+47>: cmp    %eax,0x8(%ebx,%edx,4)
0x080485a3 <countPairsUpTo+51>: sete   %al
0x080485a6 <countPairsUpTo+54>: movzbl %al,%eax
0x080485a9 <countPairsUpTo+57>: add    %eax,%edi
0x080485ab <countPairsUpTo+59>: add    $0x1,%edx
0x080485ae <countPairsUpTo+62>: cmp    %ecx,%edx
0x080485b0 <countPairsUpTo+64>: jne    0x8048597 <countPairsUpTo+39>
0x080485b2 <countPairsUpTo+66>: mov    %edi,%eax
0x080485b4 <countPairsUpTo+68>: pop    %ebx
0x080485b5 <countPairsUpTo+69>: pop    %esi
0x080485b6 <countPairsUpTo+70>: pop    %edi
0x080485b7 <countPairsUpTo+71>: pop    %ebp
0x080485b8 <countPairsUpTo+72>: ret    
2个回答

嗯,有许多优化技术由GCC. 此类优化从死代码消除到循环展开、函数内联等。在解释优化技术之前,我将从编译器在优化之前所做的事情开始。

通常对IR提供的代码(中间表示)执行优化大多数编译器将输入代码 ( C, C++, Fortran, ...) 翻译成IR. 该转换由将*front-end*提供IR*middle-end*将应用优化传递并将其提供给*back-end*然后将生成机器代码的 执行。

GCCIR 中称为 GIMPLE 并在链接中简要介绍什么GCC也做的是转换其GIMPLE给定的代码到的表示SSA形式(静态单赋值),这是在描述1989年出版。

如果您点击链接,您将找到所有GCC带有简要说明的 SSA 优化通道而且我认为您还应该检查 RTL 通过。在这里,您会找到两个目录,其中详细说明了具体GCC功能。这些是您能找到的最可靠的参考资料,因为它们来自GCC人民。

您必须知道的是,某些优化会以不同的顺序执行多次。例如,死代码消除,包括从程序的CFG(控制流图)中消除孤岛,可以在某些可能导致孤岛创建的优化之后执行。

这是一个例子。假设您有一个如下所示的代码:

   int x = 1;

   //Some code that doesn't alter the value of x

   if (x == 2)
    { BLOCK1; }
   else
      { BLOCK2; }

现在,假设编译器按以下顺序应用优化:

  1. 死代码消除
  2. 分支预测

当然,第一遍不会在CFG. 另一方面,第二个(分支预测)会发现 x 的值没有改变,并且从声明和初始化到在 if 条件中的使用,它一直与 2 不同。这意味着整个 if 语句必须被消除并替换为 BLOCK2。因此,在分支预测之后需要进行死代码消除过程。

另一个优化是循环展开。它包括复制循环体和增加步幅以填充 CPU 管道并具有更好的缓存位置。例如,这个减少循环可以展开 4 次以获得一些循环并改善缓存访问:

   //Ununrolled version
   for (int i = 0; i < N; i++)
       r += t[i];

   //Unrolled version handling any value of N 
   for (int i = 0; i < (N & ~3); i += 4)
     {
        r += t[i]; 
        r += t[i + 1]; 
        r += t[i + 2]; 
        r += t[i + 3]; 
     }

    //Handling the rest of the array elements 
    for (int i = (N & ~3); i < N; i++)
        r += t[i];

如果我们假设 t 是一个浮点数组(sizeof(float) = 4bytes),展开 4 次意味着每次迭代访问 16 字节。如果此代码在英特尔 SandyBridge上运行,它将表现良好,但效果不佳(如果未激活预取)。为什么 ?因为SandyBridge微架构上的缓存线大小为 64 字节,并且循环每次迭代访问缓存线的 1/4。展开 16 次肯定会带来更好的性能。

这种优化技术的难点在于为正确的循环找到正确的展开因子。选择太大或太小的值都会导致性能下降。此外,底层架构起着非常重要的作用(PentiumHaswell上的缓存大小不相似,重新排序缓冲区和管道的大小相同)。一些编译器所做的是对具有不同展开因子的循环执行静态或动态分析,并选择具有最佳配置文件的循环。

这些优化通常在 IR 上执行,但也可以在汇编代码上执行。还有其他与汇编紧密相关的优化,例如指令重新排序。这种优化包括改变指令的顺序,以获得更好的指令流水线或并行支持。有时,如果指令之间存在依赖关系,并且如果重新排序会带来性能的巨大收益,那么依赖关系将被打破,指令将被重新排序。
下面的代码显示了重新排序指令如何产生更好的结构:

   //Ordered
   mov eax , [@1]
   add eax , ecx
   mov [@0], eax    
   mul ecx , ebx
   sub edx , eax

   //Reordered 
   mov eax , [@1]
   add eax , ecx
   sub edx , eax
   mul ecx , ebx
   mov [@0], eax

很明显,有序和重新排序的代码执行相同的操作,但重新排序的版本将比有序版本消耗更少的周期,因为addsubmul指令属于同一类,不会在管道中相互阻塞. 您还可以注意到内存操作位于代码的末端。这种模式通常比使用混合指令的模式具有更好的性能,尤其是当此类代码位于循环或经常执行的基本块中时。

有很多有趣的优化:自动向量化、函数内联、内存对齐……但不幸的是,这个页面不够大,我无法涵盖所有​​这些。您可以查看GCC的源代码和手册以获取更多信息。我在上面和下面指出的参考资料非常有帮助。如果您设法消化了他们提供的大部分内容,我相信您会找到所需的内容。

根据 gcc 手册,-O1您在评论中提到的意思是打开以下标志:

-fauto-inc-dec -fcprop-registers -fdce -fdefer-pop 
-fdelayed-branch -fdse -fguess-branch-probability 
-fif-conversion2 -fif-conversion -finline-small-functions 
-fipa-pure-const -fipa-reference -fmerge-constants 
-fsplit-wide-types -ftree-builtin-call-dce -ftree-ccp 
-ftree-ch -ftree-copyrename -ftree-dce -ftree-dominator-opts 
-ftree-dse -ftree-fre -ftree-sra -ftree-ter -funit-at-a-time

您可以在gcc 手册页中阅读有关这些标志的更多信息我还建议(如果您还没有这样做)阅读Aho's Dragon 书籍