栈与栈帧
在操作系统中,栈(Stack)是一种用于管理函数调用和局部变量的数据结构。它采用后进先出(Last-In-First-Out,LIFO)的原则,类似于我们日常生活中的堆叠物品的方式。栈通常是在内存中分配一块连续的空间,用于存储函数调用时的临时数据。
栈帧(Stack Frame)是栈中的一个重要概念,也被称为活动记录(Activation Record)或者帧(Frame)。每当一个函数被调用时,就会在栈上创建一个新的栈帧,用于存储该函数的局部变量、函数参数、返回地址等信息。
栈空间由高地址往低地址增长。
当一个函数被调用时,操作系统会为该函数分配一块新的栈帧,并将其推入栈顶。栈帧包含以下几个重要的部分:
- 局部变量区域:用于存储函数内部定义的局部变量。这些变量在函数执行期间被使用,并在函数返回后自动释放。
- 函数参数:用于存储函数调用时传递给函数的参数。这些参数在函数内部可以被访问和使用。
- 返回地址:指向函数调用者的下一条指令地址,用于在函数执行完毕后返回到正确的位置继续执行。
- 上一个栈帧指针:指向调用当前函数的上一个栈帧的地址,用于函数返回后恢复调用者的上下文。
当函数执行完毕后,当前的栈帧会被销毁,栈指针会回退到上一个栈帧,继续执行调用者函数的剩余部分。
栈的使用使得函数调用变得简单和高效,因为每个函数都有自己的独立空间来存储局部变量和参数,避免了相互之间的干扰。栈帧的概念则提供了一种有效的方式来组织和管理函数调用过程中的数据。
寄存器
常见寄存器如下,一般32位寄存器以%e开头如%eax,64位寄存器以%r开头,如%rax。
- sp 栈顶指针
- bp 栈帧基指针
- di
- si
- ax 通常存放函数返回值
- bx
- cx
- ip 要执行指令的地址
除了这些之外还有很多通用寄存器以及特定用途寄存器,就不在这里一一列出了。
函数调用约定
函数调用约定主要约定的以下三点:
- 函数参数是如何传递的
- 堆栈清理由谁操作的
- 返回值是如何返回的
返回值:默认以ax寄存器保存返回值
堆栈清理与参数传递:
根据微软的文档,x86-32bit平台不同的函数调用规范如下:
关键字 | 堆栈清理 | 参数传递 |
调用方 | 在堆栈上按相反顺序推送参数(从右到左) | |
不适用 | 按顺序将参数加载到 CLR 表达式堆栈上(从左到右)。 | |
被调用方 | 在堆栈上按相反顺序推送参数(从右到左) | |
被调用方 | 存储在寄存器中,然后在堆栈上推送 | |
被调用方 | 在堆栈上推送;存储在 ECX 中的 this 指针 | |
被调用方 | 存储在寄存器中,然后按相反顺序在堆栈上推送(从右到左) |
其中__cdecl为C/C++的默认调用约定,__stdcall为win32特有。
x64 应用程序 使用类似 fastcall 的 x64调用约定。 系统在调用堆栈上分配空间作为影子存储,供被调用方保存这些寄存器。详情参见x64调用约定。
示例
C |
int add(int a, int b){ return a+b; } int fun(){ int a=1; int b=2; int sum = add(a,b); sum+=1; return sum; } |
ASM from Compiler Expoler linux-x64-gcc |
add(int, int): pushq %rbp movq %rsp, %rbp movl %edi, -4(%rbp) //从寄存器接收参数 movl %esi, -8(%rbp) //从寄存器接收参数 movl -4(%rbp), %edx movl -8(%rbp), %eax addl %edx, %eax popq %rbp ret fun(): pushq %rbp //保存上个函数栈帧的栈帧基指针 movq %rsp, %rbp subq $16, %rsp movl $1, -4(%rbp) //局部变量 movl $2, -8(%rbp) //局部变量 movl -8(%rbp), %edx movl -4(%rbp), %eax movl %edx, %esi //传递参数到寄存器 movl %eax, %edi //传递参数到寄存器 call add(int, int) // 将函数返回地址保存并跳转 movl %eax, -12(%rbp) addl $1, -12(%rbp) movl -12(%rbp), %eax //将函数返回值保存在eax中 leave //相当于 movq %rbp, %rsp 和 popq %rbp;恢复上个函数的rpb和rsp指针 ret //相当于pop %rip; 恢复到调用者函数的剩余部分继续执行
|