栈 Stack

堆栈指针(stack pointer),帧指针(frame pointer),堆栈帧(stack frame),活动记录(active record),调用惯例(call convention)等相关概念。

术语 Terminology

指CPU中的一个寄存器,该寄存器始终指向栈的顶部,同时也指向当前函数活动记录的顶部。在X86架构下,该寄存器是esp,在MIPS架构下,该寄存器是sp,即MIPS的32个通用寄存器中的29号寄存器(从0开始编号)。

同时需要指出的是,对堆栈指针的操作,在属于复杂指令集(CISC)的X86下,除了使用加减指令(ADD,SUB)之外,还有专门的指令POP,PUSH,在属于精简指令集的计算机(RISC)的MIPS下,则只能使用通用的加减指令(ADD,SUB)。

帧指针是存储函数活动记录中固定地址的寄存器。其实就是CPU中的一个寄存器,该寄存器始终指向函数活动记录的一个固定位置,不会随这个函数的执行而变化(注意,并不是指向活动记录的最底部位置)。在X86架构下,该寄存器是ebp,在MIPS架构下,该寄存器是s8,即MIPS的32个通用寄存器中的30号寄存器(从0开始编号)。

指由栈保存的一个函数调用所需维护的信息。一般包括:函数返回地址和参数,临时变量和局部变量,某些寄存器的值。

函数调用方和被调用方对于函数如何调用的一个约定。一般包括:函数参数的传递顺序和方式,栈的维护方式,名字修饰策略等。

调用惯例 ―― cdecl

cdecl这个调用惯例是C语言默认的调用惯例。首先,对C语言中常见的几种调用惯例进行一个总结性的比较。然后就其中一些特别的地方做一个说明。

调用惯例 出栈方 参数传递 名字修饰
cdecl 函数调用方 从右向左入栈 下划线+函数名
stdcall 函数本身 从右向左入栈 下划线+函数名+@+参数字节数
fastcall 函数本身 寄存器+从右向左入栈 @+函数名+@参数字节数
pascall 函数本身 从左向右入栈 较复杂

函数调用实例

下图为cdecl调用惯例的一般情况示意图。

image/cdel_call_convention.png

下面分析cdecl调用惯例中,调用函数和被调用函数分别负责活动记录(堆帧栈)中的什么操作呢?首先是被调用函数,在函数的入口,需要做以下操作:

上面是被调用函数需要负责的事情,那么调用函数需要负责什么呢?主要有一下几点:

X86架构下的函数调用

这个调用中没有调整堆栈指针的操作,因为不需要用到额外的堆栈空间。所以,只有上面提到的第一点和第二点。

push   %ebp
mov    %esp,%ebp
.....
add    %edx,%eax
pop    %ebp
ret   

MIPS架构下的函数调用

在子程序的入口,sp会被升到该子程序需要用到的最大堆栈的位置。在子程序中,堆栈的升降的汇编代码一般都大同小异,下面将romStart()函数开头和结尾的堆栈升降提取出来,代码如下所示:

addiu	$sp,$sp,-32
sw	$ra,28($sp)
sw	$s8,24($sp)
move	$s8,$sp
……
move	$sp,$s8
lw	$ra,28($sp)
lw	$s8,24($sp)
jr	$ra
addiu	$sp,$sp,32

第一句代码的-32就说明,被调用函数romStart()最多用到的堆栈空间为32bytes。然后就是将返回地址ra和调用函数的堆栈位置(存放在\(s8中)放入堆栈中,然后将被调用函数堆栈栈顶位置放入\)s8中(move \(s8,\)sp)。在函数返回就是一个逆操作,恢复调用函数的堆栈现场。有关$s8(又叫做帧指针fp)的知识,请参考下一节。