控制转移

在进行函数调用时,需要把控制转移被调用函数,然后当被调用函数执行完毕后再回到原来的位置继续执行。

我们已经知道操作系统会把进程的内存空间划分成几个具有不同功能的区域。除了前面提到的堆栈段(也被称为栈)以外,还有一个叫做代码段的区域,它用来存放该进程运行中所需要的机器指令。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代码可能会对应对条机器指令,每条机器指令在内存中都会有一个地址。

results matching ""

    No results matching ""