我在上面运行了gcc -S:
int main() { printf ("Hello world!"); }
我得到了以下汇编代码:
.file "test.c" .section .rodata .LC0: .string "Hello world!" .text .globl main .type main, @function main: leal 4(%esp), %ecx andl $-16, %esp pushl -4(%ecx) pushl %ebp movl %esp, %ebp pushl %ecx subl $20, %esp movl $.LC0, (%esp) call printf addl $20, %esp popl %ecx popl %ebp leal -4(%ecx), %esp ret .size main, .-main .ident "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)" .section .note.GNU-stack,"",@progbits
我很好奇了解此输出。有人可以在理解此输出时共享一些指针吗?或者有人可以在这些行/每组行中标记注释以说明其效果会很好。
这是怎么回事:
.file "test.c"
原始源文件名(调试器使用)。
.section .rodata .LC0: .string "Hello world!"
以零结尾的字符串包含在“ .rodata”部分中(“ ro”表示“只读”:应用程序将能够读取数据,但是任何写入数据的尝试都会触发异常)。
.text
现在,我们将内容写入代码所在的“ .text”部分。
.globl main .type main, @function main:
我们定义了一个称为“ main”的函数,并且该函数在全局范围内可见(其他目标文件将可以调用它)。
leal 4(%esp), %ecx
我们在寄存器中存储%ecx值4+%esp(%esp是堆栈指针)。
%ecx
4+%esp
%esp
andl $-16, %esp
%esp稍作修改,使其变为16的倍数。对于某些数据类型(对应于C double和的浮点格式long double),当内存访问位于16的倍数处时,性能会更好。 ,但是在不使用优化标志(-O2…)的情况下,编译器往往会生成大量通用的无用代码(即,在某些情况下可能有用但在此处无效的代码)。
double
long double
-O2
pushl -4(%ecx)
这有点奇怪:在这一点上,address -4(%ecx)处的单词是之前位于堆栈顶部的单词andl。该代码检索该单词(顺便说一句,它应该是返回地址)并再次推送它。这种类型的仿真将通过具有16字节对齐堆栈的函数的调用获得的结果。我的猜测是,这push是参数复制序列的残余。由于函数已经调整了堆栈指针,因此它必须复制函数参数,可以通过堆栈指针的旧值访问这些参数。这里,除了函数返回地址外,没有参数。请注意,将不使用该词(再次,这是未经优化的代码)。
-4(%ecx)
andl
push
pushl %ebp movl %esp, %ebp
这是标准函数的序言:保存%ebp(因为我们将要对其进行修改),然后设置%ebp为指向堆栈框架。此后,%ebp将用于访问函数参数,使其%esp再次释放。(是的,没有参数,因此对于该函数没有用。)
%ebp
pushl %ecx
我们保存%ecx(在函数退出时将需要它,以恢复%esp到之前的值andl)。
subl $20, %esp
我们在堆栈上保留32个字节(请记住,堆栈增长为“向下”)。该空间将用于存储to的参数printf()(这太过分了,因为只有一个参数,它将使用4个字节[这是一个指针])。
printf()
movl $.LC0, (%esp) call printf
我们将参数“推入” printf()(即,确保%esp指向包含该参数的单词,此处$.LC0为rodata节中常量字符串的地址)。然后我们打电话printf()。
$.LC0
addl $20, %esp
当printf()返回时,我们除去分配给参数的空间。这addl取消了subl上面的操作。
addl
subl
popl %ecx
我们恢复%ecx(如上所示);printf()可能已经对其进行了修改(调用约定描述了可以在退出时不恢复功能的情况下修改功能的寄存器;%ecx此类寄存器就是其中之一)。
popl %ebp
功能结语:恢复%ebp(对应于pushl %ebp以上内容)。
pushl %ebp
leal -4(%ecx), %esp
我们恢复%esp到其初始值。此操作码的作用是存储在%espvalue中%ecx-4。%ecx在第一个功能操作码中设置。这会取消对的任何更改%esp,包括andl。
%ecx-4
ret
功能出口。
.size main, .-main
这设置了main()函数的大小:在汇编过程中的任何时候,“ .”都是“我们现在要添加内容的地址”的别名。如果在此处添加了另一条指令,它将到达“ .” 指定的地址。因此,“ .-main”是函数代码的确切大小main()。该.size指令指示汇编程序将该信息写入目标文件中。
main()
.
.-main
.size
.ident "GCC: (GNU) 4.3.0 20080428 (Red Hat 4.3.0-8)"
GCC只是喜欢留下其行动的痕迹。该字符串最终作为目标文件中的一种注释。链接器将其删除。
.section .note.GNU-stack,"",@progbits
GCC在其中写了一个特殊的部分,该代码可以容纳不可执行的堆栈。这是正常情况。对于某些特殊用途(不是标准C),需要可执行堆栈。在现代处理器上,内核可以创建一个不可执行的堆栈(如果有人试图以代码的形式执行堆栈中的某些数据,则该堆栈会触发异常)。某些人将其视为“安全功能”,因为将代码放在堆栈上是利用缓冲区溢出的常用方法。在本节中,可执行文件将被标记为“与不可执行的堆栈兼容”,内核将很高兴地提供这样的文件。