函数调用之详细分析

来源:互联网 发布:java中log 编辑:程序博客网 时间:2024/06/04 17:46

  印象中,函数调用是编译器负责的事情,之前学过汇编了解到,在函数调用时先要保存调用者函数的相关寄存器入栈,调用完再进行出栈等操作。今天偶然看到一篇文章详细分析了函数调用的过程,再次也对概念进行一下梳理。
  在window下,栈的生长方向是由高地址向低地址。在嵌入式中,栈的空间申请是放在*.s启动文件中的,我们可以对其进行修改。栈的默认空间相比于堆而言比较小,是一段连续的空间,经常会发生栈溢出问题。
  先来看看在栈中函数调用过程中其数据的存储方式,在window下通过一个小程序,查看其反汇编语言来说明。在此之前,先需要简单了解一下汇编机制下一些寄存器及指令。
1.32位通用数据寄存器 eax/ebx/ecx/edx,低16位寄存器(AX/BX/CX/DX),8位寄存器AH
-AL、BH-BL、CH-CL、DH-DL
寄存器AX和AL通常称为累加器(Accumulator),用累加器进行的操作可能需要更少时间。累加器可用于乘、除、输入/输出等操作,它们的使用频率很高;
寄存器BX称为基地址寄存器(Base Register)。它可作为存储器指针来使用;
寄存器CX称为计数寄存器(Count Register)。在循环和字符串操作时,要用它来控制循环次数;在位操作中,当移多位时,要用CL来指明移位的位数;
寄存器DX称为数据寄存器(Data Register)。在进行乘、除运算时,它可作为默认的操作数参与运算,也可用于存放I/O的端口地址。
2.变址和指针寄存器 ESI 和EDI
(1)ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
(2)EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
这是在codeblock下运行的一个C++程序
在主函数中:

0x0040134D  lea    0x4(%esp),%ecx0x00401351  and    $0xfffffff0,%esp0x00401354  pushl  -0x4(%ecx)0x00401357  push   %ebp0x00401358  mov    %esp,%ebp0x0040135A  push   %ecx0x0040135B  sub    $0x24,%esp0x0040135E  call   0x417cd0 <__main>0x00401363  movl   $0x1,-0xc(%ebp)0x0040136A  movl   $0x2,-0x10(%ebp)0x00401371  movl   $0x2,0x4(%esp)0x00401379  movl   $0x1,(%esp)0x00401380  call   0x401340 <sum(int, int)>0x00401385  mov    %eax,-0x14(%ebp)0x00401388  movl   $0x474024,0x4(%esp)0x00401390  movl   $0x47e860,(%esp)0x00401397  call   0x46f360 <std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)>0x0040139C  mov    -0x14(%ebp),%edx0x0040139F  mov    %edx,(%esp)0x004013A2  mov    %eax,%ecx0x004013A4  call   0x44fca0 <std::ostream::operator<<(int)>0x004013A9  sub    $0x4,%esp0x004013AC  movl   $0x46dce0,(%esp)0x004013B3  mov    %eax,%ecx0x004013B5  call   0x44f9e0 <std::ostream::operator<<(std::ostream& (*)(std::ostream&))>0x004013BA  sub    $0x4,%esp0x004013BD  mov    $0x0,%eax0x004013C2  mov    -0x4(%ebp),%ecx0x004013C5  leave0x004013C6  lea    -0x4(%ecx),%esp0x004013C9  ret

在函数内部插入一个断点,可以看到下面程序:

0x00401340  push   %ebp0x00401341  mov    %esp,%ebp0x00401343  mov    0xc(%ebp),%eax0x00401346  mov    0x8(%ebp),%edx0x00401349  add    %edx,%eax0x0040134B  pop    %ebp0x0040134C  ret

  看以看到当函数被调用时,首先会把调用函数的栈底压栈到自己函数的栈中(push %ebp),然后将原来函数栈顶rsp作为当前函数的栈底(mov %esp, %ebp)。函数运行完成时,会将压入栈中的rbp重新出栈到rbp中(popq %rbp)。
  我们知道,main主函数也是通过启动文件对其进行调用,是供程序员开发程序的主要入口。
  函数调用栈的数据结构

在linux下系统调用堆栈的情形还不一样,有机会再进行linux的汇编分析。

参考文献:
1. http://blog.jobbole.com/99301/

0 0
原创粉丝点击