GCC装配优化 - 为什么这些是等效的?

时间:2015-07-01 16:21:41

标签: c gcc assembly x86-64 gas

我正在尝试学习汇编如何在初级阶段工作,所以我一直在玩gcc编译的-S输出。我写了一个简单的程序,定义了两个字节并返回它们的总和。整个计划如下:

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

当我使用以下代码编译时没有进行优化:

gcc -O0 -S -c test.c

我得到的测试结果如下:

    .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    $16, %esp
    call    ___main
    movb    $5, 15(%esp)
    movb    $10, 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"

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

.global _main
_main:
    movl    $15, %eax
    ret

在我看来,这是可能的代码量最少(但我意识到可能是完全错误的)来执行这个公认的微不足道的任务。这个表单是我的C程序中最“优化”的版本吗?

为什么GCC的初始输出更加冗长?从.cfi_startproc到call__main的行甚至是什么?什么叫__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    $15, %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"

这似乎已经删除了许多操作,但仍然留下所有导致调用__main的行似乎没必要。 什么是.cfi_XXX行?为什么添加这么多标签? .section,.ident,.def .p2align等等做了什么?

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

更新

通过说明来澄清

  

在我看来,这是可能的代码量最少(但是我   意识到可能是完全错误的)执行这个公认的微不足道的任务。   这个表单是我的C程序中最“优化”的版本吗?

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

为什么我添加这个位的核心是为了说明为什么我感到困惑的是这个汇编代码的4行版本可以有效地实现与其他代码相同的效果。在我看来,海湾合作委员会已经添加了许多“东西”,其目的是我无法辨别的。

5 个答案:

答案 0 :(得分:8)

感谢Kin3TiX提出的一个新手问题,这个问题不仅仅是一些没有评论的恶意代码的代码转储,而且是一个非常简单的问题。 :)

作为一种让您的ASM湿透的方法,我建议使用除main以外的功能。例如只是一个带有两个整数args的函数,并添加它们。然后编译器无法对其进行优化。您仍然可以使用常量作为参数调用它,如果它与main不同,则不会内联,因此您甚至可以单步执行它。

在编译main时理解asm级别的内容会有一些好处,但除了嵌入式系统之外,您只需要编写优化的内部内容在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引用的往返,以查看%rdi中哪个函数arg进入,以及哪个struct成员位于偏移量8。

  

从.cfi_startproc到call__main的行甚至是什么?

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

正如其他人所说,.cfi内容正在调试信息。它是strip将从您的二进制文件中删除的内容,或者如果您没有使用-g,则首先不会出现这些内容。 IDK为什么他们在-S输出中没有-g。我常常从objdump -d输出中查看asm,而不是gcc -S。通常是因为我可以对可执行文件进行基准测试并查看其asm,而无需多次调用gcc

推送%ebp然后将其设置为函数入口上的堆栈指针值的东西设置了什么称为"堆栈帧"。这就是%ebp被称为基指针的原因。如果使用-fomit-frame-pointer进行编译,这些insn将无法存在,这为代码提供了额外的寄存器。 (这对于32位x86来说是巨大的,因为它会带你从6到7个regs。(%esp仍然被捆绑为堆栈指针;暂时将其存储在xmm或mmx reg中,然后将其用作另一个GP reg是可能的,但你的代码很难调试!)

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

我对帧指针的目的并不完全清楚。使用调试符号,即使使用-fomit-frame-pointer也可以回溯调用堆栈,它是amd64的默认值。 (amd64 ABI具有堆栈的对齐要求,在其他方面也更好。例如,在regs而不是堆栈中传递args。)

    andl    $-16, %esp
    subl    $16, %esp

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

    call    ___main

_main(asm name = __main)可能是一个gcc运行时库函数,它为需要它的东西调用构造函数。也许库设置的东西,它可能是从你的任何自己的全局/静态变量的构造函数调用。 (这个old mailing list message表示_main适用于构造函数,但主要不应该在支持获取启动代码的平台上调用它。也许i386没有那,只有amd64?)编辑:你在评论中说这是来自cygwin。这可以解释它,因为cygwin必须制作非ELF .exes。

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

为什么GCC的初始输出更加冗长?

如果未启用优化,gcc会尽可能将C语句映射到asm。做其他事情需要更多的编译时间。因此,movb来自两个变量的初始值设定项。返回值是通过执行两次加载来计算的(带符号扩展,因为我们需要在添加之前上转换为int,以匹配写入的C代码的语义,以及溢出)。

  

我无法确定两个减法操作的用途。

只有一条sub条指令。在调用__main之前,它会在函数的变量上保留堆栈空间。您在谈论哪个其他潜艇?

  

.section,.ident,.def .p2align等等做了什么?

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

.ident.def:看起来像gcc将其标记放在目标文件上,因此您可以告诉编译器/汇编程序生成它。不相关,忽略这些。

.section:确定ELF对象文件的哪个部分来自所有后续指令或数据指令(例如.byte 0x00)的字节,直到下一个.section汇编程序指令。 code(只读,可共享),data(初始化读/写数据,私有)或bss(块存储段。零初始化,不做目标文件中的任何空格。)

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

  

我加入这一点的核心原因是为了说明为什么我感到困惑   这个汇编代码的4行版本可以有效地实现   与其他人一样的效果。在我看来,GCC已经添加了很多   "东西"其目的我无法辨别。

将感兴趣的代码单独放入函数中。关于main,很多事情都很特别。

你是正确的,mov - 即时和ret是实现该功能所需的全部内容,但是gcc显然没有用于识别琐碎整体的快捷方式 - 程序并省略main的堆栈帧或对_main的调用。 >。<

但问题很好。正如我所说,只是忽略所有废话,并担心你想要优化的小部分。

答案 1 :(得分:5)

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

如果您想一般使用编译器,可以使用以下编译驱动程序调用命令-o <filename.S> -S -masm=intel -fno-asynchronous-unwind-tables <filename.C>或只使用godbolt's interactive compiler

答案 2 :(得分:1)

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

我不知道对__main的呼叫在那里做了什么 - 我的海湾合作委员会不这样做。事实上,我的GCC(4.9.2)为gcc test.c -S -O1提供了以下内容:

    .section __TEXT,__text_startup,regular,pure_instructions
    .globl _main
_main:
LFB0:
    movl    $15, %eax
    ret
LFE0:
    .section __TEXT,__eh_frame,coalesced,no_toc+strip_static_syms+live_support
EH_frame1:
    .set L$set$0,LECIE1-LSCIE1
    .long L$set$0
LSCIE1:
    .long   0
    .byte   0x1
    .ascii "zR\0"
    .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$1,LEFDE1-LASFDE1
    .long L$set$1
LASFDE1:
    .long   LASFDE1-EH_frame1
    .quad   LFB0-.
    .set L$set$2,LFE0-LFB0
    .quad L$set$2
    .byte   0
    .align 3
LEFDE1:
    .subsections_via_symbols

并且你会看到它,_main正是你期望的双指令序列。 (__eh_frame内容更多是以不同格式调试信息。)

答案 3 :(得分:1)

-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    $5, -2(%rbp)
        movb    $10, -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    $15, %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生成了相同的代码。也就是说,最轻微的优化会在编译时评估所有常量。

答案 4 :(得分:0)

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

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

可能不值得花时间来优化这个微不足道且相当无用的案例。

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