深入理解函数的调用--栈帧

来源:互联网 发布:淘宝千人千面原理 编辑:程序博客网 时间:2024/06/16 17:44

为什么要有函数

c语言作为一门面向对象的语言,把要实现的功能分成很多模块,每一个模块我们称为函数,每个函数承担某一功能,在使用过程中我们可能会经常使用这些函数,调用函数即可实现功能,不用重新定义,节省了重复代码和时间。

本文使用的代码

#define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h>#include <stdlib.h>int add(int x, int y){    int z = x + y;    return z;}int main(){    int a = 0xAAAAAAAA;    int b = 0xBBBBBBBB;    int c = add(a, b);    system("pause");    return 0;}

调用堆栈

c语言中(这里我们暂时只讨论c语言)函数可以被调用,比如我们需要求两个数的和,用函数实现,可以取名叫add,调用add就能够实现两个数相加的过程。既然add可以被调用,那么主函数main呢?在vs2017调试状态下打开调用堆栈窗口,默认是不显示main之前的函数的,在调用堆栈窗口右键,选择显示外部代码,得到下面的窗口
jo-qzy的博客
可以看到图中在main函数的下面有几个未见过的名字,最底下有一个mainCRTStartup(),在堆栈中,如果要调用函数,会把调用的函数入栈,先执行调用的函数,比如我调用add函数,main函数上面就会显示add(如下图),说明main函数其实是被调用的,在其之前最早开始的函数是mainCRTStartup
jo-qzy的博客

几句需要了解的汇编指令和几个寄存器

为了更好的理解栈帧的开辟过程,首先需要了解几句汇编指令和几个调用过程中要用到的寄存器

寄存器:

ebp:栈帧的栈底指针
esp:栈帧的栈顶指针
eax、ebx、ecx、edx:通用寄存器
eip:程序计数器(pc指针),指向当前正在执行指令的下一条语句

汇编指令:

push eax(这里eax只是举个例子,下面相同):入栈,将eax里的数据存入栈esp向下移4个地址
pop eax:出栈,将寄存器esp指向的空间的数据给eax,esp向上移动4个地址
mov eax , 0AAAAAAAAh:0xAAAAAAAA移动到eax中
add ebp , 8:给ebp的指针位置加8,也就是上移8个地址
sub ebp , 8:给ebp的指针位置减8,也就是下移8个地址
ret:出栈一次,esp指针地址减4,将出栈的数据存入eip
call add (00401000):入栈一次,入栈的数据是当前指令的下一条汇编指令的地址,跳转到指令地址00401000,通过jmp来修改eip来实现跳转

栈帧

什么是栈帧:

函数调用过程中,肯定需要空间的开辟,而调用这个函数时为该函数开辟的空间就叫做该函数的栈帧

栈帧的创建过程:

看下图,图中是main函数的汇编代码(学习栈帧需要在汇编下来了解过程,所以本篇的栈帧开辟过程在vc++6.0下来画)
从main开始,在int a = 0xAAAAAAAA;前面有一部分汇编代码
这部分代码包括了存main函数之前的函数的esp和ebp指针地址和开辟栈的汇编代码,我们不去关心

这里写图片描述

重点看int a = 0xAAAAAAAA;后面的代码
mov dword ptr [ebp-4],0AAAAAAAAh
ebp-4即往下减4的地址,也就是开辟4个空间给变量a
下一句ebp-8则是为b开辟的空间
从此可以看出,为变量开辟空间是从髙地址向下开辟的,a的地址高,b的地址低
在这之后调用add函数,可以看到又申请了两个空间来存放临时变量,并将其入栈
先入栈的是b的临时变量,再是a,临时变量根据参数列表从右往左来创建的
也就是说临时变量是在main内申请的,这个过程我们称为形参实体化,形参是实参的一份临时拷贝
call add (00401000)入栈存下当前程序执行的语句的下一句的地址,方便以后跳回
接下来跳转到add函数的地址(00401000),调用add函数
在call这句汇编指令处按F11可以进入跳转的部分汇编代码,会有一句jmp汇编指令,这句话就是汇编里实现跳转的指令
上面所有指令花去的地址空间,我们就称之为main函数的栈帧,栈帧的大小不是固定的,它随着语句的执行在更改

