过程在不同的语言中的表现形式不同,比如function、method、subroutine、handler等。但其底层都有一些共同的特性,假设过程P调用过程Q:
- 传递控制:包括如何开始执行过程代码,以及如何返回到开始的地方
- 传递数据:包括过程需要的参数以及过程的返回值
- 内存管理:如何在过程执行的时候分配内存,以及在返回之后释放内存
运行时栈
![2255ef241f090a9b973883d96f163b16.png](https://img-blog.csdnimg.cn/img_convert/2255ef241f090a9b973883d96f163b16.png)
程序栈其实就是一块内存区域,这个区域内的数据满足先进后出的原则。从栈底到栈顶,地址由高变低。所以新加入栈的以及新开辟的空间的地址都是较小的。有两个特殊寄存器是与栈有关的。寄存器 %ebp
叫做帧指针,保存当前栈帧开始的位置。寄存器 %esp
叫做栈指针,始终指向栈顶。栈帧(stack frame)是指为单个过程分配的那一小部分栈。大多数信息访问都是相对于帧指针访问的。以前经常看到这类代码:movl 8(%ebp), %eax
意思就是将存放在比帧指针地址大8的变量移动到寄存器里。
假设过程 P(调用者)调用了过程 Q(被调用者),则 Q 的参数存放在 P 的栈帧中。调用 Q 时,P 的返回地址被压入栈中,形成 P 的栈帧的末尾。返回地址就是当过程 Q 返回时应该继续执行的地方。Q 的栈帧紧跟着被保存的帧指针副本开始,后面是其他寄存器的值。
栈中会存放局部变量。有下面三个原因:
- 不是所有的变量都能放到寄存器中的,没有那么多寄存器。
- 有些局部变量是数组,或者结构体。
- 有些时候需要对某些变量使用
&
运算符,获得其地址,因此要将其放在栈中。寄存器变量是没有地址的。
栈向低地址方向增长。可以利用指令pushl
将数据存入栈,利用popl
将指令从栈中取出。由于栈指针%esp
始终指向栈顶,所以可以通过减小栈指针的值来分配空间,增加栈指针来释放空间。
注意,栈的示意图中,栈顶位于下方,栈底位于上方,我们可以理解为,由于栈底的内存地址更大,所以它位于上方。
procedure三要素:传递控制
传递控制就是过程 P(调用者)调用了过程 Q(被调用者),语句需要
- 进入Q中执行
- Q执行完毕后,回到P中的特定位置
涉及到的命令有
![26d2065d59022de475e2becf53393755.png](https://img-blog.csdnimg.cn/img_convert/26d2065d59022de475e2becf53393755.png)
这些这些指令在程序OBJDUMP产生的反汇编输出中被称为callq和retq。添加的后缀
q
,只是为了强调这些是x86-64版本的调用和返回,而不是IA32的。
call 指令的效果是
- 将返回地址压入栈中(也就是保存返回地址)。这里的返回地址指的是Q调用的下一条命令的地址,这样当被调用过程返回时,执行从此(call 指令的下一条指令)继续;这个效果通过修改栈指针
%rsp
达到; - 然后跳转到被调用过程的起始处。也就是函数Q的开头。这个效果通过修改栈程序计数器
%rip
达到;
ret & rep 指令是同样的指令,它的效果是:
- 认为栈顶是下一条指令的地址,从栈中弹出返回地址,然后跳转到返回地址的位置;这个效果通过修改栈指针
%rsp
和栈程序计数器%rip
达到。
procedure三要素:传递数据
传递数据主要通过寄存器实现,主要涉及
- 把数据作为参数进行传递
- procedure返回值
x86·64中,可以通过寄存器最多传递6个整型(例如整数和指针)参数。寄存器的使用是有特殊顺序的可以通过64位寄存器适当的部分访问小于64位的参数。例如,如果第一个参数是32位的,那么可以用%edi
来访问它。
![fea012e02eb3664a8de27bc7a9e20f4b.png](https://img-blog.csdnimg.cn/img_convert/fea012e02eb3664a8de27bc7a9e20f4b.png)
![ba4ef2c41af2d09884faea334e3bf854.png](https://img-blog.csdnimg.cn/img_convert/ba4ef2c41af2d09884faea334e3bf854.png)
注意这些寄存器仅存储整型数字,浮点数有其他的寄存器可以保存。
参数超出6个时,会将超出的参数放在栈上的内存中,宽度被置为8的倍数。
![1e7c9b5e70fdd883f4ee89a3d7665b9e.png](https://img-blog.csdnimg.cn/img_convert/1e7c9b5e70fdd883f4ee89a3d7665b9e.png)
IA32时代,所有的参数都在栈中被传递
一个参数传递的示例:
![333088343f7b0252f892887561da71e8.png](https://img-blog.csdnimg.cn/img_convert/333088343f7b0252f892887561da71e8.png)
procedure三要素:内存分配
到了这里我们就可以明确一下,什么是栈帧了。在很多编程语言中,你都能听说方法栈或者函数栈的说法,其中常常会提到栈帧。那么栈帧是什么呢?
我们考虑一点:
int add(int a, int b){
return a + b;
}
这个函数是否需要用到额外的空间呢,答案是不需要。a、b两个参数和返回值都由寄存器保存,这个函数内没有其他的空间需要了。
对于那些需要额外的地址来保存数据或其他内容的过程或函数,用来保存这个过程或函数的栈上地址区间,被称作栈帧(frame)。
通常在以下情况,我们需要使用到栈上空间:
- 寄存器不足够存放所有的本地数据。
- 对一个局部变量使用地址运算符
&
,因此必须能够为它产生一个地址。 - 某些局部变量是数组或结构体,因此必须能够通过数组或结构体引用被访问到。在描述数组和结构体分配时,我们会讨论这个问题。
寄存器共享
寄存器是在过程调用中唯一能被所有过程共享的资源。因此我们必须保证被调用者不会覆盖某个调用者稍后会使用的寄存器的值。根据惯例,寄存器%rbx
、%rbp
、%r12~%r15
被划分为被调用者保存寄存器,Q 在覆盖这些寄存器的值之前,必须将其压入栈中,然后在返回前恢复他们;其他寄存器除了%rsp
剩下的所有的寄存器,被划分为调用者保存寄存器。当过程 P 调用过程 Q 时,Q 可以覆盖这些寄存器的数据,而不会破坏 P 所需的数据。看下面的例子:
int P(int x)
{
int y = x * x; //变量 y 是在调用前计算的
int z = Q(y);
return y + z; //要保证变量 y 在 Q 返回后还能使用。
}
- 基于调用者保存:过程 P 在调用 Q 之前,将 y 的值保存在自己的栈帧中;当 Q 返回时,过程 P 由于自己保存了这个值,就可以从自己的栈中取出来。
- 基于被调用者保存:过程 Q 将值 y 保存在被调用者保存寄存器。如果过程 Q 和其他任何 Q 调用的过程,想使用保存 y 值的被调用者保护寄存器,它必须将这个寄存器的值存放到栈帧中,然后在返回前恢复 y 的值。
这两种方案都是可行的。
内存分配实例
考虑下面给出的C语言代码。函数 caller 中包括一个对函数 swap_add 的调用。
long swap_add(long *xp, long *yp)
{
long x = *xp;
long y = *yp;
*xp = y;
*yp = x;
return x + y;
}
long caller()
{
long arg1 = 534;
long arg2 = 1057;//局部变量,以及作为参数
long sum = swap_add(&arg1, &arg2);
long diff = arg1 - arg2;
return sum * diff;
}
函数swap_add交换两个指针指向的值,并返回他们的和。caller创建传给作为swap_add的参数的指针。他们编译的汇编代码如下:
_caller:
subq $16, %rsp # 分配16字节的栈帧
movq $534, (%rsp) # arg1 存534
movq $1057, 8(%rsp) # arg2 存1057
leaq 8(%rsp), %rsi # 第二个参数(寄存器)存arg2地址
movq %rsp, %rdi # 第一个参数(寄存器)存arg1地址
callq _swap_add # 调用
movq (%rsp), %rdx # 取得arg1
subq 8(%rsp), %rdx # arg1 - arg2;
imulq %rdx, %rax # sum * diff 并准备返回
addq $16, %rsp # 释放栈帧
retq
另外一个例子
![65a90e15929991f4df504d74a9c52e8d.png](https://img-blog.csdnimg.cn/img_convert/65a90e15929991f4df504d74a9c52e8d.png)
第3~6行分配形式和结果见下图,这是在栈上分配的局部变量信息:
![f580211babad696ce903b9d8eecf299d.png](https://img-blog.csdnimg.cn/img_convert/f580211babad696ce903b9d8eecf299d.png)
当程序返回call_proc时,代码会取出4个局部变量(第17~20行),并执行最终的计算。在程序结束前,把栈指针加32,释放这个栈帧。
递归实例
![e9154983bee97d82692762616ec2e8d5.png](https://img-blog.csdnimg.cn/img_convert/e9154983bee97d82692762616ec2e8d5.png)
表明,函数调用自身,也是可以为每个不同的调用在栈上保存私有空间的。