控制转移
在进行函数调用时,需要把
控制转移
到被调用函数
,然后当被调用函数执行完毕后再回到原来的位置继续执行。
我们已经知道操作系统会把进程的内存空间
划分成几个具有不同功能的区域。除了前面提到的堆栈段
(也被称为栈)以外,还有一个叫做代码段
的区域,它用来存放该进程运行中所需要的机器指令。CPU在读取指令的过程中,它需要知道当前应该执行哪一条指令。因此CPU提供了一个叫做程序计数器
(PC)的寄存器,用它来存放CPU当前要执行的指令在代码段
中的内存地址。
由于CPU在执行时都是从PC
中获取指令地址,所以当发生函数调用时可以把PC
修改成被调用者
指令的起始地址
,这样一来CPU就会从被调用者
开始执行。当被调用者
执行完之后,为了让CPU从原来的位置继续执行。可以再次修改PC
为在发生调用时的下一条指令
的地址,这个地址被称为函数的返回地址
。
考虑有如下C语言代码:
void Q() {
printf("this is Q.");
return;
}
void P() {
printf("readying to call Q.");
Q();
return;
}
我们假设代码的行号
就是指令地址
,在最开始时PC为7,也就是函数P
的起始指令地址。CPU往下执行后发现是一个函数调用,此时把PC修改为被调用函数Q
的起始指令地址2。直至Q
执行完毕之后,最后再把PC修改为P
中调用Q
时的下一条指令地址9,整个调用过程结束。
现在有一个问题就是对于函数返回地址
的保存,考虑当有大量函数嵌套调用时,每发生一次函数调用都会产生一个返回地址,并且这些返回地址还要和每次调用进行关联。为了满足这个需求我们需要使用栈
来存储函数的返回地址
,当每次发生调用时就将返回地址
压入到栈
中,函数执行完之后再把从把它从栈中弹出到PC
中。
下面是一个C语言嵌套函数调用的例子,我们分别对它的调用和返回过程进行说明:
void Q() {
printf("this is Q.\n");
return;
}
void P() {
printf("readying to call Q.\n");
Q();
return;
}
void main() {
printf("readying to call P.\n");
P();
return;
}
调用过程:
还是假设代码的行号
为每条指令的地址,最开始PC为13,也就是main
函数的第一行代码,程序继续执行到14,发现这里调用了函数P
,因此把PC设为P
的起始指令地址7。随后将调用P
处的下一条指令地址15压入栈中。程序继续执行到8,发现这里调用了函数Q
,同样把PC设为Q
的起始指令地址2,最后将调用Q
处的下一条指令地址9压入栈中,此时栈中存储了两个返回地址
分别是9和15。
栈变化过程:
返回过程:
在函数Q
执行完之后,函数开始返回,返回时会把栈中之前保存的返回地址
弹出到PC中,此时栈顶是指令地址9,将它从栈中弹出到PC中,函数成功的返回到P
中。再待P
执行完成之后,继续把栈顶的指令地址15弹出到PC中,到最后函数返回到了main
中,栈也恢复成了发生调用前的样子。
栈变化过程:
整套过程可以推广至任意层函数调用以及递归调用,栈完美的保存了函数的返回地址。这里为了举例方便把一行C代码当成一条指令,实际上这里的指令是经过编译后的机器指令
,那么一行C代码可能会对应对条机器指令,每条机器指令在内存中都会有一个地址。