我们下面就来探究一下高级语言中函数的调用和递归等性质是怎样通过系统栈巧妙实现的。请看如下代码:
int func_B(int arg_B1, int arg_B2) int func_A(int arg_A1, int arg_A2) int main(int argc, char **argv, char **envp) |
这段代码经过编译器编译后,各个函数对应的机器指令在代码区中可能是这样分布的,如图4.1.2所示。
根据操作系统的不同、编译器和编译选项的不同,同一文件不同函数的代码在内存代码区中的分布可能相邻,也可能相离甚远;可能先后有序,也可能无序;但它们都在同一个PE文件的代码所映射的一个“节”里。我们可以简单地把它们在内存代码区中的分布位置理解成是散乱无关的。
当CPU在执行调用func_A函数的时候,会从代码区中main函数对应的机器指令的区域跳转到func_A函数对应的机器指令区域,在那里取指并执行;当func_A函数执行完闭,需要返回的时候,又会跳回到main函数对应的指令区域,紧接着调用func_A后面的指令继续执行main函数的代码。在这个过程中,CPU的取指轨迹如图4.1.3所示。
![]() |
图4.1.2 函数代码在代码区 图4.1.3 CPU在代码区中的取指轨迹示意图 中的分布示意图 |
那么CPU是怎么知道要去func_A的代码区取指,在执行完func_A后又是怎么知道跳回到main函数(而不是func_B的代码区)的呢?这些跳转地址我们在C语言中并没有直接说明,CPU是从哪里获得这些函数的调用及返回的信息的呢?
原来,这些代码区中精确的跳转都是在与系统栈巧妙地配合过程中完成的。当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,并把它压入栈中。这个栈帧中的内存空间被它所属的函数独占,正常情况下是不会和别的函数共享的。当函数返回时,系统栈会弹出该函数所对应的栈帧。
![]() |
图4.1.4 系统栈在函数调用时的变化 |
如图4.1.4所示,在函数调用的过程中,伴随的系统栈中的操作如下。
在main函数调用func_A的时候,首先在自己的栈帧中压入函数返回地址,然后为func_A创建新栈帧并压入系统栈。
在func_A调用func_B的时候,同样先在自己的栈帧中压入函数返回地址,然后为func_B创建新栈帧并压入系统栈。
在func_B返回时,func_B的栈帧被弹出系统栈,func_A栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址重新跳到func_A代码区中执行。
在func_A返回时,func_A的栈帧被弹出系统栈,main函数栈帧中的返回地址被“露”在栈顶,此时处理器按照这个返回地址跳到main函数代码区中执行。
题外话:在实际运行中,main函数并不是第一个被调用的函数,程序被装入内存前还有一些其他操作,图4.1.4只是栈在函数调用过程中所起作用的示意图。