简单函数的调用原理

来源:互联网 发布:文件上传外链源码 编辑:程序博客网 时间:2024/06/10 00:02

上文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-64Disassembly 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    #第一个参数入栈

类似这样的意思,就是将参数入栈。


然后找到函数里callq 63 ,这个就是在调用tmp_print函数,但是不要以为63 是tmp_print的地址,这个反汇编的目标文件func.o,还没有经过链接呢,所以函数调用神马的要等链接后确定地址。链接是另一个复杂的过程了,再写一篇都不够,这里就先不说了。我们关注本文的重点,你现在可以把这句就理解成call tmp_print。那这个call干了什么呢,它实际上相当于干了两件事,一是将下一条指令的地址(返回地址)压入栈中。二是修改指令指针%rip指向要调用的函数的起始处。



经过参数入栈(也可以传递给寄存器,这里分析入栈的情况)和返回地址入栈,现在栈中的情况如下(注意这里栈是向低地址增长的,也就是栈底在高地址,入栈时栈指针向低地址移动。):

===高地址===

参数 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指针,用来寻址栈上空间。


以上就是调用一个函数的时候栈上的动作,大致意思就是这样了。

0 0
原创粉丝点击