gpt4 book ai didi

c - GCC assembly 优化-为什么这些等效?

转载 作者:行者123 更新时间:2023-12-02 22:11:08 26 4
gpt4 key购买 nike

我正在尝试学习汇编在基本级别上的工作方式,因此我一直在玩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),我已经可以手工减少汇编以使用以下代码执行相同的功能:

.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行版本可以有效实现与其他版本相同的效果。在我看来,海湾合作委员会增加了很多我无法辨认的“东西”。

最佳答案

谢谢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参考的往返时间,以查看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


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

按下 %ebp然后将其设置为函数条目上的堆栈指针的值的工作将建立所谓的“堆栈框架”。这就是为什么 %ebp被称为基本指针的原因。如果使用 -fomit-frame-pointer进行编译,这些insns将不存在,这会为代码提供额外的寄存器以供使用。 (这对于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 .exe,所以可以这样解释。

    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:确定以下所有指令或数据指令(例如 .byte 0x00)中的字节进入ELF目标文件的哪个部分,直到下一个 .section汇编程序指令为止。 code(只读,可共享), data(初始化的读/写数据,私有)或 bss(块存储段。零初始化,不占用目标文件中的任何空间)。

.p2align:2的幂对齐。用nop指令填充,直到所需的对齐。 .align 16.p2align 4相同。对齐目标时,跳转指令的速度更快,这是因为以16B的块为单位取指令,不跨越页面边界或仅不跨越高速缓存行边界。 (如果代码已在Intel Sandybridge及更高版本的uop缓存中,则32B对齐才有意义。)例如,请参见 Agner Fog's docs


为什么我添加此位的核心是为了说明为什么我感到困惑
该汇编代码的4行版本可以有效实现
和其他人一样。在我看来,海湾合作委员会增加了很多
我无法分辨其目的的“东西”。


将感兴趣的代码本身放在函数中。 main有很多特别之处。

您是对的,实现该功能只需要 mov-即时和 ret,但是gcc显然没有识别琐碎的整个程序并忽略 main的堆栈框架或调用的快捷方式到 _main。 >。<

很好的问题。正如我所说,只需忽略所有这些废话,而只担心要优化的一小部分。

关于c - GCC assembly 优化-为什么这些等效?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31166773/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com