- 工具:指令+约定+存储(寄存器和栈内存)
- 怎么实现?
基本问题
- 什么时候需要栈?什么时候不需要栈?
怎么实现?
- 只使用寄存器
当只有一个函数P时,且函数P没有调用任何函数,且函数P的参数不超过6个,且函数P的所有局部变量都可以保存在寄存器中; - 使用寄存器加栈;
必须要使用栈,可以方便地函数Q执行返回后,继续执行函数P;
函数调用
假设函数P调用函数Q,然后函数Q执行,返回给函数P,这三个操作涉及以下3种机制:
- 控制权转移
调用函数Q时,将程序计数器设置为函数Q代码的开始地址;
调用结束返回时,将程序计数器设置为调用函数Q指令的下一个指令; - 数据传递
函数P必须提供函数Q执行所需的多个参数;
函数Q必须能传递一个返回值给函数P; - 内存分配和释放
当函数Q开始执行时,函数Q可能需要分配内存用于存储局部变量;
当函数Q执行结束返回给函数P时,需要释放之前分配的内存;
函数的X86-64实现
特殊指令+特殊约定
当函数Q在执行时,函数P被临时挂起;
当函数Q在执行时,只有函数Q需要分配新的内存来用于存储局部变量或者建立对另一个函数的调用;
当函数Q返回时,其创建的任何局部内存都会被释放;
X86-64的栈是从高地址向低地址增长的;
%rsp
指向的是栈顶元素;
使用pushq
和popq
指令来往栈上存储数据和从栈上获取数据;
没有具体初始值的数据,即临时变量所需的内存空间分配是通过将栈指针递减某个合适的量来的;
类似的,这部分空间的释放是将栈指针递增某个合适的量实现的;
什么时候需要在函数栈?
当X86-64的函数需要的内存超过寄存器的内存时,就需要在函数栈上分配内存了;
一个函数在栈上分配的内存区域,叫做函数的栈帧;
运行时栈run-time stack
大部分函数的栈帧都是大小固定的;
有些函数的帧需要大小可变;
当前正在执行的函数的帧通常位于栈顶;
当函数P调用函数Q时,要将函数Q的返回地址压入函数P的栈帧中;
函数Q的栈帧分配及构成
- 通过拓展当前栈边界来分配函数Q所需的空间;
- 如何利用这份空间,即函数Q的栈帧?
保存寄存器的值;
分配局部变量的存储空间;
设置函数Q需要调用的其他函数的参数;
函数Q栈帧的构成
- 寄存器的值;
- 局部变量的值;
- 函数Q需要调用的其他函数的参数值;
函数P总共可以传递给6个整数值,包括指针值和整数,这些值保存在6个寄存器中(%rdi
, %rsi
, rdx
, %rcx
, %r8
, %r9
);
如果函数Q需的参数值超过6个,则在调用函数Q之前,将这些参数值保存在函数P的栈帧中;
X86-64按需分配函数的栈帧;
加速大概率事件
比如,大部分函数的参数都不超过6个,这些参数值都是通过寄存器来传递的;
当一个函数的所有局部变量都可以使用寄存器保存,且不调用任何函数时,就不需要给这个函数在栈上分配帧,即这个函数的栈帧就不存在;
运行时栈
不是所有的函数都有栈帧;
控制权转移
- 函数P将控制权转移给函数Q
设置程序计数器的值为函数Q的代码的开始地址; - 函数Q将控制权转移给函数P
必须要记录函数P调用函数Q完成后要执行的代码位置;
什么时候发生?
指令call Q
发生时;
怎么记录?
指令call Q
将返回地址A压入函数P的栈中;
怎么清除?
指令ret
将返回值A从函数P的栈帧中弹出;
函数调用指令
-
call Q
:
将返回地址A压入函数P的栈中,设置PC
为函数Q的开始地址;
返回地址A的值为函数P中调用函数Q后的下一个指令的地址;
call
指令的跳转分为两类,一个是直接跳转;另一类是间接跳转;
call
指令的操作数指示着函数Q的开始的地址,即函数Q的开始地址; -
ret
:
从函数P的栈帧中弹出返回地址A,设置PC
的值为返回地址A;