GCC 程序集优化 - 为什么这些是等价的?

GCC Assembly Optimizations - Why are these equivalent?

我正在尝试了解汇编的初级工作原理,因此我一直在研究 gcc 编译的 -S 输出。我写了一个简单的程序,定义了两个字节和 returns 它们的总和。整个程序如下:

int main(void) {
  char A = 5;
  char B = 10;
  return A + B;
}

当我在没有优化的情况下编译它时使用:

gcc -O0 -S -c test.c

我得到 test.s 如下所示:

    .file   "test.c"
    .def    ___main;    .scl    2;  .type   32; .endef
    .text
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    subl    , %esp
    call    ___main
    movb    , 15(%esp)
    movb    , 14(%esp)
    movsbl  15(%esp), %edx
    movsbl  14(%esp), %eax
    addl    %edx, %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
LFE0:
    .ident  "GCC: (GNU) 4.9.2"

现在,认识到这个程序可以很容易地简化为 return 一个常量 (15) 我已经能够使用以下代码手动减少程序集以执行相同的功能:

.global _main
_main:
    movl    , %eax
    ret

在我看来,这似乎是执行这项公认微不足道的任务所需的最少代码(但我意识到可能是完全错误的)。这种形式是我的C程序最“优化”的版本吗?

为什么 GCC 的初始输出如此冗长?从 .cfi_startproccall __main 的行甚至做了什么? call __main 是做什么的?我不知道这两个减法运算是干什么用的

即使 GCC 中的优化设置为 -O3 我也明白了:

    .file   "test.c"
    .def    ___main;    .scl    2;  .type   32; .endef
    .section    .text.unlikely,"x"
LCOLDB0:
    .section    .text.startup,"x"
LHOTB0:
    .p2align 4,,15
    .globl  _main
    .def    _main;  .scl    2;  .type   32; .endef
_main:
LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    andl    $-16, %esp
    call    ___main
    movl    , %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
LFE0:
    .section    .text.unlikely,"x"
LCOLDE0:
    .section    .text.startup,"x"
LHOTE0:
    .ident  "GCC: (GNU) 4.9.2"

这似乎已经删除了一些操作,但仍然留下了似乎不必要的所有通向 call __main 的行。 所有 .cfi_XXX 行的用途是什么?为什么加了那么多标签? .section.ident.def .p2align等是做什么的?

我了解包含许多标签和符号用于调试,但如果我没有启用 -g 进行编译,是否应该去除或省略这些标签和符号?


更新

为了澄清,说

This appears to me to be the least amount of code possible (but I realize could be quite wrong) to perform this admittedly trivial task. Is this form the most "optimized" version of my C program?

我并不是在暗示我正在尝试或已经实现了该程序的优化版本。我意识到该程序无用且微不足道。我只是将它用作学习汇编和编译器工作原理的工具。

我添加这一点的核心是为了说明为什么我对这个汇编代码的 4 行版本能有效地达到与其他版本相同的效果感到困惑。在我看来,GCC 添加了很多我无法辨别的“东西”。

我认为那部分只是一个固定模式,它设置了一个 16 字节对齐的堆栈,并且 CFI 与 exception frame 处理相关。

很难确定任何 main() 不需要这些,因为这是全局优化,因为 main 可能会调用其他编译单元中的函数。

而且花时间优化这个琐碎且相当无用的案例可能不值得。

如果您不这么认为,您可以随时开始进行此类优化并将其提交给 gcc。

首先,CFI 的内容用于调试目的(在 C++ 中,用于异常处理)。它告诉调试器每条指令的栈帧是什么样子的,这样调试器就可以重建程序变量的状态。这些不会导致可执行语句,并且对程序的运行时性能的影响为零。

我不知道对 __main 的调用在那里做什么 - 我的 GCC 不这样做。事实上,我的 GCC (4.9.2) 为 gcc test.c -S -O1 提供了以下内容:

    .section __TEXT,__text_startup,regular,pure_instructions
    .globl _main
_main:
LFB0:
    movl    , %eax
    ret
LFE0:
    .section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
EH_frame1:
    .set L$set[=10=],LECIE1-LSCIE1
    .long L$set[=10=]
