在汇编语言中需要调用函数时要call这个函数名,函数的执行过程如下:
准备执行
在主程序中每次调用函数时,先依次把各参数以相反的顺序入栈;
然后call func_name, 这里call要做两件事: 一是把函数的返回地址入栈,二是让指令执行指针%eip指向函数开始处。
开始执行
现在函数要开始执行了,但它执行函数代码前还要做一点小事,首先把原来的基地址寄存器%ebp值入栈(基址寻址?看汇编),因为在程序执行中%ebp要另作它用, 接着堆栈指针%esp的值复制给%ebp, 此后在函数执行中%ebp一直保持不变,可以由此寻址获得函数参数。
pushl %ebp (值入栈,占据4个位)
movl %esp, %ebp (ebp esp又再指向一个新位置,但并不一定在栈中装入了数据)
下面开始执行函数代码了。函数先要把它的局部变量保存在栈中,这很简单。比如要保存一个long型数据,只要把%esp指针向下移动4个字节(因为栈增长方向是由高地址到低地址),再根据%esp把该数据移入. 下面是保存两个局部变量long后的堆栈内容:
Parameter #N <--- N*4+4(%ebp)
...
Parameter 2 <--- 12(%ebp)
Parameter 1 <--- 8(%ebp)
Return Address <--- 4(%ebp)
Old %ebp <--- (%ebp)
Local Variable 1 <--- -4(%ebp)
Local Variable 2 <--- -8(%ebp) and (%esp)
从上可以看出通过%ebp基地址寻址(基址寻址?看汇编)可以访问所有的函数参数和局部变量. 当然也可以不用
%ebp而用其它的寄存器进行同样的基地址寻址。但对于x86结构使用%ebp寄存器可能会更
快一点。
执行结束:
现在函数执行要结束了,在它返回之前,还要做下面几件事:
1. 把函数的返回值存放在通用寄存器%eax中,供外部使用
2. 把%esp指向函数开始执行的位置, 即movl %ebp,%esp
3. 在函数返回ret之前,要还原ebx, 即popl %ebp
movl %ebp, %esp
popl %ebp
ret
调用函数的过程,再看一下函数是如何退出的。观察main
和f
不难发现,退出函数使用的是如下指令
leave
ret
leave
指令相当于如下指令:
movl %ebp, %esp
popl %ebp
- 第一条语句是将
esp
重置到ebp
,可以理解为清空当前函数所使用的栈 - 第二条语句是将栈顶值赋值给
ebp
,并弹出,栈顶值是什么呢?通过上面的分析不难发现,此时的栈顶值实际上是前一个函数的栈基地址,所以第二条语句的意思就是把ebp
恢复到前一个函数的栈基地址
接着ret
就是相当于,恢复指令指向:
popl %eip
总结
最后,通过这个例子,总结一下函数调用的过程:
由main函数进入f函数:
- 当前栈基地址压栈(当前栈基地址实际上是前一个函数(main)的栈基地址) pushl %ebp ; movl %esp, %ebp ;
调用f函数:
- 参数从右到左进栈(实参)
- 下一条指令地址进栈
退出函数:
- 栈顶
esp
归位,回到本函数的ebp movl %ebp, %esp;
- 基地址回退到上一个函数的基地址 popl %ebp (弹值,该处ebp的值与1、中ebp的值不同,由此可以推测形参并不在栈中)
eip
退回到上一个函数即将要执行的那条语句的地址上 ret