函数调用--函数栈

文一

转载自函数调用--函数栈


函数调用大家都不陌生,调用者向被调用者传递一些参数,然后执行被调用者的代码,最后被调用者向调用者返回结果。还有大家比较熟悉的一句话,就是函数调用是在栈上发生的,那么在计算机内部到底是如何实现的呢?
 
对于程序,编译器会对其分配一段内存,在逻辑上可以分为代码段,数据段,堆,栈:
  1. 代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执行不可写
  2. 数据段:保存初始化的全局变量和静态变量,可读可写不可执行
  3. BSS:未初始化的全局变量和静态变量
  4. 堆(Heap):动态分配内存,向地址增大的方向增长,可读可写可执行
  5. 栈(Stack):存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,非常非常重要(因此下图中,栈底指针在上,栈顶指针在下,栈底指针的值大于栈顶指针),可读可写可执行
如图所示
寄存器
EAX:累加(Accumulator)寄存器,常用于函数返回值
EBX:基址(Base)寄存器,以它为基址访问内存
ECX:计数器(Counter)寄存器,常用作字符串和循环操作中的计数器
EDX:数据(Data)寄存器,常用于乘除法和I/O指针
ESI:源变址寄存器
DSI:目的变址寄存器
ESP:堆栈(Stack)指针寄存器,指向堆栈顶部--对栈操作则其值会不停动态变化
EBP:基址指针寄存器,指向当前堆栈底部
EIP:指令寄存器,指向下一条指令的地址

源代码
int print_out(int begin, int end)
{
 printf("%d ", begin++);
 int *p;
 p = (int*)(int(&begin) - 4);
 if(begin <= end)
  *p -= 5;
 return 1;
}
 
int add(int a, int b)
{
 return a+b;
}
 
int pass(int a, int b, int c) {
 char buffer[4] = {0};
 int sum = 0;
 int *ret;
 ret = (int*)(buffer+28);
 //(*ret) += 0xA;
 sum = a + b + c;
 return sum;
}
 
int main()
{
 print_out(0, 2);
 printf("\n");
 int a = 1;
 int b = 2;
 int c;
 c = add(a, b);
 pass(a, b, c);
 int __sum;
 __asm
 {
  mov __sum, eax
 }
 printf("%d\n", __sum);
 system("pause");
}


函数初始化

