GCCassembly优化 – 为什么这些相同?

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

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 $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)我已经能够手动减少程序集以使用以下代码执行相同的function:

 .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启用编译,是否应该删除或省略这些标签和符号?


UPDATE

澄清,说

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

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

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

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

作为一种让您的ASM湿透的方法,我建议使用其他函数而不是main 。 例如,只是一个带有两个整数args的函数,并添加它们。 然后编译器无法优化它。 您仍然可以将常量称为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,以及哪个结构会员在抵消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个寄存器。( %esp仍然被捆绑为堆栈指针;暂时存放在xmm或mmx reg中,然后将其用作另一个GP reg是可能的,但是你的代码很难调试!)

ret之前的leave指令也是这个堆栈帧的一部分。

我对帧指针的目的并不完全清楚。 使用调试符号,即使使用-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运行时库函数,它为需要它的东西调用构造函数。 也许库设置的东西,它可能是从你的任何自己的全局/静态变量的构造函数调用。 (这个旧的邮件列表消息表明_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等等呢?

请参阅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很多事情都很特别。

你是正确的,只需要一个mov -immediate和ret来实现这个函数,但是gcc显然没有用于识别普通程序并省略main的堆栈帧或调用_main快捷方式。 > <

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

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

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

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

我不知道对__main的调用是做什么的 – 我的GCC不这样做。 事实上,我的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东西是更多不同格式的调试信息)。

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

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

  .file "tc" .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 tc

  .file "tc" .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 tcgcc -O1 -c -S tc生成相同的代码。 也就是说,最轻微的优化会在编译时评估所有常量。

我认为该部分只是一个固定模式,它设置一个16字节的对齐堆栈,而CFI是exception帧处理相关的。

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

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

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