上文http://blog.csdn.net/zr_lang/article/details/39962297我们提到了系统调用,现在说一下函数调用。函数调用应该是一个编程者除了写if...else以外最常写的东西了,记得多年前我第一次写一个C语言函数的时候还觉得很神奇。我们的程序不能只有一个代码段,那么做将使得程序很难协同开发和维护,将程序分割为部分进行封装,每一部分都定义良好的接口,这是非常重要的,我一直觉得封装和接口是人类进步的基本方式。好吧,这里我们不讨论封装和接口,我们讨论函数是怎么调用和返回的。
我们先来复习一下一个函数可能包含哪些东西? 函数名、函数参数、局部变量、静态变量、全局变量、返回地址、返回值, 我目前想到的就这些。
函数名: 就是一个符号,一个名称,这个符号代表一个地址,就是这个函数的起始地址。
函数参数: 就是调用这个函数的东西显示给这个函数用来处理的数据项, 函数返回后参数本身被废弃。
局部变量: 在函数里定义并处理的数据存储区,在函数返回后即被废弃。
静态变量和全局变量我不想在这里讨论,对下文没有直接影响。
返回地址: 这其实是一个看不见的参数,因为它不能直接在函数中使用。但是它确实存储在函数执行所用的栈中,当函数执行结束后会返回这个地址。
返回值: 传回给调用者的反馈,在C语言中是一个数值,在其它语言中可能会不一样。这种不同是语言的调用约定。汇编作为很底层的语言,它可以使用自己偏好的任何调用约定,但是如果要让汇编和其它语言之间进行互动,那就要考虑遵从其它语言的预定。
下面以一个简单的C语言例子开始func.c:
1#include 2void tmp_print(int a, int b, int c){ 3 printf ("a=%d, b=%d, c=%d\n", a, b, c); 4} 5 6int main(int argc, char *argv[]){ 7 int a=0, b=1, c=33; 8 tmp_print(a, b, c); 9 return 0; 10}
这个程序没什么意义,仅仅是用来分析。先编译一下:
gcc -c -o func.o func.c
就编译成目标文件吧,因为再进一步的话过了链接后很多东西就链接进来变的复杂了。
然后介绍一下一个伟大的黑客工具objdump,简单好用。我们用它做个简单的反汇编来分析一下上面的C程序对应的汇编。
objdump -d func.o
输出的内容大致如下:
func.o: 文件格式 elf64-x86-64 Disassembly of section .text: 0000000000000000: 0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 83 ec 10 sub $0x10,%rsp 8: 89 7d fc mov %edi,-0x4(%rbp) b: 89 75 f8 mov %esi,-0x8(%rbp) e: 89 55 f4 mov %edx,-0xc(%rbp) 11: 8b 4d f4 mov -0xc(%rbp),%ecx 14: 8b 55 f8 mov -0x8(%rbp),%edx 17: 8b 45 fc mov -0x4(%rbp),%eax 1a: 89 c6 mov %eax,%esi 1c: bf 00 00 00 00 mov $0x0,%edi 21: b8 00 00 00 00 mov $0x0,%eax 26: e8 00 00 00 00 callq 2b 2b: c9 leaveq 2c: c3 retq 000000000000002d: 2d: 55 push %rbp 2e: 48 89 e5 mov %rsp,%rbp 31: 48 83 ec 20 sub $0x20,%rsp 35: 89 7d ec mov %edi,-0x14(%rbp) 38: 48 89 75 e0 mov %rsi,-0x20(%rbp) 3c: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp) 43: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp) 4a: c7 45 f4 21 00 00 00 movl $0x21,-0xc(%rbp) 51: 8b 55 f4 mov -0xc(%rbp),%edx 54: 8b 4d f8 mov -0x8(%rbp),%ecx 57: 8b 45 fc mov -0x4(%rbp),%eax 5a: 89 ce mov %ecx,%esi 5c: 89 c7 mov %eax,%edi 5e: e8 00 00 00 00 callq 63 63: b8 00 00 00 00 mov $0x0,%eax 68: c9 leaveq 69: c3 retq
首先看51到5c这几行,由于我没有使用例如O2这样的优化编译的选项,所以参数在传递和处理时显得很乱,但是还是可以看出最终是用edx,esi,edi传递的a,b,c三个参数。在参数很少的情况下编译器会选择让寄存器来传递参数,但这并不是一个通用的方法,通用的方法是将参数压入栈,所以51到5c这几行也可以写做:
push $0x21 #第三个参数入栈
push $0x1 #第二个参数入栈
push $0x0 #第一个参数入栈
类似这样的意思,就是将参数入栈。
经过参数入栈(也可以传递给寄存器,这里分析入栈的情况)和返回地址入栈,现在栈中的情况如下(注意这里栈是向低地址增长的,也就是栈底在高地址,入栈时栈指针向低地址移动。):
===高地址===
参数 c
参数 b
参数 a
返回地址 <--(%rsp)
===低地址===
而且指令指针已经指向了tmp_print函数的起始地址,再执行就执行tmp_print的第0行。看0,1和4都干了什么:
0: 55 push %rbp 1: 48 89 e5 mov %rsp,%rbp 4: 48 83 ec 10 sub $0x10,%rsp首先rbp入栈, BP寄存器是基址指针寄存器,是专门用来处理函数调用的。那么push %rbp就是将rbp寄存器的内容先保存入栈,然后mov %rsp, %rbp,将现在的%rbp的值赋值为和%rsp相等。经过这两步,栈中的情况变为:
参数 c <-- 32(%rbp)
参数 b <-- 24(%rbp)
参数 a <-- 16(%rbp)
返回地址 <-- 8(%rbp)
旧%rbp <-- (%rsp)和(%rbp)
接着下面一句
sub $0x10,%rsp这一句的作用是让%rsp栈指针寄存器移动(减)0x10字节,为tmp_print函数预留一段栈上空间供局部变量使用,那么现在tmp_print函数的栈就变成这样了:
参数 c <-- 32(%rbp)
参数 b <-- 24(%rbp)
参数 a <-- 16(%rbp)
返回地址 <-- 8(%rbp)
旧%rbp <-- (%rbp)
局部变量1 <-- -4(%rbp)局部变量2 <-- -8(%rbp)
局部变量3 <-- -12(%rbp)
未使用空间 <-- -16(%rbp) 和 (%rsp)
注意看%rbp和%rsp的位置,%rbp总是指向旧%rbp位置,%rsp总是指向栈顶。SP就是专门的栈指针,push和pop操作都会导致起自动增减,所以必须让它总是指向栈顶,否则就该乱套了。
BP寄存器是用来为一个函数在栈中寻址用的基址寄存器,可以看到以%rbp为分界,向栈底方向可以寻址到参数,向栈顶方向可以寻址到局部变量空间(前面的文章我已经说过了基址寻址),比如在C语言中使用变量a,可能在汇编语言中使用的就是-4(%rbp)。
到此我们基本上把函数的几个重要要素说了一遍:
函数参数: 在call之前入栈,当然也可能是使用寄存器传递。
函数名: 在call的时候将函数地址给IP寄存器。
返回地址: 在call的时候将call的下一条指令的地址入栈。
局部变量: 如果需要局部变量空间,则继续移动SP指针,预留更多的栈空间。
最后还有一个BP指针,用来寻址栈上空间。
以上就是调用一个函数的时候栈上的动作,大致意思就是这样了。