LSCIE1:
    .long   0
    .byte   0x1
    .ascii "zR[=10=]"
    .byte   0x1
    .byte   0x78
    .byte   0x10
    .byte   0x1
    .byte   0x10
    .byte   0xc
    .byte   0x7
    .byte   0x8
    .byte   0x90
    .byte   0x1
    .align 3
LECIE1:
LSFDE1:
    .set L$set,LEFDE1-LASFDE1
    .long L$set
LASFDE1:
    .long   LASFDE1-EH_frame1
    .quad   LFB0-.
    .set L$set,LFE0-LFB0
    .quad L$set
    .byte   0
    .align 3
LEFDE1:
    .subsections_via_symbols

你看一下,_main 正是你所期望的双指令序列。 (__eh_frame 内容是更多不同格式的调试信息)。

.cfi(调用框架信息)指令在gas(Gnu ASsembler)中主要用于调试。它们允许调试器展开堆栈。要禁用它们,可以在调用编译驱动程序时使用以下参数 -fno-asynchronous-unwind-tables.

如果你想玩一般的编译器,你可以使用下面的编译驱动调用命令-o <filename.S> -S -masm=intel -fno-asynchronous-unwind-tables <filename.C>或者直接使用godbolt's interactive compiler

-o0 选项将输出定向到名为 0 的文件。也许你的意思是优化级别(大写 O)?:禁用优化。

我不明白为什么会调用 ____main 除非这是为某些模拟或挂钩的环境生成的。当我用 gcc -O0 -c -S t.c 编译时,我得到:

        .file   "t.c"
        .text
.globl main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        movb    , -2(%rbp)
        movb    , -1(%rbp)
        movsbl  -2(%rbp), %edx
        movsbl  -1(%rbp), %eax
        leal    (%rdx,%rax), %eax
        leave
        .cfi_def_cfa 7, 8
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-11)"
        .section        .note.GNU-stack,"",@progbits

也许您期待高水平的优化?这就是我用 gcc -O3 -c -S t.c:

得到的
        .file   "t.c"
        .text
        .p2align 4,,15
.globl main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        movl    , %eax
        ret
        .cfi_endproc
.LFE0:
        .size   main, .-main
        .ident  "GCC: (GNU) 4.4.7 20120313 (Red Hat 4.4.7-11)"
        .section        .note.GNU-stack,"",@progbits

除调试信息外,尽可能短。为 gcc -O2 -c -S t.cgcc -O1 -c -S t.c 生成相同的代码。也就是说,最轻微的优化会在编译时评估所有常量。

谢谢你,Kin3TiX,提出了一个 asm 新手问题,它不仅仅是一些没有注释的讨厌代码的代码转储,而且是一个非常简单的问题。 :)

作为让您熟悉 ASM 的一种方式,我建议您使用 main 以外的函数。例如只是一个接受两个整数参数并将它们相加的函数。然后编译器无法优化它。您仍然可以使用常量作为参数来调用它,如果它位于与 main 不同的文件中,它不会被内联,因此您甚至可以单步执行它。

编译时了解 asm 级别发生的事情有一些好处 main,但除了嵌入式系统之外,您只会在 asm 中编写优化的内部循环。 IMO,如果你不打算优化它,那么使用 asm 毫无意义。否则,您可能无法击败更易于阅读的源代码的编译器输出。

了解编译器输出的其他技巧:使用
编译 gcc -S -fno-stack-check -fverbose-asm。每条指令后的注释通常很好地提醒了该负载的用途。很快它就会退化为一堆临时名称,如 D.2983,但类似于
movq 8(%rdi), %rcx # a_1(D)->elements, a_1(D)->elements 将节省您到 ABI 引用的往返行程,以查看哪个函数 arg 进入 %rdi,以及哪个结构成员位于偏移量 8。

另见


What do the lines spanning from .cfi_startproc to call__main even do?

    _main:
LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5

.cfi 东西是调试器(和 C++ 异常处理)展开堆栈的堆栈展开信息 如果您从 objdump -d 输出而不是 gcc -S 查看 asm,它不会在那里,或者您可以使用 -fno-asynchronous-unwind-tables.

