976 字
5 分钟
过程Procedure

[toc]

过程(Procedure)#

过程是指实现某种功能的具体方式。通过对过程的抽象,能够将代码封装起来,提供简明的接口定义。

一个函数必须包括以下的机制:

  • 传递控制:将PC程序计数器,指向新过程的指令地址。并把调用者的指令地址紧随其后地放在后面,用于返回。
  • 传递数据:传递过程需要的参数、寄存器。
  • 分配和释放内存:为调用的过程分配局部变量空间,返回时释放。

栈帧#

对于一个虚拟内存,单个栈是从高内存地址->低内存地址。

栈的增长则是以栈帧为单位的。在进行函数调用时,会以栈帧的形式将函数的局部变量保存下来。

一般,Linux的stack siz e为 8196Bytes

通用的栈帧结构:

image-20240530134753770

栈的增长如下图所示:

%rbp:Base Register,保存栈帧的基址信息

%rsp:stack Register,保存栈顶的地址。

image-20240527152817618

上面是高地址,下面是低地址。

传递控制之Call & Ret#

Call和Ret实现了过程(Procedure)中,控制方面的任务,实现PC程序计数器的跳转。

  • Call指令执行有两个任务:
    • 下一条指令的地址push到栈中,为了能够调用完返回。
    • 跳转到对应的Label指令,继续执行。
    • 在Call调用后,至少会把返回地址Push到栈中
  • Ret指令执行有两个任务:
    • 从stack中pop 在Call时保存的指令地址。
    • 跳转到Pop出的指令地址继续执行。

CSAPP示例:

image-20240530141936771

image-20240530141946891

  1. M1: 调用top(100):

    • callq 0x400545 <top>
      • 前 %rsp = 0x7fffffffefe820
      • 压入返回地址后 %rsp = 0x7fffffffefe818
      • 按理说,返回地址应为8字节,但从表格中%rsp的变化看,这里显示只减了2字节。
      • 表格内容有误。

数据传送#

寄存器和栈帧#

在函数调用的过程中,会存在传递数据的情况。

x86-64中,可以通过寄存器最多传递6个整型数据(整数或指针)。不同大小的数据使用不同类型的寄存器。

如果存在超过6个整型的数据,参数16仍然使用寄存器保存,而参数7n使用栈来传递,将数据保存在内存的栈帧中(紧随寄存器)。

注:栈帧一般使用8字节对齐,因此只有2字节的数据也会使用8Bytes来保存数据。

被调用者保存寄存器(called-saved register)

根据惯例,一般%rbx, %rbp%r12 ~ %r15被划分为被调用者保存寄存器。即:当P调用Q时,Q需要保存这些寄存器的数据,用于恢复P。

当Q需要使用这些寄存器时,必须先把数据压入到栈中。

被调用者保存寄存器,用于保存临时不变的数据,即:只在本次调用范围内保持不变。

调用者保存寄存器(call-saved register)

除了%rsp之外的其他所有寄存器,被称之为调用者保存寄存器。即:P中的数据,在调用Q时如果不想被修改,P需要先压入栈中。Q可以毫无顾虑地直接修改这些寄存器。

调用者保存寄存器,用于保存长期不变的数据,在之后的一系列call中,都不会改变这个数据。

局部变量#

在栈上保存局部变量的情况如下:

  • 当寄存器无法存储所有的局部变量
  • 这个局部变量需要指定地址,如:使用了&取地址符号。
  • 局部变量是结构体或者数组时。

在栈上分配局部变量时不会采用内存对齐的方式。一个8Bytes大小的内存中可能保存多个局部变量。(使用正确的指令和内存偏移位置就能访问)