函数调用栈帧的分析

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

先来看一段代码

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

void fun(){
    printf("\n\nfun runing\n");
    Sleep(5000);
    printf("fun end\n");
    exit(1);
}

int fun1(int a,int b){
    printf("\n\nfun1 runing\n");
    int *p = &a;
    p--;
    *p = fun;
    p = &a;
    *(++p) = 0xcccc;
    int d = 0xdddd;
    printf("fun1 end\n");
    return d; 
}

int main(){
    printf("runing\n");
    int a = 0xaaaa;
    int b = 0xbbbb;
    int c = fun1(a,b);
    printf("a = %d\nb = %d\nc = &d\n",a,b,c);
    printf("should be here\n");
}

这段小程序在vs2013中的运行结果:
函数调用栈帧的分析

为什么会是这个结果嘞。按照main函数的逻辑,应该是在调用完fun1函数之后返回到main函数中继续执行之后的代码,诡异的是非但没有回到主函数中反而进入了fun函数。这个现象是很可怕的,如果这个fun函数是一段恶意程序,那后果不敢想象。
要解释上述现象的原因,就必须说一说函数调用原理–栈帧。

一、当可执行程序执行的时候可执行文件加载到内存中,编译器会为对其分配一段空间,在逻辑上可以分为代码段,数据段,堆,栈;
代码段:保存程序文本,指令指针EIP就是指向代码段,可读可执行不可写
数据段:保存初始化的全局变量和静态变量,可读可写不可执行
BSS:未初始化的全局变量和静态变量
堆(Heap):动态分配内存,向地址增大的方向增长,可读可写可执行
栈(Stack):存放局部变量,函数参数,当前状态,函数调用信息等,向地址减小的方向增长,非常非常重要,可读可写可执行
如图所示:
函数调用栈帧的分析
寄存器
EAX:累加(Accumulator)寄存器,常用于函数返回值
EBX:基址(Base)寄存器,以它为基址访问内存
ECX:计数器(Counter)寄存器,常用作字符串和循环操作中的计数器
EDX:数据(Data)寄存器,常用于乘除法和I/O指针
ESI:源变址寄存器
DSI:目的变址寄存器
ESP:堆栈(Stack)指针寄存器,指向堆栈顶部
EBP:基址指针寄存器,指向当前堆栈底部
EIP:指令寄存器,指向下一条指令的地址

二、当发生函数调用的时候,栈空间中存放的数据是这样的:
1、调用者函数把被调函数所需要的参数按照与被调函数的形参顺序相反的顺序压入栈中,即:从右向左依次把被调函数所需要的参数压入栈;
2、调用者函数使用call指令调用被调函数,并把call指令的下一条指令的地址当成返回地址压入栈中(这个压栈操作隐含在call指令中);
3、在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后再保存调用者函数的栈顶地址,即:当前被调函数的栈底地址(mov ebp,esp);
4、在被调函数中,从ebp的位置处开始存放被调函数中的局部变量和临时变量,并且这些变量的地址按照定义时的顺序依次减小,即:这些变量的地址是按照栈的延伸方向排列的,先定义的变量先入栈,后定义的变量后入栈;
所以,发生函数调用时,入栈的顺序为:
参数N
参数N-1
…..
参数2
参数1
函数返回地址
上一层调用函数的EBP/BP
局部变量1
局部变量2
….
局部变量N
函数调用栈如下图所示:
函数调用栈帧的分析
解释:
首先,将调用者函数的EBP入栈(push ebp),然后将调用者函数的栈顶指针ESP赋值给被调函数的EBP(作为被调函数的栈底,mov ebp,esp),此时,EBP寄存器处于一个非常重要的位置,该寄存器中存放着一个地址(原EBP入栈后的栈顶),以该地址为基准,向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取函数的局部变量值,而该地址处又存放着上一层函数调用时的EBP值;
一般而言,SS:[ebp+4]处为被调函数的返回地址,SS:[EBP+8]处为传递给被调函数的第一个参数(最后一个入栈的参数,此处假设其占用4字节内存)的值,SS:[EBP-4]处为被调函数中的第一个局部变量,SS:[EBP]处为上一层EBP值;由于EBP中的地址处总是”上一层函数调用时的EBP值”,而在每一层函数调用中,都能通过当时的EBP值”向上(栈底方向)能获取返回地址、参数值,向下(栈顶方向)能获取被调函数的局部变量值”;
如此递归,就形成了函数调用栈。
下面用一段代码验证:

