函数调用栈的过程与函数调用约定(学习笔记)

1、栈介绍

视频资料

栈介绍 - CTF Wiki

通过以上资料理解栈帧(stack frame)的概念

2、栈在内存中的位置

3c8171e217c548babd6e6e0afb6d71c1.png

f9f5413ef7ef454c9aa84dd6469955dc.png

3、函数调用在栈上的实现

例如下面这段测试代码:

#inclued <stdio.h>

void swap(int* a,int* b)
{
    int tmp;
    tmp = *a;
    *a = *b;
    *b = tmp;
}

int main()
{
    int c = 1,d = 2;
    swap(&c, &d);
    return 0;
}

执行到  ←  方向时,调用者main()准备调用swap()

9393dbd685154e8aafef7326ee59b08f.pngec07852926dc41a8bc19994c2ea558c9.png

接着压swap()的参数,从右往左压,esp往下移​​

37ce3f02a5f445c29166cd975555e920.png8f0499e0a2654c2980eac970acfc5886.png

push完参数后,通过call指令,建个传送门,跳转到swap():

  1. push eip:压入下一条指令的地址
  2. jmp swap:跳到swap()函数里去执行

68e15990a6144d46a645f865b8edda00.pnga9170a39b024403bad7225d7f0f1c71d.png

被调用者swap() 首先要把ebp调用过来,但是此时的ebp要保存上一个函数的Old ebp,所以需要先存着old ebp,然后把ebp指向esp的位置,和esp指向同一个地址。

此时ebp和esp共同指的old ebp,应该指向main栈帧的Old ebp所在的地址

ab5f5e6be47c467584810c80843f9ce1.pngdd88d135766f46c9a38b4e727e2f0857.png

随后esp先行去开路(sub 指令减去的值,是根据被调用者swap()声明的局部变量、还有一些其他的因素来定的),ebp守家

bd4c1219e93e415f95f30fcee87e5087.png958511398ef74f56abbad5c042f322ac.png841328a98adf4715b99a23e5ea6f3583.png

之后tmp的三条指令里面涉及到的变量访问,都是通过ebp加上或者减去某个值来访问的

a11019e078dd44dcb4191565133b4ab2.png

随后,任务完成了,swap()函数就要让esp回去了

此时ebp 因为有点事要先撤了,于是ebp就指向了之前的old ebp所在

(若esp 没有开辟空间,可以直接pop ebp)

9e33ac039f0a4694ac69a42b4c784867.png756feb9408174f00ab041f4a99f9c000.png

3b5a3b4b769f4f61a2aeb89def995364.png8de32572f87942b7b6d02554cb3f3a80.png

由于pop,栈指针寄存器 esp 自动递增,所以就指向了上一个地址

0a9350e8d0f547ed92f398b0d47605a6.png

swap()函数的最后,是通过一条ret 指令,将原调用者main()函数的下一条指令的地址pop 到eip 中执行。同理,由于pop,栈指针寄存器 esp 自动递增,所以就又指向了上一个地址

c6d26dbe661947cbbcefd8d4405bc841.pngadccdf57eb44460da089b2656df57916.png

现在程序执行流又回到了main()的层次,现在需要把没有用的当初压入的参数给清除掉(只需要把esp 加上某个值就ok了)

a5538f488e654b588240b032bfb06841.pnga7f5f55995d644b69afb081d7b22b53b.png

这样,ebp和esp就回到了最初的位置,本来eax 是用来返回 返回值 的,但是swap()函数没有返回值,eax就没有用了

由于最开始main()函数也没有给swap()函数分配寄存器,所以这里也不用还原寄存器。

如果以后main()函数还用调用其他函数的话,那么流程与调用swap()函数基本一致

若在进入函数前,如果有要保存的寄存器:

  1. 要先把寄存器压入栈中
  2. 之后压入被调用者的参数
  3. 再压入调用者下一条指令的地址
  4. 之后将执行流跳转到函数去执行

作为 被调用函数 :

  1. 首先将old ebp压入栈中,为了方便退出函数时原来栈帧的模样
  2. 之后将ebp 拉入新的栈帧中
  3. 接着esp 再开辟栈空间

52c0f2f5ab954e39836a7c5b2be5c2f5.png

被调用的函数退出时:

  1. 首先将esp 找回
  2. 之后pop 栈中的ebp,使ebp 回到old ebp的位置(此时esp会指向 原调用者 下一条指令的地址)
  3. 此时再ret ,将下一条指令的地址pop到eip 中,也就能回到调用者的执行流
  4. 最后再让调用者清除之前调用函数时压入的参数(栈平衡)

f78e80512bdb4ab7bcacb4dc4419df06.png

main()函数执行完后也是一样的返回方式,会一层一层的返回,栈帧也会一层一层的清除掉,知道最后退出程序。

概念图与实际进程图对比:

7cd4ffeeae55432fa7cf637c6794963f.png

测试代码:

#include <stdio.h>

int test1() 
{
    return 123;
}

