栈帧详解———函数调用原理

时间:2022-09-24 03:38:44

前言:我们知道调用函数对这个函数传参时,形参实例化时会形成一份临时拷贝,在函数返回时这些临时拷贝又被释放;那么调用函数时这些参数是如何保存、被保存在哪里?又是如何释放的呢?在调用函数返回时是如何返回的?返回值是通过什么返回?类似的这些函数调用问题都可以通过栈帧原理解释

 

下面我通过一简单的实例来分析函数调用原理------栈帧

     首先应该明白,栈是从高地址向低地址延伸的。每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。每个函数都有一份属于自己的ebp和esp,cpu只有一份ebp和esp,那么怎样让每个函数都有ebp和esp指向栈底和栈顶呢?因为ebp和esp永远保存最新当前函数的值

 

寄存器ebp指向当前的栈帧的底部(高地址)

寄存器esp指向当前的栈帧的顶部(地址地)

PC指针:永远指向当前运行程序指令的下一条指令

下图为典型的存取器安排,观察栈在其中的位置

栈帧详解———函数调用原理


见上图,————黑色线指向的是调用者函数main()

       ————蓝色线指向的是被调用者函数fun()

       ————红色线指向的的是函数fun()返回时,恢复到调用函数fun()之前的状态

CPU在重复的执行三个操作:取指令、分析指令、执行指令

取指令通过pc指针,分析程序通过cpu里的各种指令集

 

通过下面这个代码及反汇编来分析函数调用原理:

#include<stdio.h>
#include<windows.h>

int fun(int x, int y)
{
int c = 0xcccccccc;
return c;
}
int main()
{
int a = 0xaaaaaaaa;
int b = 0xbbbbbbbb;
int ret = fun(a, b);
printf("You should runing here!\n");
system("pause");
return 0;
}

进入main函数:

形参实例化形成临时拷贝压入栈的过程是在调用者函数(main函数)内实现的:

栈帧详解———函数调用原理



ba依次压入栈后执行call命令,call命令的作用如下图所示:

栈帧详解———函数调用原理

栈帧详解———函数调用原理

jmp  使Pc指针指向fun函数(地址空间图上第三条蓝线;从上往下数,下面一样)

 

 

 

调用fun函数栈形成过程:

栈帧详解———函数调用原理

push ebp       首先将esp指针下移,然后把ebp寄存器的内容(main函数的栈底地址)

               存到fun函数的当前栈顶(当前esp指向的地址);  

      

mov ebp, esp    esp的内容赋给ebp(地址空间图上第一个蓝色线)fun函数的栈底。

 

sub  esp0cch   esp减去一个随机值,指向一个新的fun函数栈顶。(地址空间图上第     

                二条蓝色线)

 


 

fun函数返回时的过程:

栈帧详解———函数调用原理

mov   espebp   esp指向ebp的内容,形成新的栈顶

pop    ebp        将栈顶的内容(mainebp)弹出来放在ebp(地址空间图上的第一个

                  红线),把esp指针上移。

ret                mainretaddr返回给pcesp上移;使pc指向main函数的下一

                  条指令;(地址空间图上的第三条红线)

栈帧详解———函数调用原理

add  esp8     fun函数返回后ab就会被释放,所以这里给esp加两个整形的字节,

                使esp恢复到形参实例化前的位置(地址空间图上第二条红色线)

 

fun函数的返回值c是通过eax寄存器返回给main函数的

栈帧详解———函数调用原理

 —————————————————————————————————————————————————————

以上就是一个函数调用的整个过程及原理。




下面我们在看几个代码来了解压入栈的变量之间的地址关系:

1.想一想如何不改变形参y的值,通过形参x来改变y的值呢?

#include<stdio.h>
#include<windows.h>

int fun(int x, int y)
{
printf("修改之前:\nx -> %d\ny -> %d\n", x, y);
int c = 0xcccccccc;
int *p = &x;
p += 1;
*p = 20;
printf("修改之后:\nx -> %d\ny -> %d\n", x, y);

return c;
}
int main()
{
int a = 10;
int b = 10;
int ret = fun(a, b);
printf("You should runing here!\n");
system("pause");
return 0;
}
栈帧详解———函数调用原理

从地址空间图里可以发现,形参实例化是从左往右,所以栈空间里a的地址低b的地址高,又因为都是整形所以相差四个字节,a+1便是b的地址

 


2.那么被调用的函数定义的第一个变量与它的第一个形参之间在栈空间中相差几个字节

呢?下面看这个代码:

#include<stdio.h>
#include<windows.h>

int fun(int x, int y)
{
int c = 0xffffffff;

printf("%p\n", &c);
printf("%p\n", &x);
printf("%p\n", &y);
return c;
}
int main()
{
int a = 0xaaaaaaaa;
int b = 0xbbbbbbbb;
int ret = fun(a, b);
printf("You should runing here!\n");
system("pause");
return 0;
}

栈帧详解———函数调用原理

栈帧详解———函数调用原理


从这个代码可以发现fun函数的第一个变量c与形参x之间相差20个字节,我们从地址空间中可以发现afun函数返回地址相邻,所以相差四个字节,在vs2013ca相差20个字节,所以c与函数返回地址相差16个字节,这个和编译环境有关,不同的环境可能不同。

 

 

3.在看一个不通过fun函数调用,然后在fun函数里插入一个bug函数,这是怎样实现的呢?这里也是通过上面已知的地址关系进行插入的:

#include<stdio.h>
#include<windows.h>

void *add = NULL;

void bug()
{
int d = 0;
int *p = &d;
p += 4;
*p =(int *)add; //将fun函数原来的返回地址给bug函数,使返回到main函数
printf("bug function\n");
}

int fun(int x, int y)
{
int c = 0xffffffff;
int *p = &x;
p -= 1;
add = *p; //保存fun函数返回地址
*p = &bug; //篡改fun函数的返回地址,不返回main函数,而返回bug函数
printf("fun function\n");
return c;
}
int main()
{
int a = 0xaaaaaaaa;
int b = 0xbbbbbbbb;
int ret = fun(a, b);
printf("You should runing here!\n");
_asm{ //平衡栈帧
sub esp,4
}
system("pause");
return 0;
}


//因为bug函数不是fun函数直接调用的,而是通过修改fun函数的返回时的pc指针,

//使其跳转到bug函数,所以没有调用call命令,没有push bug函数返回地址,因此pc指针没有下移;//

//而bug返回时,进行了ret,pop使得函数返回地址弹出,pc指针上移。

 

//所以可以发现bug函数返回到main函数时,pc指针比原来调用函数时指针上移了一个指针,因此这里_asm汇编代码esp-4用来平衡栈