函数调用的过程

前两篇文章介绍了栈帧函数调用约定,这篇文章通过一个简单的程序来具体说明一下调用函数时都经历了哪些步骤。(建议结合着栈帧的介绍调用约定的介绍这两篇来理解这篇文章,然后通过一个buffer overflow的实例进行深入的理解)。

首先看一下源码,非常简单的一个例子,定义了一个foo()函数,将传入的参数xy的和赋值给z,然后返回zmain()函数中调用了foo()函数,将返回结果赋值给a并打印。

#include <stdio.h>

int foo(int x, int y)
{
        int z;
        z = x + y;
        return z;
}

int main(int argc, char* argv[])
{
        int a;
        a = foo(5, 6);
        printf("a = %d", a);

}

用pwndbg调试一下,在main函数下断点,下面是反汇编代码。

CF01.png

首先通过源码可以知道,main函数调用了foo函数,所以main函数又称调用函数(caller),foo函数又被称为被调用函数(callee)。然后通过反汇编代码可以看到,main函数在调用foo函数之前进行了传参,C语言默认的调用惯例是cdecl(不清楚的可以看一下之前的介绍调用惯例的文章),所以是通过栈进行传参的,并且是从右至左

单步运行到call foo处,查看一下栈的状态,可以看到,参数已经被逆序的压入栈中。

CF02.png

然后call foo,这个指令可以拆分为两步,第一步为push eip(保存返回地址),第二步跳转到foo函数处。

CF04.png

可以看到,进入foo函数后栈顶多了一个值,存入的是0x56555563这个地址,也就是在main函数中,foo函数下一条指令的地址,称之为返回地址,可以自行对比一下这个地址和第二个图中call foo这条指令的下一指令地址是不是一样的。

接下来再看一下foo函数的反汇编代码。

CF03.png

首先是保存调用函数ebp(也就是caller的基地址),然后将ebp寄存器的值更新为当前的栈顶的地址(也就是ebp被更新为callee的基地址)。接下来通过sub指令来抬高栈顶,为了保存寄存器和局部变量等。

CF05.png

然后通过ebp+偏移的方式将参数保存到寄存器中,然后进行相加,将结果保存到eax寄存器中,在32位程序中,返回值是放在eax寄存器里的。

最后是一个ret指令,这个指令和call指令是一对儿,他也可以理解为两步,第一步pop 返回地址,第二步jmp 返回地址,返回到call foo的下一条指令继续执行。call指令是相当于保存下一条指令的地址,然后调用callee,然后在callee的最后呢又通过ret指令,返回到之前保存的地址,继续执行。通俗的来讲就是,你在玩游戏的过程中,突然要处理一些事情,你就将游戏存档了,然后去办事去了,等事情办完了,通过那个存档点又继续了你的游戏。

至于栈溢出的漏洞,是由于一些函数没有对输入进行限制,你就可以去构造一个函数,通过覆盖掉返回地址,控制函数跳转。可以参考pwnable.kr的bof这道题.

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值