void test2(int a1, int a2, int a3, int a4, int a5)
{
    int a = a5;
    int b = a4;
}

void __attribute__((__fastcall__)) test3(int a1, int a2, int a3, int a4, int a5)
{
    int a = a5;
    int b = a4;
}

void test4(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8)
{
    int a = 100;
    int b = a8;
    int c = a7;
    int d = 2;
    int e = 3;
}


int main()
{
    int a = 10;
    int b = test1();
    test2(1, 2, 3, 4, a);
    test3(1, 2, 3, 4, a);
    test4(1, 2, 3, 4, 5, 6, 7, b);

    return 0;
}

4、 函数调用约定

函数调用约定通常规定如下几方面内容:

  1. 函数参数的传递顺序和方式
            最常见的参数传递方式是通过堆栈传递。主调函数将参数压入栈中,被调函数以相对于帧基指针的正偏移量来访问栈中的参数。对于有多个参数的函数,调用约定需规定主调函数将参数压栈的顺序(从左至右还是从右至左)。某些调用约定允许使用寄存器传参以提高性能。
     
  2. 栈的维护方式
            主调函数将参数压栈后调用被调函数体,返回时需将被压栈的参数全部弹出,以便将栈恢复到调用前的状态。该清栈过程可由主调函数负责完成,也可由被调函数负责完成。
     
  3. 名字修饰(Name-mangling)策略        

            又称函数名修饰(Decorated Name)规则。编译器在链接时为区分不同函数,对函数名作不同修饰。

若函数之间的调用约定不匹配,可能会产生堆栈异常或链接错误等问题。因此,为了保证程序能正确执行,所有的函数调用均应遵守一致的调用约定。

常见调用约定

C语言函数调用栈——函数调用约定       

        Windows下可直接在函数声明前添加关键字__stdcall、__cdecl或__fastcall等标识确定函数的调用方式,如  int __stdcall main()

        Linux下可借用函数attribute 机制,如  int __attribute__((__stdcall__)) main()

5、使用函数调用栈来实现栈溢出

函数调用约定是一种规范,用于定义函数调用的方式和标准。它包括函数调用时参数的传递方式、栈帧的创建和销毁、返回值的处理等。函数调用约定并不能直接解决栈溢出问题,但可以在一定程度上减少栈溢出的可能性。

栈溢出通常是由于递归调用层次过深或函数内部使用大量的局部变量导致的。函数调用约定可以通过优化参数的传递方式,如使用寄存器传递参数,减少栈上的数据压栈操作,从而减少栈空间的使用。

此外,编译器也可以通过尾递归优化等技术来减少函数调用产生的栈空间消耗。

虽然函数调用约定可以在一定程度上减少栈溢出的可能性,但它并不能完全解决栈溢出问题。对于涉及大量递归或大量局部变量的场景,仍然需要注意栈空间的使用情况,避免发生栈溢出。在这种情况下,可能需要考虑使用动态内存分配或其他技术来替代栈空间的使用,以避免栈溢出问题。

  • 34
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
sigprocmask()函数是一个用于设置和修改进程信号屏蔽字的系统调用。它可以在进程内部控制信号的传递。在Linux系统中,sigprocmask()函数的原型如下: ```c int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); ``` 其中,参数how用于指定信号屏蔽的方式,可以取以下三个值: - SIG_BLOCK: 将信号集set中的信号添加到当前进程的信号屏蔽字中。 - SIG_UNBLOCK: 将信号集set中的信号从当前进程的信号屏蔽字中移除。 - SIG_SETMASK: 将当前进程的信号屏蔽字设置为信号集set。 参数set是一个指向信号集的指针,用于指定需要屏蔽的信号集。参数oldset是一个指向信号集的指针,用于保存调用之前的信号屏蔽字。 下面是一个简单的示例,展示了如何使用sigprocmask()函数屏蔽SIGINT信号: ```c #include <stdio.h> #include <signal.h> int main() { sigset_t set, oldset; sigemptyset(&set); sigaddset(&set, SIGINT); sigprocmask(SIG_BLOCK, &set, &oldset); printf("SIGINT signal is now blocked\n"); sleep(10); sigprocmask(SIG_SETMASK, &oldset, NULL); printf("SIGINT signal is now unblocked\n"); return 0; } ``` 在上面的示例中,首先创建了一个信号集set,并将其中的SIGINT信号添加到其中。然后使用sigprocmask()函数将这个信号集添加到当前进程的信号屏蔽字中,从而屏蔽了SIGINT信号。程序会打印一条消息,表示SIGINT信号已被屏蔽,然后等待10秒钟。在等待期间,如果按下Ctrl+C组合键发送SIGINT信号,程序不会响应该信号。10秒钟后,程序再次调用sigprocmask()函数,将之前保存的旧信号屏蔽字恢复,从而取消了对SIGINT信号的屏蔽。 需要注意的是,sigprocmask()函数对信号处理的影响是进程级别的,而不是线程级别的。因此,在多线程程序中,对信号屏蔽的处理需要特别小心,以避免出现意外的情况。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值