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