GCC装配优化 - 为什么这些相同?

Kin*_*TiX 9 c assembly gcc x86-64 gnu-assembler

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

int main(void) {
  char A = 5;
  char B = 10;
  return A + B;
}
Run Code Online (Sandbox Code Playgroud)

当我编译它时没有使用以下优化:

gcc -O0 -S -c test.c
Run Code Online (Sandbox Code Playgroud)

我得到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    $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"
Run Code Online (Sandbox Code Playgroud)

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

.global _main
_main:
    movl    $15, %eax
    ret
Run Code Online (Sandbox Code Playgroud)

在我看来,这是可能的代码量最少(但我意识到可能是完全错误的)来执行这个公认的微不足道的任务.这个表单是我的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"
Run Code Online (Sandbox Code Playgroud)

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

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


UPDATE

澄清,说

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

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

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

Pet*_*des 8

谢谢你,Kin3TiX,问一个asm-newbie问题,这不只是一些没有评论的讨厌代码的代码转储,而且是一个非常简单的问题.:)

作为一种让您的ASM湿透的方法,我建议使用其他功能main.例如,只是一个带有两个整数args的函数,并添加它们.然后编译器无法优化它.您仍然可以将常量称为args,如果它位于不同的文件中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,哪个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
Run Code Online (Sandbox Code Playgroud)

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

推送%ebp然后将其设置为函数入口上的堆栈指针值的东西设置所谓的"堆栈帧".这就是为什么%ebp称为基指针.如果您使用编译,这些insn将不存在-fomit-frame-pointer,这为代码提供了额外的寄存器.(这对于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
Run Code Online (Sandbox Code Playgroud)

and对齐堆栈的16字节边界,不论它是什么了.的sub储量堆栈此功能上的16个字节.(注意优化版本中缺少它,因为它可以优化任何变量的内存存储需求.)

    call    ___main
Run Code Online (Sandbox Code Playgroud)

_main(asm name = __main)可能是一个gcc运行时库函数,它为需要它的东西调用构造函数.也许库设置的东西,它可能是从你的任何自己的全局/静态变量的构造函数调用.(这个旧的邮件列表消息指示_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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

什么.section,.ident,.def .p2align等等呢?

请参阅GNU汇编程序手册.也可在本地作为信息页面使用:运行info gas.

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

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

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

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

将感兴趣的代码单独放在函数中.很多事情都很特别main.

你是正确的,a mov-immediate和a ret都是实现这个函数所需要的,但gcc显然没有用于识别琐碎的整个程序和省略main堆栈框架或调用的快捷方式_main.> <

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


Nli*_*tis 5

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

如果你想一般使用编译器,你可以使用以下编译驱动程序调用命令-o <filename.S> -S -masm=intel -fno-asynchronous-unwind-tables <filename.C>或只使用godbolt的交互式编译器