【问题标题】:Why are there extra instructions in my gcc output?为什么我的 gcc 输出中有额外的指令?
【发布时间】:2009-02-01 00:05:31
【问题描述】:

GCC 编译(使用gcc --omit-frame-pointer -s):

    int the_answer() { return 42; }

进入

            .Text
    .globl _the_answer
    _the_answer:
        subl    $12, %esp
        movl    $42, %eax
        addl    $12, %esp
        ret
       .subsections_via_symbols

'$12' 常量在这里做什么,'%esp' 寄存器是什么?

【问题讨论】:

    标签: gcc assembly x86


    【解决方案1】:

    简答:堆栈帧。

    长答案:当您调用函数时,编译器将操纵堆栈指针以允许本地数据,例如函数变量。由于您的代码正在更改堆栈指针esp,这就是我假设这里发生的情况。我会认为 GCC 足够聪明,可以在实际上不需要的地方优化它,但你可能没有使用优化。

    【讨论】:

      【解决方案2】:
      _the_answer:
          subl    $12, %esp
          movl    $42, %eax
          addl    $12, %esp
          ret
      

      第一个 subl 递减堆栈指针,为函数中可能使用的变量腾出空间。例如,一个槽可用于帧指针,另一个用于保存返回地址。你说它应该省略帧指针。这通常意味着它省略了加载/存储来保存/恢复帧指针。但通常代码仍会为它保留内存。原因是它使分析堆栈的代码更容易。很容易给堆栈的偏移量一个最小宽度,所以你知道你总是可以访问 FP+0x12 来获得第一个局部变量槽,即使你省略了保存帧指针。

      好吧,据我所知,x86 上的eax 用于处理调用者的返回值。最后一个 addl 只是破坏了之前为您的函数创建的框架。

      在函数的开头和结尾生成指令的代码称为函数的“结尾”和“序言”。以下是我的端口在必须在 GCC 中创建函数序言时所做的事情(对于打算尽可能快速和多功能的实际端口来说,这要复杂得多):

      void eco32_prologue(void) {
          int i, j;
          /* reserve space for all callee saved registers, and 2 additional ones:
           * for the frame pointer and return address */
          int regs_saved = registers_to_be_saved() + 2;
          int stackptr_off = (regs_saved * 4 + get_frame_size());
      
          /* decrement the stack pointer */
          emit_move_insn(stack_pointer_rtx, 
                         gen_rtx_MINUS(SImode, stack_pointer_rtx, 
                                       GEN_INT(stackptr_off)));
      
          /* save return adress, if we need to */
          if(eco32_ra_ever_killed()) {
              /* note: reg 31 is return address register */
              emit_move_insn(gen_rtx_MEM(SImode, 
                                 plus_constant(stack_pointer_rtx, 
                                               -4 + stackptr_off)),  
                             gen_rtx_REG(SImode, 31));
          }
      
          /* save the frame pointer, if it is needed */
          if(frame_pointer_needed) {
              emit_move_insn(gen_rtx_MEM(SImode, 
                                 plus_constant(stack_pointer_rtx, 
                                               -8 + stackptr_off)), 
                             hard_frame_pointer_rtx);
          }
      
          /* save callee save registers */
          for(i=0, j=3; i<FIRST_PSEUDO_REGISTER; i++) {
              /* if we ever use the register, and if it's not used in calls
               * (would be saved already) and it's not a special register */
              if(df_regs_ever_live_p(i) && 
                 !call_used_regs[i] && !fixed_regs[i]) {
                  emit_move_insn(gen_rtx_MEM(SImode, 
                                     plus_constant(stack_pointer_rtx, 
                                                   -4 * j + stackptr_off)), 
                                 gen_rtx_REG(SImode, i));
                  j++;
              }
          }
      
          /* set the new frame pointer, if it is needed now */
          if(frame_pointer_needed) {
              emit_move_insn(hard_frame_pointer_rtx, 
                             plus_constant(stack_pointer_rtx, stackptr_off));
          }
      }
      

      我省略了一些处理其他问题的代码,主要是告诉 GCC 哪些指令对异常处理很重要(即帧指针的存储位置等)。好吧,被调用者保存的寄存器是调用者在调用之前不需要保存的寄存器。被调用的函数关心根据需要保存/恢复它们。正如您在第一行中看到的,我们总是为返回地址和帧指针分配空间。该空间只是几个字节,无关紧要。但我们只在必要时生成存储/加载。最后注意“硬”帧指针是“真实”帧指针寄存器。由于某些 gcc 内部原因,这是必要的。 “frame_pointer_needed”标志由 GCC 设置,只要我可以省略存储帧指针。在某些情况下,它必须被存储,例如当使用alloca(它动态更改堆栈指针)时。 GCC 关心这一切。请注意,自从我编写该代码以来已经有一段时间了,所以我希望我上面添加的其他 cmets 不是全错的 :)

      【讨论】:

        【解决方案3】:

        堆栈对齐。在函数入口处,esp 是 -4 mod 16,因为返回地址已被call 推送。减去 12 重新对齐它。除了在使用 mmx/sse/等的多媒体代码中,没有充分的理由在 x86 上将堆栈对齐到 16 字节,但是在 3.x 时代的某个地方,gcc 开发人员决定无论如何都应该保持堆栈对齐,强加序言/尾声开销,增加的堆栈大小,以及为了一些特殊目的利益而导致所有程序上的缓存抖动增加(这恰好是我感兴趣的一些领域,但我仍然认为这是不公平和不好的决定)。

        通常,如果您启用任何优化级别,gcc 将删除用于叶函数(不进行函数调用的函数)的堆栈对齐的无用序言/尾声,但它会在您开始调用时立即返回。

        您也可以使用-mpreferred-stack-boundary=2 解决此问题。

        【讨论】:

        • +1 这应该是公认的答案!我使用 GCC 4.8.x (MinGW) 尝试了一个类似的示例,默认情况下,在 _main 过程中,它发出 andl $-16, %esp 作为序言的一部分,这也有效地将堆栈指针对齐到 16 个字节。虽然在被调用函数_the_answer 中没有出现这样的指令
        【解决方案4】:

        使用 GCC 4.3.2 我得到了这个函数:

        the_answer:
        movl    $42, %eax
        ret
        

        ...加上周围的垃圾,使用以下命令行:echo 'int the_answer() { return 42; }' | gcc --omit-frame-pointer -S -x c -o - -

        您使用的是哪个版本?

        【讨论】:

        • 4.0.1(苹果公司内部版本 5488)。猜猜这是一个错误。
        • @Mike,不是错误。代码工作正常,因为 subl 被 addl 反转。它效率低下,但绝对不是错误。
        • 可能不是错误,可能只是 4.3 在确定哪些指令可以安全删除方面更聪明。
        • 以“-O3”消失。也许 Apple 的 GCC 的默认优化级别较低?
        • 取决于苹果开发者是否认为值得。不过,这本身并不是一个 gcc 问题。后端编写者需要考虑这一点
        猜你喜欢
        • 2015-01-09
        • 1970-01-01
        • 2011-08-19
        • 1970-01-01
        • 2023-01-24
        • 1970-01-01
        • 1970-01-01
        • 2018-12-29
        • 1970-01-01
        相关资源
        最近更新 更多