函数的调用过程(栈帧)

1.栈:
特点:

  1. 先进后出;
  2. 有栈顶(esp)也有栈底(ebp);
  3. 入栈:push
    出栈:pop;
    main函数在mainCRTStartup被调用,所以Startup是c中第一个被调的函数
    调函数形成栈帧,消耗时间空间
    返回释放栈帧

    cpu中常用的寄存器:
    通用寄存器:EAX,EBX,ECX,EDX
    程序计数器:EIP(pc)(存放当前指令的下一条指令)
    EBP:基址寄存器(栈底寄存器)
    ESP:栈顶寄存器

函数调用在汇编语言里的指令call:

  1. 保存当前指令的下一条指令的地址,目的是方便恢复(入栈保存)
  2. 跳转到目标函数的入口地址处(jmp修改EIP)
    2.栈帧的建立与撤销
 #include<stdio.h>
int myAdd(int x,int y)
{
    int z=x+y;
    return z;
}

int main()
{
    int a=0xaaaaaaaa;
    int b=0xbbbbbbbb;
    int c=myAdd(a,b);

    printf("You should run here:%d\n",c);
    return 0;
}

汇编如下:

12:       int a=0xaaaaaaaa;
00401078   mov         dword ptr [ebp-4],0AAAAAAAAh
13:       int b=0xbbbbbbbb;
0040107F   mov         dword ptr [ebp-8],0BBBBBBBBh
14:       int c=myAdd(a,b);
00401086   mov         eax,dword ptr [ebp-8]
00401089   push        eax
0040108A   mov         ecx,dword ptr [ebp-4]
0040108D   push        ecx

对应的栈帧图为:这里写图片描述
此时对应的eax里为b,ecx里为a,
所以是先形成临时变量b,再形成临时变量a,即形参实例化时是从右向左,且临时变量保存在当前栈的esp栈顶所指向的内存里
临时变量是在被调和调用函数的栈帧结构之间
接下来执行call指令:

1.将当前指令的下一条地址(00401093)进行入栈保存
2. 跳转至目标函数的入口地址处(jmp),即修改eip的地址为myADD函数的入口地址00401020

@ILT+0(_myAdd):
00401005   jmp         myAdd (00401020)
@ILT+5(_main):
0040100A   jmp         main (00401060)

这里写图片描述

这里写图片描述

00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,44h
7:        int z=x+y;
00401038   mov         eax,dword ptr [ebp+8]
0040103B   add         eax,dword ptr [ebp+0Ch]
0040103E   mov         dword ptr [ebp-4],eax
8:        return z;

这里写图片描述
形成新的栈结构,供myADD使用,同时将eip的地址改为myADD code的地址
并且将a+b的结果压入栈中
mov ebp,esp意思是ebp和esp的内容保持一致、
myAdd函数的返回值是通过eax寄存器返回的
函数调用结束,返回到main函数中,栈帧撤销

ret的作用:
1. 将当前保存的函数的地址进行出栈
2. 弹出数值,恢复EIP
EIP地址重新修改为00401093,即返回到main函数中
这里写图片描述

00401093   add         esp,8
00401096   mov         dword ptr [ebp-0Ch],eax
15:       printf("You should run here:%d\n",c);
00401099   mov         edx,dword ptr [ebp-0Ch]
0040109C   push        edx
0040109D   push        offset string "You should run here:%d\n" 

调用结束后此时的栈结构为
这里写图片描述

修改myAdd函数的返回值,不让函数返回到main函数中,而是返回到自己写的另一个函数bug函数中
代码如下:

void bug()
{
    printf("hello,I am a bug!:)\n");
    system("pause");
}
int myAdd(int x,int y)
{
    printf("myAdd  begin run...\n");
    int z=0;
    int *p=&x;
    p--;
    *p=(int)bug;
    z=x+y;
    return z;
}

在myAdd函数中调用bug函数,返回值返回到bug函数中,但是编译器会崩掉,所以调用完bug函数后最终还是要返回到main函数中

这里写图片描述
bug要返回,首先得找自己的返回值
那就先来研究一下函数内部的第一个变量和第一个参数的关系:

void fun(int x)
{
    int y;
    printf("&x: %p\n");
    printf("&y: %p\n");
}

得出x和y 的地址差12
对应于上图中局部变量到00401093的地址差8,所以可以将bug函数的代码改为:

void bug()
{
    int x=0;
    int *p=&x;
    p+=2;

但是运行的时候编译器依然会崩掉,原因:
调用bug函数时,是指针跳转过去,没用call指令,也就是没有进行push操作
但是返回的时候执行了ret指令,也就是多pop了一次,即esp变大了一个地址,
所以还需将esp再减去4;
在c语言中插入汇编语言:–asm

    __asm
    {
        sub esp,4;

    }

这样就成功的从bug函数中返回到了main函数中

CTF(Capture The Flag)是一种信息安全竞赛,通常涉及一系列与安全相关的挑战和问题。在这些挑战中,函数调用的深入理解对于解决许多问题至关重要。 在计算机程序中,函数调用涉及到(Stack Frame)的创建和销毁。函数调用过程中用于存储函数参数、局部变量以及返回地址等信息的内存区域。具体来说,的结构通常包含以下几个部分: 1. 参数区域:存放函数调用传递给函数的参数值。 2. 返回地址:函数执行完毕后,程序需要返回到的地址,通常是调用函数之后的指令地址。 3. 保存的寄存器:保存调用者上下文中的寄存器值,以便函数返回后能够恢复。 4. 局部变量区域:存放函数内部定义的局部变量。 5. 保存的指针(可选):一些系统会使用指针来引用当前的,方便函数调用和变量访问。 6. 顶指针:指向当前顶部的指针。 函数调用过程中,一般步骤如下: - 参数准备:调用者将参数准备好,按照约定的方式中。 - 调用指令:执行调用指令(如x86架构的`CALL`指令),该指令将返回地址,并跳转到被调用函数的入口地址。 - 建立:被调用函数开始执行后,首先建立自己的,可能包括设置指针和局部变量等。 - 执行函数体:函数体内执行需要的操作,使用上的局部变量进行计算。 - 函数返回:当函数执行完毕准备返回,清理局部变量,恢复之前保存的寄存器状态和顶指针,然后执行返回指令(如`RET`指令)回到调用者。 在CTF竞赛中,理解结构和函数调用机制对于逆向工程、二进制分析和漏洞利用等安全相关挑战尤为关键。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值