分类:
2009-12-20 13:15:04
本文通过一个C语言例子程序,展示函数调用中参数的传递过程。
首先理解函数调用的过程。
1. 每个C函数经过GCC编译后,都会形成下面汇编:
.globl func
.type func, @function
func:
pushl %ebp
movl %esp, %ebp
...
popl %ebp
ret
.size func, .-func
注意:
O 在函数开头,先把EBP保存到栈中,再把ESP的值复制到EBP中,函数的后续部分主要就用EBP来访问函数的参数。
O 在函数返回前,从栈中取回EBP的值。其实,如果ESP在上面过程中被修改过,在popl %ebp之前还要先把ESP修改回原来的值。即是说,在pushl %ebp和popl %ebp时,ESP必须保持相同的值。
2. 当调用C函数时,GCC先把参数列表反序入栈,然后,在执行CALL func指令时,机器会自动把函数返回地址压入栈中。栈示意图如下:
由上面的示意图可以看出:函数参数Function parameter 3 , 2, 1是以相反的顺序压入栈中的,而参数入栈之后便是函数返回地址Return Address。之后程序进入函数,函数把EBP入栈,成为Old EBP Value,此时ESP指向Old EBP Value所在位置,再把ESP的复制到EBP中。示意图最右一列是函数在使用EBP访问参数时就使用的偏移。如,(%ebp) 指向旧的EBP值,4(%ebp)指向函数返回地址,而8(%ebp)指向第一个函数的地址。
现在我们写一个简单的C程序parstk.c:
#include
void func(int k)
{
k++;
}
int main(int argc, char* argv[])
{
int k = 0;
func(k);
return 0;
}
这个程序有两个函数,并且在main()中调用了func(). 我们把它编成汇编代码:
$gcc -S -o parstk.s parstk.c
使用cat查看parstk.s文件:
.file "parstk.c"
.text
.globl func
.type func, @function
func:
pushl %ebp
movl %esp, %ebp
addl $1, 8(%ebp)
popl %ebp
ret
.size func, .-func
.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 $0, -8(%ebp)
movl -8(%ebp), %eax
movl %eax, (%esp)
call func
movl $0, %eax
addl $20, %esp
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.ident "GCC: (GNU) 4.2.4 (Ubuntu 4.2.4-1ubuntu4)"
.section .note.GNU-stack,"",@progbits
可以看到,在func()和main()的开头和结尾处都有EBP入栈,复制ESP,EBP出栈的规范操作。
现在我们来分析一下main()函数调用func()的过程。
在main()中,
1)movl $0, -8(%ebp)
相当于执行了int k = 0. -8(%ebp)是局部变量k的地址,此时它被初始化为0。
2) 接下去两行是把变量k压入栈中。这里解释一下为什么没有使用pushl。首先,main()在movl %esp, %ebp之后,又用subl $20, %esp将ESP的值减了20,相当于往栈中偷偷地压入20个空的字,后面直接使用movl直接把k的值放到ESP指向的内存中,相当于覆盖栈顶,而此时栈顶并没有重要的数据,所以用movl而没用pushl是安全的,且节省了栈空间。
3) call func
注意,此句执行完毕之后,函数返回地址自动入栈,且程序转到func()函数执行。
在func()中,
1) EBP入栈,复制ESP。
2) addl $1, 8(%ebp)
对第一个参数加1. 正如前面所说,使用了8(%ebp)访问第一个参数。
此外,还可以看出:
O main()函数中对ESP减了20之后,在返回前还是把它加了回来。指令Subl $20, %esp和addl $20, %esp。
O func()函数中没有修改ESP,所以也不需要在返回前对ESP做修复工作。