栈 Stack
堆栈指针(stack pointer),帧指针(frame pointer),堆栈帧(stack frame),活动记录(active record),调用惯例(call convention)等相关概念。
术语 Terminology
- 堆栈指针(stack pointer)
指CPU中的一个寄存器,该寄存器始终指向栈的顶部,同时也指向当前函数活动记录的顶部。在X86架构下,该寄存器是esp,在MIPS架构下,该寄存器是sp,即MIPS的32个通用寄存器中的29号寄存器(从0开始编号)。
同时需要指出的是,对堆栈指针的操作,在属于复杂指令集(CISC)的X86下,除了使用加减指令(ADD,SUB)之外,还有专门的指令POP,PUSH,在属于精简指令集的计算机(RISC)的MIPS下,则只能使用通用的加减指令(ADD,SUB)。
- 帧指针(frame pointer)
帧指针是存储函数活动记录中固定地址的寄存器。其实就是CPU中的一个寄存器,该寄存器始终指向函数活动记录的一个固定位置,不会随这个函数的执行而变化(注意,并不是指向活动记录的最底部位置)。在X86架构下,该寄存器是ebp,在MIPS架构下,该寄存器是s8,即MIPS的32个通用寄存器中的30号寄存器(从0开始编号)。
- 堆栈帧(stack frame)/活动记录(active record)
指由栈保存的一个函数调用所需维护的信息。一般包括:函数返回地址和参数,临时变量和局部变量,某些寄存器的值。
- 调用惯例(call convention)
函数调用方和被调用方对于函数如何调用的一个约定。一般包括:函数参数的传递顺序和方式,栈的维护方式,名字修饰策略等。
调用惯例 ―― cdecl
cdecl这个调用惯例是C语言默认的调用惯例。首先,对C语言中常见的几种调用惯例进行一个总结性的比较。然后就其中一些特别的地方做一个说明。
调用惯例 | 出栈方 | 参数传递 | 名字修饰 |
cdecl | 函数调用方 | 从右向左入栈 | 下划线+函数名 |
stdcall | 函数本身 | 从右向左入栈 | 下划线+函数名+@+参数字节数 |
fastcall | 函数本身 | 寄存器+从右向左入栈 | @+函数名+@参数字节数 |
pascall | 函数本身 | 从左向右入栈 | 较复杂 |
- cdecl调用惯例的参数传递为从右向左,参数入栈。这是C语言函数实现可变长参数的基础。最常见的可变长参数函数是printf();
- cdecl调用惯例的出栈方是函数调用方,这是和其他3种调用惯例不一样的地方,它们的出栈方都是函数本身。那么,调用惯例中的出栈方具体是指什么呢?
函数调用实例
下图为cdecl调用惯例的一般情况示意图。
image/cdel_call_convention.png
下面分析cdecl调用惯例中,调用函数和被调用函数分别负责活动记录(堆帧栈)中的什么操作呢?首先是被调用函数,在函数的入口,需要做以下操作:
-
需要将返回地址入栈,例如MIPS下的
sw \(ra,28(\)sp)
一句,就是将函数的返回地址入栈。对于x86架构,由于call命令会自动将返回地址入栈,所以在函数入口处没有返回地址入栈的指令; -
需要将调用函数的帧指针入栈,然后设置本身自己函数的帧指针。在x86下,是
push %ebp;mov %esp,%ebp
两句,而在MIPS下,是sw \(s8,24(\)sp);move \(s8,\)sp;
两句。这里也可以看出x86的拥有专门入栈指令的特点push的特点; - 调整堆栈指针的大小,升栈到函数需要的堆栈大小。
上面是被调用函数需要负责的事情,那么调用函数需要负责什么呢?主要有一下几点:
- 准备好函数调用参数;
- 调用指令call跳转到被调用函数。
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)的知识,请参考下一节。