一. 代码编程极简进化史
从纸带上的机器码,到汇编语言算是比较自然而然的变化。因为汇编语言每一条指令即对应着一条机器指令码。汇编中的jump和branch为代码模块化组织提供了最原始的形式。
从汇编语言到C语言算是又一个比较自然的进一步简化,因为C语言是过程式。而相较于汇编的jump/branch,C语言的函数调用(function call)算是带领代码模块化组织形式走出了石器时代。
后续的面向对象语言,也是基于function call, 只是编译器会自动生成constructor/destructor。但正如函数调用的概念,为面向对象提供了基础一样,基于function call和function generate的面向对象语言,也提供了很新的代码组织形式和新的想象,带领编程走进了现代化。
二. 函数栈
程序 = 算法 + 数据。
算法的承载为一条条指令;数据的承载即是寄存器和内存,为指令操作的直接对象。
相较于汇编语言只使用寄存器即可执行运算,并跳来跳去。C语言并不以寄存器为直接操作对象,而是内存中的一个一个变量。
相较于汇编语言jump/branch时,依赖于LR寄存器记录跳转指令下一条指令的地址,然后才能跳回来,C语言的function call则依赖于编译器根据ABI中规定的参数寄存器和返回值寄存器,以及栈来存储函数临时变量和函数的返回地址;
每个函数占用的栈区间,我们成为函数的栈帧,栈帧的内容为:
1. 函数用到的临时变量;
2. 函数的返回地址;
函数调用一层层进行下去,栈里的内容即为一层层函数调用的栈帧,即为函数栈。
下面举例说明。
int __add(int a, int b)
{
return a + b;
}
int add(int a, int b)
{
return __add(a, b);
}
int main(void) {
volatile int i = 0;
int v = add(1, 3);
/* Loop forever */
for (;;) {
i++;
}
}
函数add,基于PowerPC VLE的汇编代码如下:
其中,mflr r0把存有函数返回地址的lr寄存器的值存入r0,然后se_stw r0,36(rsp)把返回地址写入栈;函数返回时,se_lwz r0,36(rsp)从栈中读取函数返回地址,然后se_mtlr r0把返回地址写入lr,然后blr返回到lr指定的地址处执行,即上一层函数调用本函数的下一条指令处执行。
Java里面Exception的printStackTrace()打印出的一层层函数调用栈是函数栈最明显的例子。Linux Kernel panic时也会打印函数调用栈,以及栈帧的内容,可以观察临时变量和返回地址。
三. 进程栈
进程栈存储进程的调用关系吗?不是,进程没有调用关系。
进程栈存储进程的父子关系吗?不是,父子进程之间也没有调用关系。
进程栈存储进程的抢占关系,高优先级进程抢占低优先级进程时,则需要把低优先级进程的Context存储起来,以便后续恢复。另一种情况是,中断打断当前进程时,也需要存储当前进程的Context。
进程上下文存储的内容:
1.进程的函数调用栈;
2.进程的状态,Task Control Block(TCB),包括进程使用的寄存器的当前值;
每个进程都会分配专属的栈空间,以存储该进程的上下文。需要恢复时,即从该进程的上下文中恢复各个寄存器的值,接着被抢占时的位置继续执行。
四. 对比
由此可以看出,进程栈是指进程的栈,而非函数栈那样,存放一层层函数调用的栈帧。多个进程的栈是离散的,分开的,而非像函数栈那样连续存放。