函数的工作机制主要是依托栈结构来实现的,首先栈的一种后进先出的结构,为什么使用这种数据结构,因为这和我们函数调用的流程很类似,当程序嵌套调用时,最后一个调用的函数总是最先返回。
对于函数,我们先从宏观上大体的先去了解,后面在看一些内部细节,简单理解函数调用的话,就是在程序动态的运行中,每进入一个函数,总有一块独立的栈函数空间供它使用,这段空间可以存储各类函数需要用到信息,如局部变量,参数等等,当函数返回时,该空间会销毁。
那么说到栈,当有函数调用发生,需要开辟一块新的栈空间,这时总得记录栈顶的位置吧,只有知道了栈顶的位置,这样子我们才能抬高栈顶(开辟新空间),相对于栈顶,还有一个概念就是栈底,也可以说是函数的底部位置。对于栈顶和栈底,CPU中使用ESP和EBP这两个寄存器来保存其内容。
ESP指向栈顶
//函数的内部空间
EBP指向栈底
下面来看一段递归程序,用于分析其函数调用的过程
int GetSum(int num)
{
num && (num += GetSum(num-1)); //num为0时不会执行后半部分,递归出口
return num;
}
int main(int argc, char* argv[])
{
printf("%d",GetSum(2));
return 0;
}
下面来画一下函数递归调用的流程图
函数的返回流程图
所以这里最终的打印结果就是3。看完上面的流程分析,那么现在对函数的栈结构应该有一定的宏观上的了解了。
下面就可以说说细节了,我们就来分析一下上述程序的汇编代码,看懂汇编后,你就能知道程序是如何转移的,又是如何返回的,又是如何获取参数的...
先来看Main函数中的调用
12: printf("%d",GetSum(2));
00401158 6A 02 push 2 //压入参数
0040115A E8 BF FE FF FF call @ILT+25(sub_4010A0) (0040101e) //调用GetSum
0040115F 83 C4 04 add esp,4
对于call指令而言,其实他的工作就是把下一行的汇编指令地址给压入栈中,也就是说我们可以如下等价替换
12: printf("%d",GetSum(2));
00401158 6A 02 push 2
push 0040115F //压入下一行的地址
mov EIP,0040101e //修改EIP使其到跳转到函数(模拟,真实该指令无效)
0040115F 83 C4 04 add esp,4