26: int main()
27: {
011C1540 push ebp //压栈,保存ebp,注意进行push操作隐含esp-4(栈顶变化)
011C1541 mov ebp,esp //把esp的值传递给ebp,设置当前ebp--通过把当前栈顶的值赋给它,设置栈底的值
011C1543 sub esp,0F0h //给函数开辟空间,范围是(ebp, ebp-0xF0)
011C1549 push ebx
011C154A push esi
011C154B push edi
011C154C lea edi,[ebp-0F0h] //把edi赋值为ebp-0xF0
011C1552 mov ecx,3Ch //函数空间的dword数目,0xF0>>2 = 0x3C
011C1557 mov eax,0CCCCCCCCh
011C155C rep stos dword ptr es:[edi] 
//rep指令的目的是重复其上面的指令.ECX的值是重复的次数.
//STOS指令的作用是将eax中的值拷贝到ES:EDI指向的地址,然后EDI+4
 

一般所用函数的开头都会有这段命令,完成了状态寄存器的保存,堆栈寄存器的保存,函数内存空间的初始化

函数调用

28: print_out(0, 2);
013D155E push 2 //第二个实参压栈
013D1560 push 0 //第一个实参压栈
013D1562 call print_out (13D10FAh)//返回地址压栈,本例中是013D1567,然后调用print_out函数
013D1567add esp,8  //(此条指令的地址即为调用print_out函数后的返回地址)两个实参出栈
//注意在call命令中,隐含的操作是把下一条指令的地址压栈,也就是所谓的返回地址

除了VS可能增加一些安全性检查外,print_out的初始化与main函数的初始化完全相同



被调用函数返回
013D141C mov eax,1  //返回值传入eax中
013D1421 pop edi   
013D1422 pop esi   
013D1423 pop ebx //寄存器出栈
013D1424 add esp,0D0h //从此开始的以下3条命令是调用VS的__RTC_CheckEsp,检查栈溢出
013D142A cmp ebp,esp
013D142C call @ILT+315(__RTC_CheckEsp) (13D1140h)
013D1431 mov esp,ebp //ebp的值传给esp,也就是恢复调用前esp的值(ebp是print_out函数的栈底,它的值为main中的esp的值)
013D1433 pop ebp //弹出ebp,恢复ebp的值
013D1434 ret  //把返回地址写入EIP中,相当于pop EIP

call指令隐含的操作是push EIP,ret指令隐含的操作是pop EIP,两条指令完全对应起来。
写到这里我们就可以分析一下main函数调用print_out函数前后堆栈(Stack)发生了什么变化,下面用一系列图说明
 
  
接下来是返回过程,从上面的013D1431 行代码开始
    
print_out函数调用前后,main函数的栈帧完全一样,perfect!
下面我们来看看print_out函数到底做了什么事情
int *p;
p = (int*)(int(&begin) - 4);
if(begin <= end)
  *p -= 5;

根据上面调用print_out函数后的示意图,可以知道p实际上是指向了函数的返回地址addr,然后把addr-5,这又会发生什么?
再回头看一下反汇编的代码:
013D1560 push 0 //第一个实参压栈
013D1562 call print_out (13D10FAh)//返回地址压栈,本例中是013D1567,然后调用print_out函数
013D1567 add esp,8  //两个实参出栈

分析可知,返回地址addr的值是013D1567 ,addr-5为013D1562 ,把返回地址指向了call指令,结果是再次调用print_out函数,
从而print_out函数实现了打印从begin到end之间的所有数字,可以说是循环调用了print_out函数
 
对于add函数,主要是为了说明返回值存放于寄存器eax中。
 
另外,VS自身会提供一些安全检查
CheckStackVar安全检查 http://blog.csdn.net/masefee/article/details/5630154 ,通过ecx和edx传递参数, 局部变量有数组时使用
__security_check_cookie返回地址检查, 数组长度大于等于5时使用
__RTC_CheckEsp程序栈检查,printf函数用使用





文二

1 问题描述

  在此之前,我对C中函数调用过程中栈的变化,仅限于了解有好几种参数的入栈顺序,其中的按照形参逆序入栈是比较常见的,也仅限于了解到这个程度,但到底在一个函数A里面,调用另一个函数B的过程中,函数A的栈是怎么变化的,实参是怎么传给函数B的,函数B又是怎么给函数A返回值的,这些问题都不能很明白的一步一步解释出来。下面,便是用一个小例子来解释这个过程,主要回答的问题是如下几个:

  1、函数A在执行到调用函数B的语句之前,栈的结构是什么样子?

  2、函数A执行调用函数B这一条语句的过程中,A的栈是怎样的?

  3、在执行调用函数B语句时,实参是调用函数A来传入栈,还是被调函数B来进行入栈?

  4、实参的入栈顺序是怎样的?

  5、执行调用函数B的过程中,函数A的栈又是怎样的,B的呢?

  6、函数B执行完之后,发生了什么事情,怎样把结果传给了函数A中的调用语句处的参数(比如:A中int c = B_fun(...)这样的语句)?

  7、调用函数的语句结束后,怎样继续执行A中之后的语句?

  大概的问题也就这些,其实也就是整个过程中一些自己认为比较重要的步骤。接下来详细描述这个过程,以下先给出自己的C测试代码,和对应的反汇编代码。

2 测试代码

  2.1 C测试代码

  C测试代码如下:(代码中自己关注的几个地方是L14 15 16 17)

int
fun(int *x, int *y)
{
    int temp = *x;
    *x = *y;
    *y = temp;

    return *x + *y;
}

int
main(void)
{
    int a = 5;
    int b = 9;
    int c = 3;
    c = fun(&a, &b);
    a = 7;
    b = 17;
    return 0;
}

 主要关注的地方是:

  1、main中定义int变量 a b c 时,是怎样的定义顺序?

  2、L17 的过程。

  3、进入fun之后,的整个栈的结构。

    2.2 汇编测试代码


 080483b4 <fun>:
 80483b4:    55                       push   %ebp
 80483b5:    89 e5                    mov    %esp,%ebp
 80483b7:    83 ec 10                 sub    $0x10,%esp
 80483ba:    8b 45 08                 mov    0x8(%ebp),%eax
 80483bd:    8b 00                    mov    (%eax),%eax
 80483bf:    89 45 fc                 mov    %eax,-0x4(%ebp)
 80483c2:    8b 45 0c                 mov    0xc(%ebp),%eax
 80483c5:    8b 10                    mov    (%eax),%edx
 80483c7:    8b 45 08                 mov    0x8(%ebp),%eax
 80483ca:    89 10                    mov    %edx,(%eax)
 80483cc:    8b 45 0c                 mov    0xc(%ebp),%eax
 80483cf:    8b 55 fc                 mov    -0x4(%ebp),%edx
 80483d2:    89 10                    mov    %edx,(%eax)
 80483d4:    8b 45 08                 mov    0x8(%ebp),%eax
 80483d7:    8b 10                    mov    (%eax),%edx
 80483d9:    8b 45 0c                 mov    0xc(%ebp),%eax
 80483dc:    8b 00                    mov    (%eax),%eax
 80483de:    01 d0                    add    %edx,%eax
 80483e0:    c9                       leave  
 80483e1:    c3                       ret    

 080483e2 <main>:
 80483e2:    55                       push   %ebp
 80483e3:    89 e5                    mov    %esp,%ebp
 80483e5:    83 ec 18                 sub    $0x18,%esp
 80483e8:    c7 45 f4 05 00 00 00     movl   $0x5,-0xc(%ebp)
 80483ef:    c7 45 f8 09 00 00 00     movl   $0x9,-0x8(%ebp)
 80483f6:    c7 45 fc 03 00 00 00     movl   $0x3,-0x4(%ebp)
 80483fd:    8d 45 f8                 lea    -0x8(%ebp),%eax
 8048400:    89 44 24 04              mov    %eax,0x4(%esp)
 8048404:    8d 45 f4                 lea    -0xc(%ebp),%eax
 8048407:    89 04 24                 mov    %eax,(%esp)
 804840a:    e8 a5 ff ff ff           call   80483b4 <fun>
 804840f:    89 45 fc                 mov    %eax,-0x4(%ebp)
 8048412:    c7 45 f4 07 00 00 00     movl   $0x7,-0xc(%ebp)
 8048419:    c7 45 f8 11 00 00 00     movl   $0x11,-0x8(%ebp)
 8048420:    b8 00 00 00 00           mov    $0x0,%eax
 8048425:    c9                       leave  
 8048426:    c3                       ret

3 分析过程

  3.1 main栈

  1、L24 执行push %ebp:main函数先保存之前函数(在执行到main之前的初始化函数,具体的细节可以参考程序员的自我修养这本书有讲整个程序执行的流程)帧指针%ebp。此时,即进入了main函数的栈,图标描述如下

描述

内容

注释

main:%ebp

被保存的start函数的%ebp

每个函数开始前,先保存之前函数的帧指针%ebp



  2、L25 执行mov %esp,%ebp:步骤1已经保存了之前函数的%ebp,接下来需要修改函数main的栈底指针,指示main栈的开始,即修改%ebp,使其内容为此时寄存器%esp的内容(C描述为:%ebp = %esp),此时栈结构如下:

描述

内容

注释

main:%ebp

被保存的start函数的%ebp

每个函数开始前,先保存之前函数的帧指针%ebp

  3、L26 执行sub $0x18,%esp:此处即修改main函数栈的大小。由于linux里,栈增长的方向是从大到小,所以这里是%esp = %esp - $0x18;关于为什么减去$0x18,即十进制的24,深入理解计算机系统一书P154这样描述:“GCC坚持一个x86编程指导方针,也就是一个函数使用的所有栈空间必须是16字节的整数倍。包括保存%ebp值的4个字节和返回值的4个字节,采用这个规则是为了保证访问数据的严格对齐。”,所以这里main函数栈的大小 = 24 + 4 + 4 = 32(分配的24,保存%ebp的4,保存返回值的4)。此时栈结构如下:

描述内容注释
main:%ebp被保存的start函数的%ebp每个函数开始前,先保存之前函数的帧指针%ebp
   
   
   
   
   
   
%esp  

  4、 L27 movl $0x5,-0xc(%ebp);L28 movl $0x9,-0x8(%ebp);L29movl $0x3,-0x4(%ebp)这三行是定义的变量a b c。此时栈结构如下,可以看出来,变量的定义顺序不是按照在main里面声明的顺序定义的,这个我不是很懂,求指导。

描述内容注释
main:%ebp被保存的start函数的%ebp每个函数开始前,先保存之前函数的帧指针%ebp
%ebp - 0x43c = 3
%ebp - 0x89b = 9
%ebp - 0xc5a = 5
   
   
   
%esp  

   5、L30 lea -0x8(%ebp),%eax; L31 mov %eax,0x4(%esp)这两行是把变量b的地址赋值到%esp + 4(变量b的地址入栈),栈结构如下:

描述内容注释
main:%ebp被保存的start函数的%ebp每个函数开始前,先保存之前函数的帧指针%ebp
%ebp - 0x43c = 3
%ebp - 0x89b = 9
%ebp - 0xc5a = 5
   
   
%esp + 0x4&b变量b的地址
%esp  

  6、L32 lea -0xc(%ebp),%eax; L33 mov%eax,(%esp)这两行是把变量a的地址赋值到%esp(变量a的地址入栈),栈结构如下:

描述内容注释
main:%ebp被保存的start函数的%ebp每个函数开始前,先保存之前函数的帧指针%ebp
%ebp - 0x43c = 3
%ebp - 0x89b = 9
%ebp - 0xc5a = 5
   
   
%esp + 0x4&b变量b的地址
%esp&a变量a的地址

 

  7、L34 call 80483b4 <fun>;可以看出这一行,即调用的是fun(int *, int *)函数,而且也从第6步知道实参是调用函数传入栈,且是逆序传入。这里call指令会把之后指令的地址压入栈,即L35的指令地址804840f。(从汇编代码看不出来这一步压栈的过程,但根据后续分析,这样是正确的,书上也是这么描述call指令的,怎样能直观的看到栈的变化,我不懂,哪位知道可以留言告诉我)此时栈的结构如下:

描述内容注释
main:%ebp被保存的start函数的%ebp每个函数开始前,先保存之前函数的帧指针%ebp
%ebp - 0x43c = 3
%ebp - 0x89b = 9
%ebp - 0xc5a = 5
   
   
 &b变量b的地址
 &a变量a的地址
%esp804840f返回地址

  到这一步,关于main函数栈的情况分析就到这里,接下来进入fun函数进行分析。

  3.2 fun函数栈

  1、L2 push%ebp:同main函数第一步一样,先保存之前函数的栈底,即保存main函数的帧指针%ebp,此时栈情况如下:

描述内容注释
main:%ebp被保存的start函数的%ebp每个函数开始前,先保存之前函数的帧指针%ebp
%ebp - 0x43c = 3
%ebp - 0x89b = 9
%ebp - 0xc5a = 5
   
   
 &b变量b的地址
 &a变量a的地址
 804840f返回地址
fun栈开始被保存的main函数的%ebp 

 

  2、L3 mov %esp,%ebp:同上述main描述里面步骤2,修改寄存器%ebp。栈如下:

描述内容注释
main:被保存的start函数的%ebp每个函数开始前,先保存之前函数的帧指针%ebp
 3c = 3
 9b = 9
 5a = 5
   
   
 &b变量b的地址
 &a变量a的地址
 804840f返回地址
fun栈开始(%esp与%ebp)被保存的main函数的%ebp 

  3、L4 sub $0x10,%esp:同上述main描述步骤3,修改函数fun的栈大小,(不明白的是这里怎么修改的大小为十进制16,这样加上其他的最后不是16的整数倍?)此时栈如下:

描述内容注释
main:被保存的start函数的%ebp每个函数开始前,先保存之前函数的帧指针%ebp
 3c = 3
 9b = 9
 5a = 5
   
   
 &b变量b的地址
 &a变量a的地址
 804840f返回地址
fun栈开始(%ebp)被保存的main函数的%ebp 
   
   
   
%esp  

 

  4、L5 mov 0x8(%ebp),%eax;L6 mov (%eax),%eax;L7 mov%eax,-0x4(%ebp):这三行功能分别是把%eax = &a; %eax = a; %ebp - 0x4 = a;对应的是fun函数语句int temp = *a;其中,L7会改变栈的情况,此时栈如下:

描述内容注释
main:被保存的start函数的%ebp每个函数开始前,先保存之前函数的帧指针%ebp
 3c = 3
 9b = 9
 5a = 5
   
   
 &b变量b的地址
 &a变量a的地址
 804840f返回地址
fun:%ebp被保存的main函数的%ebp 
 %ebp - 0x45a = 5
   
   
%esp  

 

  5、L8 mov 0xc(%ebp),%eax;L9 mov (%eax),%edx;L10 mov 0x8(%ebp),%eax; L11 mov %edx,(%eax)对应功能分别是:get &b; get b; get &a; a = b。其中,只有L11会修改栈内容,栈内容如下:

描述内容注释
main:被保存的start函数的%ebp每个函数开始前,先保存之前函数的帧指针%ebp
 3c = 3
 9b = 9
 9a = 9(修改了a的值)
   
   
 &b变量b的地址
 &a变量a的地址
 804840f返回地址
fun:%ebp被保存的main函数的%ebp 
%ebp - 0x45a = 5
   
   
%esp  

  6、L12mov 0xc(%ebp),%eax;L13 mov-0x4(%ebp),%edx;L14 mov %edx, (%eax):功能分别对应get &b; %edx = temp;b = a。其中L13会修改栈内容,具体栈情况更改如下:

描述内容注释
main:被保存的start函数的%ebp每个函数开始前,先保存之前函数的帧指针%ebp
 3c = 3
 5b = 5(修改了b的值)
 9a = 9(修改了a的值)
   
   
 &b变量b的地址
 &a变量a的地址
 804840f返回地址
fun:%ebp被保存的main函数的%ebp 
%ebp - 0x45a = 5
   
   
%esp  

 

  7、然后就是L15,L16,L17,L18这4行分别得到&a, a, &b, b。这些都不会造成栈内容的变化。

  L19 add %edx, %eax会计算出a + b的值,并把结果保存在寄存器%eax,也即返回值在%eax(这里大家都清楚,函数如果有返回值,一般都是保存在%eax)

  8、L10 leave:深入理解计算机系统一书P151这样描述leave指令:

    movl %ebp, %esp

    popl %ebp

    以下分两步来描述:

      即先把寄存器%ebp赋值给%esp,其中%ebp保存的是之前main函数的%ebp,这一步修改了%esp的内容,即栈情况会发生变化。这一步之后栈情况为:

描述内容注释
main:被保存的start函数的%ebp每个函数开始前,先保存之前函数的帧指针%ebp
 3c = 3
 5b = 5
 9a = 9
   
   
 &b变量b的地址
 &a变量a的地址
 804840f返回地址
%esp被保存的main函数的%ebp 

 

      然后是popl %ebp,即把%ebp的内容恢复为之前main函数的帧指针,经过这一步之后%ebp指向了main栈的开始处:如下表示

描述内容注释
main:%ebp被保存的start函数的%ebp每个函数开始前,先保存之前函数的帧指针%ebp
 3c = 3
 5b = 5
 9a = 9
   
   
 &b变量b的地址
 &a变量a的地址
 804840f返回地址
%esp(%ebp)被保存的main函数的%ebp 

 

  9、L21 ret:从栈中弹出地址,并跳转到这个位置。栈即如下:

描述内容注释
main:%ebp被保存的start函数的%ebp每个函数开始前,先保存之前函数的帧指针%ebp
 3c = 3
 5b = 5
 9a = 9
   
   
 &b变量b的地址
%esp&a变量a的地址

  到这里fun函数即执行完,然后又跳转到main函数开始执行后续指令。后续L35行用到的%eax即之前fun函数的返回值,L35 L36 L37都用到了%ebp,此时%ebp已经指向了main函数的帧指针,后面已经没有什么可以描述的了,最后还会修改变量a b c 的值,只需要相应的修改栈中内容即可,没有什么可说的了。

  到这里全部分析过程就结束了。希望能够帮助到跟我一样对过程调用不熟悉的朋友。

 

Reference

  Randal E.Bryant(作者) . 龚奕利(译者).深入理解计算机系统(第二版). 机械工业出版社. 2010.



  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值