函数的堆栈调用

每一个程序的执行都使用了栈,栈被定义为一个特殊的容器,用户可以将数据压入栈中(入栈,push),也可以将已经压入的数据弹出(pop,出栈),但栈这个容器必须遵守一条规则“先进后出”。

在操作系统中,栈是动态内存区域,程序可以将数据压入栈中,也可以将数据从栈顶弹出。在i386下,栈顶由称之为esp的寄存器进行定位。

栈在程序运行中具有举足轻重的地位。栈保存了一个函数调用所需要的维护信息,被称之为堆栈帧或活动记录。一般包括如下几个方面的内容:

1.函数的返回地址和参数;

2.临时变量:包括函数的非静态局部变量以及编译器自动生成的其他临时变量;

3.保存的上下文:包括在函数调用前后需要保持不变的寄存器。
 

具体写一个例子来进行分析:

#include<stdio.h>
int sum(int a,int b)
{
    int tmp=0;
    tmp=a+b;
    return tmp;
}
 
int main()
{
    int a=10;
    int b=20;
    int ret=0;
 
    ret=sum(a,b);
    printf("ret=%d\n",ret);
    return 0;
}
首先,先从main函数开始,查看main()函数的反汇编代码,进去之后会发现在一开始就看到如下部分:

紧接着,继续看反汇编,如下图

画出在栈中的整个过程:

整个函数的调用,结合上面两张图就基本明白了。那现在说一下刚开始的问题,在第一张图中也做了一点标注。对于每个函数的刚开始都会出现基本类似的指令。这一大堆指令总结起来就干了四件事情:

第一:将调用方的栈底地址入栈。====》push  ebp

第二:让原本指向调用方栈底的ebp指向当前函数的栈底。====》mov   ebp,esp

第三:给当前函数开辟栈帧。====>sub   esp,44h

第四:对开辟的栈帧进行初始化。初始化的大小不一定。====>rep   stos  

所以对于sum函数我们可以理解,但是在main函数刚开始也有这些指令,不由地,我们知道,main函数也是通过一个函数来进行调用的,所以也需要上面这四个步骤!!此时也就可以回答图二中我画??的地方咯,它一定存的是调用main函数的函数栈底地址。是不是很清楚呀^O^

自己写一下整个过程:

#include<stdio.h>
int sum(int a,int b)
{
    /*
    push ebp
    mov ebp,esp
    sub esp,44h
    push ebx
    push esi
    push edi
    lea edi,[ebp-44h]
    mov ecx,11h
    mov eax,0xccccccch
    rep stos ===>[esp,ebp]=0xcccccccc
    */
    int tmp=0;//mov dword ptr[ebp-4],0
    tmp=a+b;
    /*
    mov eax,dword ptr[ebp+8]
    add eax,dword ptr[ebp+0ch]
    mov dword ptr[ebp-4],eax
    */
    return tmp;//mov dword ptr[ebp-4],eax
}
/*
mov eax,dword ptr[ebp-4]
mov esp,ebp
pop ebp
ret
*/
 
int main()
{
    /*
    push ebp
    mov ebp,esp
    sub esp,44h
    push ebx
    push esi
    push edi
    lea edi,[ebp-44h]
    mov ecx,11h
    mov eax,0xccccccch
    rep stos ===>[esp,ebp]=0xcccccccc
    */
    int a=10;//mov dword ptr[ebp-4],0Ah
    int b=20;//mov dword ptr[ebp-8],14h
    int ret=0;//mov dword ptr[ebp-0Ch],0
    ret=sum(a,b);
    /*
    mov eax,ptr[ebp-8]
    push eax
    mov ebx,ptr[ebp-4]
    push ebx
    push ecx
    call sum
    add esp,8
    mov dword ptr[ebp-0ch],eax
    */
    printf("ret=%d\n",ret);
    return 0;
}
总结一下吧~

1、函数的运行都是在栈上开辟内存。

2、栈是通过esp(栈顶指针)、ebp(栈底指针)两个指针来标识的。

3、对于栈上的访问都是通过栈底指针的偏移来访问的。

4、在call一个函数时,有两件事情要做:先将调用函数的下一行指令的地址压入栈中;再进行跳转。

5、在函数调用时检查函数是否申明、函数名是否相同、函数的参数列表是否匹配、函数的返回值多大。

①如果  【函数的返回值<=4个字节】,则返回值通过寄存器eax带回。

②如果  【4<函数的返回值<=8个字节】,则返回值通过两个寄存器eax和edx带回。

③如果  【函数的返回值>8个字节】,则返回值通过产生的临时量带回。

6、函数结束ret指令干了两件事:先出栈;再将出栈的值放到CPU的PC寄存器中。因为PC寄存器中永远放的是下一次执行指令的地址,所以就顺理成章的在函数调用完之后依旧接着原来的代码继续执行。

本文部分为转载文章,吃水不忘挖井人,写的特别详细,收获良多!以下是原作者信息:
作者:zhuoya_ 
原文:https://blog.csdn.net/zhuoya_/article/details/80516246 
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值