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_startproc
到 call __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.c
和 gcc -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
的调用。 >.<
问得好。正如我所说,忽略所有这些废话,只担心你想要优化的一小部分。
我正在尝试了解汇编的初级工作原理,因此我一直在研究 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_startproc
到 call __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.c
和 gcc -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
的调用。 >.<
问得好。正如我所说,忽略所有这些废话,只担心你想要优化的一小部分。