#include<stdio.h>

void fun(int a,int b,int c){
    int x = 0xffffffff;
    int y = 0xeeeeeeee;
    int z = 0xdddddddd;
    int *pa = &a;
    int *pb = &b;
    int *pc = &c;
    printf("\n\n参数c的地址 %x %x\n", pc,c);
    printf("参数b的地址 %x %x\n", pb,b);
    printf("参数a的地址 %x %x\n\n", pa,a);
    printf("\n局部变量x的地址 %x %x\n", &x,x);
    printf("局部变量y的地址 %x %x\n", &y,y);
    printf("局部变量z的地址 %x %x\n", &z,z);
}

int main(){
    int a = 0xaaaaaaaa;
    int b = 0xbbbbbbbb;
    int d = 0xdddddddd;
    int c[10] = { 0 };
    printf("\n\n局部变量a的地址 %x %x\n",&a,a);
    printf("局部变量b的地址 %x %x\n", &b,b);
    printf("数组c[9]的地址 %x\n",&c[9]);
    printf("数组c[0]的地址 %x\n\n", &c[0]);
    fun(a,b,d);
    getchar();
}

运行结果:
函数调用栈帧的分析
结果分析:
在main函数中可以看到局部变量的分布是按照总高地址向低地址排布,对于数组来说,首地址在低地址,尾在高地址。然后再main函数中调用了fun函数,此时发生函数调用栈帧。可以看到函数参数列表中的参数排布是按照从参数c开始到参数a结束,地址依次减小;接下来是函数的返回地址。然后是fun函数的局部变量,按照先后定义的次序从高地址处开始依次排布。

三、关于堆栈空间利用最核心的一点就是:函数调用栈。而要深入理解函数调用栈,最重要的两点就是:栈的结构变化,ebp寄存器的作用。
首先要认识到这样两个事实:
1. 一个函数调用动作可分解为:零到多个push指令(用于参数入栈),一个call指令。call指令内部其实还暗含了一个将eip返回地址(即call指令下一条指令的地址)压栈的动作。
2. 几乎所有本地编译器都会在每个函数体之前插入类似的指令:push %ebp,mov %esp %ebp。
因此,在程序执行到一个函数的真正函数体的时候,已经有以下数据压入到堆栈中:零到多个参数,返回地址eip,ebp。
由此得到如下的栈结构(其中参数入栈顺序跟调用方式有关,这里以C语言默认的CDECL为例):
函数调用栈帧的分析

首先将ebp入栈,然后将栈顶指针esp赋值给ebp。“mov %esp %ebp”这条指令表面上看是用esp把ebp原来的值覆盖了,其实不然,因为在给ebp赋值之前,原ebp值已经被压栈(位于栈顶),esp赋值给ebp后,ebp恰好指向栈顶(即被压栈的原esp的位置)。
此时,ebp寄存器就处在一个非常重要的地位,该寄存器中存储着栈中的一个地址(原ebp入栈后的栈顶),以该地址为基准,向上(栈底方向)能获取返回地址,函数调用参数值;向下(栈顶方向)能获取函数局部变量值;而该地址处又存储着上一层函数调用时的ebp值!!一般而言,ss:[ebp+4]处为返回地址,ss:[ebp+8]处为第一个参数值(最后一个入栈的参数值,此处假设其占用4字节内存),ss:[ebp-4]处为第一个局部变量,ss:[ebp]处为上一层ebp值。
由于ebp中的地址总是“上一层函数调用时的ebp值”,而在每一层函数调用中,都能通过当时的ebp值“向上(栈底方向)能获取返回地址、函数调用参数,向下(栈顶方向)能获取函数局部变量值”。如此形成递归,直至到达栈底。这就是函数调用栈。由此看见,编译器对于ebp寄存器的使用实在是太精妙了。此外,从当前ebp出发,逐层向上找到所有的ebp是非常容易的。

四、现在回到最开始的程序。应该很清楚出现这种现行的原因了。
在fun函数中,将第一个参数a的地址赋予int* p变量,p–即可获得函数的返回地址,因此通过*p = fun指令;将返回地址修改为fun的入口地址。让原本应该返回main函数的程序进入到了fun函数中。在使用p也可以获得参数b的地址,如果获得了参数的地址,修改其内容就很简单啦。通过下面三行代码很轻易的纠可以不是用参数b的名称达到访问参数b的效果。

int *p = &a;
p++;
*p = 0x12345678;