压入 %ebp 然后将其设置为函数入口堆栈指针的值的东西建立了所谓的“堆栈框架”。这就是为什么 %ebp 被称为基指针的原因。如果您使用 -fomit-frame-pointer 编译,这些 insn 将不会存在,这为代码提供了一个额外的寄存器来使用。默认情况下,-O2。 (这对于 32 位 x86 来说是巨大的,因为这需要你从 6 到 7 个可用的 regs。(%esp 仍然被绑定为堆栈指针;暂时将它存储在 xmm 或 mmx reg 中,然后将它用作另一个 GP reg 在理论上是可行的,但编译器永远不会那样做,它会使 POSIX 信号或 Windows SEH 之类的异步内容无法使用,并使调试更加困难。)

ret 之前的 leave 指令也是此堆栈帧内容的一部分。

帧指针主要是历史包袱,但确实要使堆栈帧中的偏移量保持一致。使用调试符号,即使使用 -fomit-frame-pointer,您也可以很好地回溯调用堆栈,这是 amd64 的默认设置。 (amd64 ABI 对堆栈有对齐要求,在其他方面也更好。例如,在 regs 中而不是在堆栈中传递 args。)

    andl    $-16, %esp
    subl    , %esp

and 将堆栈对齐到 16 字节边界,不管它之前是什么。 sub 在栈上为这个函数保留了 16 个字节。 (注意它是如何从优化版本中丢失的,因为它优化了对任何变量的内存存储的任何需求。)

    call    ___main

__main (asm name = ___main) 是 cygwin 的一部分:它调用共享库(包括 libc)的构造函数/初始化函数。在 GNU/Linux 上,这由 _start 处理(在到达 main 之前),甚至动态链接器挂钩让 libc 在可执行文件自己的 _start 到达之前初始化自身。我读过动态链接器挂钩(或来自静态可执行文件的 _start)而不是 main 中的代码 在 Cygwin 下是可能的,但他们只是选择不要那样做。

(这个 old mailing list message 表示 _main 用于构造函数,但是在支持获取启动代码来调用它的平台上 main 不应该调用它。)

    movb    , 15(%esp)
    movb    , 14(%esp)
    movsbl  15(%esp), %edx
    movsbl  14(%esp), %eax
    addl    %edx, %eax
    leave
    ret

Why is the initial output of GCC so much more verbose?

在不启用优化的情况下,gcc 会尽可能按字面意思将 C 语句映射到 asm。做任何其他事情都会花费更多的编译时间。因此, movb 来自两个变量的初始值设定项。 return 值是通过两次加载计算的(带有符号扩展,因为我们需要在添加之前向上转换为 int,以匹配所写的 C 代码的语义,直到溢出)。

I cannot figure what the two subtraction operations are for.

只有一条sub指令。在调用 __main 之前,它在堆栈上为函数的变量保留 space。你说的是哪个潜艇?

What do .section, .ident, .def .p2align, etc. etc. do?

manual for the GNU assembler。也可在本地作为信息页面使用:运行 info gas.

.ident.def:看起来 gcc 在目标文件上打上了自己的印记,因此您可以知道是哪个编译器/汇编器生成的。不相关,忽略这些。

.section:确定所有后续指令或数据指令(例如 .byte 0x00)的字节进入 ELF 目标文件的哪个部分,直到下一个 .section 汇编器指令。 code(只读,可共享),data(初始化 read/write 数据,私有),或 bss(块存储段。零初始化,不占用目标文件中的任何 space)。

.p2align:2 的幂对齐。用 nop 指令填充直到所需的对齐。 .align 16.p2align 4 相同。当目标对齐时,跳转指令更快,因为指令以 16B 块的形式获取,不跨越页面边界,或者只是不跨越缓存行边界。 (当代码已经在 Intel Sandybridge 及更高版本的 uop 缓存中时,32B 对齐是相关的。)参见 Agner Fog's docs,例如。

The core of why I added this bit is to illustrate why I am confused that the 4 line version of this assembly code can effectively achieve the same effect as the others. It seems to me that GCC has added alot of "stuff" whose purpose I cannot discern.

将感兴趣的代码单独放在一个函数中。 main.

有很多特别之处

你是正确的,一个 mov-immediate 和一个 ret 是实现该功能所需的全部,但 gcc 显然没有识别琐碎的整个程序和省略 main 的堆栈帧或对 _main 的调用。 >.<

问得好。正如我所说,忽略所有这些废话,只担心你想要优化的一小部分。