函数调用基础

前言

函数调用充满着程序整个生命周期,是很基础但又很重要的东西。在写这篇介绍的时候,笔者也用了相当的时间整理和验证,信息量也比较大,第一次看不一定能全部看懂,建议不懂的部分自行搜索,亲自动手验证也是极好的办法。
函数调用就是很基础但繁杂的东西,但仔细钻研的话,必定有所收获。如果大家在看的过程中有不懂的地方,可能是正常的,但事实上整个过程都是很自然和精细的,有疑问的话我们可以继续细细探讨

1. 函数调用过程

函数调用,其实就是线程栈的状态转变和回复的过程。这里说的函数调用过程,其实也可看作调用者和被调用者的一种约定。这个约定定义了函数调用时函数参数的传递方式,函数返回值的返回方式,寄存器如何在调用者和被调用者之间进行共享。 下面主要说说在调用一个函数的时候,是如何建立栈帧[1],以及被调函数完成结束后,如何回到调用的位置继续执行未完的代码。
[1]帧: 可当成被调函数的起始,主要用来描述函数调用关系

1.1 函数调用之–prologue

prologue其实就是建立栈帧的过程,它主要明目张胆地记下了来时的路,同时为将来的路作好铺垫,可谓瞻前顾后。

prologue特征

prologue其实很明显,只要看汇编我们基本能看到它,它其实就是两行汇编(在我们的环境下)
00522377 55 push ebp
00522378 8bec mov ebp,esp
(这里可能还有一些预留栈空间的东东)
当这两个指令都执行了,我们就可以认为新的函数帧已经生成,ebp也是基于当前被调用函数的。

prologue如何埋下伏笔,回到过去?

也就是短短的两行汇编,prologue就已经完成。
prologue标志如下:
push ebp 表示将 ebp 的值入栈 [2]
mov ebp,esp 表示将 esp 的值给 ebp(ebp=esp) [3]
解释:
[2]中入栈的动作,其实就是为了保留当前帧的基地址(即ebp)到栈上,方便在[3]中取得即将建立的新帧的基地址
此外,每次push都是将它本身的值入栈,而不是将它指向的值入栈

发散
[2]中,当前的ebp会是谁?它是不是上一帧的ebp?每一次我调用新函数都把上一帧的ebp都push入栈,会发生什么?
是的,当前ebp正是上一帧的ebp。考虑到每次我们都把上一帧的ebp都入栈,是不是说在一次多函数的嵌套调用中,我可以根据栈来找到每一帧的ebp,从而复原调用栈?应该如何找?
(可参照 再探WinDbg–如何用WinDbg重建调用堆栈)

看完上面的介绍,我可能会问的问题如下:

  1. 新帧是如何确立的?
    一般我们调用函数是通过 call 指令,到某个地址继续执行汇编,在有prologue的情况下,我们自然会看到标志性的两个指令,在保存ebp到栈并赋予它新值时,新帧正式确立。
  2. ebp和esp是什么鬼?
    ebp、esp都是通用寄存器。esp始终指向栈顶,ebp可看作一个函数帧的起点,详细见后面的补充。

1.2 函数调用之–epilogue

与建立新栈帧的prologue相对应,自然就有人要来收拾残局,把堆栈恢复到未调用前的状态,epilogue正是如此正义又从容的存在。

epilogue特征

mov esp,ebp [4] pop ebp // 回复外层函数ebp [5] ret [6] epilogue就是指回到当前调用函数的那个地方,并把函数帧重新取回上一层的帧。一般就可以当成 ret 附近的操作

这里不得不扯到call这个指令了,我们在恢复现场的时候,也多亏了call在调用前期耿直的配合。
call:
call指令其实相当于两个小指令:push eip + jmp dest_func_address

我们在调用一个函数时,会把当前指令的下一条指令入栈,随后上一帧入栈和新栈帧确立,正式到了调用的函数。其后基于该函数帧以上的栈可能又会push一些新的局部变量什么的,然而并没有什么关系。因为我们已经记住了esp(栈顶)应该回复到哪个位置。
在epilogue时,[4]会将栈顶重置为当前栈帧,也就是该函数的起点。然后回忆一下,我们的[2]正是将ebp入栈,而[5]就是取回上一层函数所对应的帧。这样貌似一切都回复过来了呢!等等,好像当前即将执行的代码不对啊?
是的,最后一句[6]其实包含了 pop eip 操作,这样eip也还原了。ret对应了我们的 call 指令,遥遥相对,call完成了一记助攻。

至此,在整个epilogue完成后,堆栈的状态也完全回复到未调用函数前的状态了,调用时发生了什么,创建了什么局部变量,或许已经难以考究。一切都好像没发生过,代码继续执行,直到再调用新的函数,再创建当前帧的局部变量,直到…它自己的epilogue。

2 函数调用中的参数相关问题

下面补充一下函数调用时函数参数的传递方式 首先由一个比较简单的程序引入,这个程序全部代码如下:

// function_call_learn.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream>
void empty()
{
	return;
}

void printNum(int k)
{
	std::cout << k;
	return;
}

int add(int i, int* j)
{
	int k = i + *j;
	return k;
}

int zer
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值