c语言函数调用过程-栈帧详解

c语言函数调用过程-栈帧详解

测试环境,vs2017,win10

用一个测试代码来看

#include <stdio.h>

int fun(int a, int b)
{
    int z = a + b;
    return z;

}
int main()
{
    int a = 0xaaaaaaaa;
    int b = 0x11111111;
    int c = fun(a, b);
    printf("%d", c);

}

F10单步调试到fun函数调用这步,查看反汇编代码是这个样的

    int a = 0xaaaaaaaa;
00DA180E  mov         dword ptr [a],0AAAAAAAAh  
    int b = 0x11111111;
00DA1815  mov         dword ptr [b],11111111h  
    int c = fun(a, b);
00DA181C  mov         eax,dword ptr [b]  
00DA181F  push        eax  
00DA1820  mov         ecx,dword ptr [a]  
00DA1823  push        ecx  
00DA1824  call        _fun (0DA113Bh)  
00DA1829  add         esp,8  
00DA182C  mov         dword ptr [c],eax  
    printf("%d", c);

首先要明白函数中的non-static变量都是存在于进程的地址空间中的栈区。

*这段进程的地址空间会在程序运行期间一直存在

ebp 指向栈底, esp 指向栈顶,eip 存放cpu下一条要读取的指令地址

9RjaxP.png

栈区从高地址向下生长,压入新的变量。

从汇编的前两句push可以看出函数的两个参数b先压入,a后进入栈中,然后调用call指令。

call 指令有两个作用

  1. 先把call 的下一条指令地址存到内存中(栈)
  2. 将eip的值用函数的地址覆盖(完成了函数跳转)

9RjcPs.png

之后进入F11 进入 fun函数的汇编指令

int fun(int a, int b)
{
00DA17B0  push        ebp  
00DA17B1  mov         ebp,esp  
00DA17B3  sub         esp,0CCh    
...
    return z;
00DA17D7  mov         eax,dword ptr [z]  

}
...
00DA17DD  mov         esp,ebp  
00DA17DF  pop         ebp  
00DA17E0  ret

这段代码首先压入ebp,这时ebp的值是main函数的栈底,然后ebp指向esp,给esp减去一个常数等于开辟了一块新的空间,这个新的空间就是fun函数的栈帧。

9RjhrT.png

当有一个返回值z时,把他保存到eax寄存器里,然后移动ebp,esp,使得回到main函数的栈帧中。调用ret,和call相反,先pop出之前在栈里保存的call的下一条指令地址,然后覆盖eip达到回去的目的。
9Rjoa4.png

回去看一下call的下一条命令地址是不是0x00da1829!!!

下面在内存中确认一下栈中的数据对不对
9RjIZF.png

和我们说的栈数据一致。

通过栈帧结构修改函数返回时的跳转地址

#include <stdio.h>
#include <windows.h>
void fun_b()
{
    int a;
    printf("fun b");
    Sleep(3000);
}

int fun_a(int x)
{
    int c = 0;
    int *p = &x;
    p--;
    *p = fun_b;
    printf("fun a 函数\n");
}
int main()
{

    printf("主函数\n");
    fun_a(1);
    system("pause");
}

第一个参数地址加一就是返回指令的地址,修改为funb函数的地址,达到跳转效果。也可以用c变量地址向上寻找。

打印结果:

主函数

fun a 函数

fun b

然后崩溃,因为funb在执行完之后不知道跳转到哪里(因为之前没有用call调用funb,所以少压入一个返回指令地址,esp应该是比funa开始的esp大1的)。

使非正常跳转的函数回来

上面的问题就是funb不知道返回地址,那我们可以在进入funa的时候保存一下call的下一条指令地址。

#include <stdio.h>
#include <windows.h>
void * ret = NULL;
void fun_b()
{
    int a;
    int *p = &a;
    printf("fun b\n");
    p += 4;                          //在这里因为没有参数,所以用第一个变量地址找返回指令的地址,
                                    //在我的环境下是+4
    *p = (int)ret;
    Sleep(3000);
}

int fun_a(int x)
{
    int c = 0;
    int *p = &x;
    p--;
    ret = *p;
    *p = fun_b;
    printf("fun a 函数\n");
}
int main()
{

    printf("主函数\n");
    fun_a(1);
    printf("我又回来了!\n");
    system("pause");
}

打印结果:

主函数
fun a 函数
fun b
我又回来了!
请按任意键继续…

手动调整main栈帧结构

但是一按回车就又崩了,这个问题是因为不正常函数跳转导致少一次call 压栈,最后多push了一次,导致main函数的栈帧结构不平衡,手动给ESP-4。

#include <stdio.h>
#include <windows.h>
void * ret = NULL;
void fun_b()
{
    int a;
    int *p = &a;
    printf("fun b\n");
    p += 4;
    *p = (int)ret;
    Sleep(3000);
}

int fun_a(int x)
{
    int c = 0;
    int *p = &x;
    p--;
    ret = *p;
    *p = fun_b;
    printf("fun a 函数\n");
}
int main()
{

    printf("主函数\n");
    fun_a(1);
    printf("我又回来了!\n");
    _asm {
        sub esp, 4;             //在这里嵌入汇编代码,ok啦
    }
    system("pause");

}

完美解决~~

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页