接下来是add函数的栈帧

jo-qzy的博客

从int add(int x, int y)下面看起
有一句push ebp,也就是把main的ebp存下来,方便之后add结束时把ebp修改回来,此时esp再次减4
下一句mov ebp,esp将ebp的指针移到esp处
接下来的几句语句是开辟add需要的内存空间的指令,我们不细讲,但是可以看出esp已经移到新的开辟的地址的顶部
然后实现z = x + y:
mov eax,dword ptr [ebp+8](还记得之前创建的临时变量a和b吗,ebp-8是临时变量a)
add eax,dword ptr [ebp+0Ch](ebp+12的地址就是临时变量b的地址)
mov dword ptr [ebp-4],eax
add将x (临时变量a)和 y (临时变量b)加在一起存到eax中
eax再把值给z(z的地址ebp - 4)
要return了,将z的值取出来放到eax中,等会再处理,先存着
上述的在add函数内执行汇编所消耗的内存地址,称作add函数的栈帧

下面看add执行完了怎么回到main函数

jo-qzy的博客

可以看到add函数结束后还有几条汇编指令
首先执行指令pop了几次,出栈,我们暂时不考虑这几句pop
下面这句mov esp,ebp,让esp指向ebp
也就是说这句话执行后,之前使用的空间已经还给计算机了(这地方有点难,多想几遍)
PS:内存归还不一定要把内存清零,释放不等同于归零,没有其他程序在占用内存就是空闲的
pop ebp出栈一次,esp+4,上移4个地址,并将出来的数据给ebp
我们看一下出栈的数据是什么(看上一张图的ebp的位置),之前在这个出栈的地方存了main的ebp的地址
这样ebp就指回到main的ebp了
下一条指令ret:出栈一次,并把出栈的地址给eip
出栈一次,这次出栈的数据,就是之前存的00401063
这就是之前调用add函数汇编指令的下一句汇编指令的地址,把这个地址给eip
我们再按F10,此时就回到了main函数里去了

释放临时变量a和临时变量b以及返回值的处理

jo-qzy的博客

add esp,8该句指令将esp的地址向上移了8个位置,作用是归还了临时变量的空间
mov dword ptr [ebp-0Ch],eax,将刚才的z的值给c,完成返回值的动作

至此,我们就完成了add函数的调用过程,下面的指令就不再继续讨论,本文主要是讨论函数的栈帧实现过程

下面是一段函数非法调用的代码

不使用调用语句,仅通过修改内存里的信息来实现函数的非法调用

如果你对此代码感兴趣,可以前往我的github下载此段代码

#include <stdio.h>#include <stdlib.h>#include <time.h>void* ret = NULL;void bug(){    int x = 0;    int *q = &x;    q = q + 2;//q指向ebp-4,而存储返回语句的地址的地址在ebp+4    *q = ret;//把返回的地址直接放入q    printf("bug : haha,you meet a bug!\n");}void test(int x){    int *p = &x;//这里通过形参x的地址来找存储main函数里的汇编语句的地址    p--;//临时变量的前一个地址,存的是该函数结束后需要跳转的main函数语句的地址    ret = *p;//把该函数结束时本来要跳转的语句的地址记录下来    *p = bug;    printf("test : you get in test!\n");}int main(){    int a = 1;    printf("main : test begin!\n");    test(a);    __asm//插入一段汇编代码    {        sub esp,4//没有这一部分,程序可以运行但是会报错    }    printf("main : finish run test code!\n");    system("pause");    return 0;}
原创粉丝点击