以机器指令的角度看过程调用

最近真是闲得蛋疼啊,看完了第二遍《Python源码剖析》之后开始看计算机组成原理。

回顾一下,自从接触了Python,扯了一大堆东西。从windows转移到Linux(现在用的是OS X—苹果系统)。从VS2012转移到vim,再到现在得emacs。从Sql Server到Mysql。从web开发,桌面应用开发,到各种服务和api的开发。

从米帮的基础框架sputnik开始扯,到看tornado源码,然后学HTTP协议和Linux IO多路复用模型,然后到C语言基础,然后到各种算法和数据结构,然后到编译原理,然后到现在的计算机组成原理。好大的一个栈。不知道什么时候能够全部出栈,不知道接下去是不是还要继续把状态机,图灵机什么的也压入栈。不得不感慨一句:跳出.net,外面又是一个十分广阔的世界。

好吧,不扯远了。

1
2
3
4
5
6
7
int leaf_example(int g, int h, int i, int j)
{
int f;

f = (g + h) - (i + j);
return f;
}
leaf_example:
# 保存现场
addi    $sp, $sp, -12        # 调整栈指针
sw    $t1, 8 ($sp)        # 保存$t1寄存器的内容到当前栈帧的栈底
sw    $t0, 4 ($sp)        # 保存$t0寄存器的内容到$t1的上面
sw    $s0, 0 ($sp)        # 保存$s0寄存器的内容到栈顶

# 函数体执行
add     $t0, $a0, $a1        # 计算g + h并将结果保存到$t0寄存器
add     $t1, $a2, $a3        # 计算i + j并将结果保存到$t1寄存器
sub    $s0, $t0, $t1        # 计算( $t0 - $t1 )并将结果保存再$s0
add     $v0, $s0, $zero        # 记录返回值

# 恢复现场
lw    $s0, 0 ($sp)        # 将栈顶数据加载到$s0寄存器中
lw    $t0, 4 ($sp)        # 将接下来的数据加载到$t0寄存器中
lw    $t1, 8 ($sp)        # 将栈底的数据加载到$t1寄存器中
addi    $sp, $sp, 12        # 将栈指针向上移动3个单位

# 控制转移
jr    $ra                # 将控制权转移给调用者

总得来说一个函数调用过程需要进行这几个步骤。首先是当前CPU现场得保存,然后执行函数体,最后恢复现场将控制权转移给调用者。

因为CPU中寄存器的数量有限,所以在寄存器数量不够的情况下,需要将寄存器中的数据转移到内存中(PS:我还知道转移的指令是数据传输指令:存字和取字指令)。

当然这里过程调用的现场保存主要还是为了方便恢复现场,只有恢复了现场,在被调用的过程执行完之后,当前过程才能继续正确执行。

被转移出去的数据是以栈的形式存放在内存中的,$sp就是这个栈的栈顶指针。

另外这几个寄存器也是有特定含义的,$sp之前说过了,是当前运行时栈的栈指针,$t0~$t9这10个寄存器是临时寄存器,用来存放临时变量,$s0~$s7保留寄存器,一旦使用由被调用者保存和恢复。$v0~$v1用来存放过程调用中的返回值,$a0~$a3用来存放过程调用中的参数信息。$ra是是用来存放返回地址的,还有一个抽象的东西,程序计数器,存放CPU要执行的下一条指令,其实程序计数器并不是真实存在的一个寄存器,每个体系的好像都不一样,在x86体系中是CS:IP这两个寄存器的组合。