千磨万击还坚劲
任尔东西南北风
函数调用
函数调用就是就是一层一层递进的执行某些功能(动作),在这个过程中,考虑到每个函数执行结束后还要返回,必然会留下很多信息,而计算机也需要存储这些信息。举个例子,A()中调用了B(),那B()执行结束后怎么知道返回A呢,以及A中定义的那些局部变量呢?为了存储这些信息,则计算机给每个即将执行或正在执行的函数分配了一个调用过程中存储信息的空间,就叫栈帧(没参考别人,我瞎说的)。
栈帧
如下图是一个x86机器的栈帧结构:
因为现在大家都用的64位的机器,所以结合下面这个结构理解会更好一点:
High
| ... |
+---------+
+24| Arg 1 |
+---------+
+16| Arg 2 |
+---------+
+ 8| Return |
+---------+
EBP+--> |Saved EBP|
+---------+
- 8| Var 1 |
+---------+
ESP+--> | Var 2 |
+---------+
| ... |
Low
EBP对应64位中寄存器中RBP,同理ESP对应64位中寄存器中RSP。
为什么会讲到这几个寄存器呢?
因为在X64系统中,函数前6个参数的值一开始是存储在寄存器中的,后面就会转存到内存的栈中,然后就能通过RBP存储的地址+偏移量去访问到真的变量和参数。
函数压栈过程
可以把前一个函数叫做“调用者函数”,正在执行的函数叫做“被调函数”:
-
调用者函数把被调函数的参数从右向左依次压入栈;
-
调用者函数使用call指令调用被调函数,并把call指令的下一条指令的地址当成返回地址压入栈中(这个压栈操作隐含在call指令中);
-
在被调函数中,被调函数会先保存调用者函数的栈底地址(push ebp),然后再保存调用者函数的栈顶地址,即:当前被调函数的栈底地址(mov ebp,esp);
-
在被调函数中,从ebp的位置处开始存放被调函数中的局部变量和临时变量,并且这些变量的地址按照定义时的顺序依次减小,即:这些变量的地址是按照栈的延伸方向排列的,先定义的变量先入栈,后定义的变量后入栈;
从操作系统的内存布局分布就可以理解了,栈内存是从大到小增长的,堆内存相反。所以在函数调用里,先调用的函数所在内存地址值肯定会大于后调用函数内存的地址值。