前言
1.我们在进行c语言代码编程的时候,常常会把独立的一个功能抽象为函数,利用函数去实现各种的功能,那么,函数是如何调用的?函数的返回值是怎么返回的?参数又是如何传参的?所有这些问题都会跟函数栈帧问题有关。
正片开始:
1.什么是函数栈帧
具体来说,函数栈帧就是在函数调用过程中在程序的调用栈开辟的空间,而这些空间就用来存放:
1.函数参数和函数的返回值
2.临时变量(包括函数的非静态的局部变量一集编译器自动产生的其他临时变量)
3.保存上下文信息(包括函数调用前后需要保持不变的寄存器)
2.理解了函数栈帧后能解决什么问题?
我认为理解这些是一种修炼”内功“的行为,在理解了函数栈帧的创建与销毁后,以下的问题便迎刃而解了
1.局部变量是怎么在栈区中创建的
2.为什么局部变量不出货内容是随机的?
3.函数调用时参数是怎么传递的?传递的顺序是怎么样的?
4函数的实参和形参是怎么分别实例化的
函数栈帧的创建和销毁解析
栈
栈(stack)是现代计算机中最重要的概念之一,每一个程序都用到了栈,没有栈就没有函数,也就没有各种计算机语言,
栈被定义为一种特殊的容器,我们也会在数据结构中学到栈,用户可以将数据压入栈(push),也可以将入栈的数据弹出(pop),但是必须遵守的一个规则是:先入后出,先入栈的数据后出栈(就跟弹匣一样,压入子弹,射出子弹)
在计算机系统中,栈是一个具有以上属性的动态内存区域,程序可以将数据压栈,也可以将数据出栈,压栈会让栈增长,出栈会让栈减小。
在经典的操作系统,栈总是向下增长的(高地址向低地址)
在我们常见的i386或者X86-64下,栈顶是由esp的寄存器进行定位(涉及汇编指令)。
相关寄存器和汇编指令
相关寄存器
eax:通用寄存器,保留临时数据,常用于返回值
ebx:通用寄存器,保留临时数据
ebp:栈底寄存器
esp:栈顶寄存器
eip:指令寄存器,保存当前指令的下一条指令的地址
rbp:是 x86-64 架构中的寄存器,它在汇编语言中通常用于构建函数调用栈帧(stack frame)。
rdi:是 x86-64 架构中的通用寄存器之一。在函数调用中,
rdi
通常用于传递函数的第一个参数。在 x86-64 架构中,函数的前六个参数(整数或指针类型)通常通过rdi
、rsi
、rdx
、rcx
、r8
和r9
这六个通用寄存器传递
相关汇编命令
mov:数据转移指令
push:数据入栈,同时esp栈顶寄存器也要发生改变
pop:数据弹出至指定位置,同时esp栈顶寄存器也要发生改变
sub:减法命令
add:加法命令
call:函数调用,1. 压入返回地址 2. 转入目标函数
jump:通过修改eip,转入目标函数,进行调用
ret:恢复返回地址,压入eip,类似pop eip命令
lea:用于加载有效地址(Load Effective Address)。虽然
lea
的名称中包含 "load" 一词,但它实际上并不执行内存加载操作。相反,lea
计算一个有效地址,并将结果存储到目标操作数中
解析函数栈帧的创建与销毁
1.预备知识
1.在每一次的函数调用中,都需要为本次函数调用开辟空间,也就是函数栈帧的空间。
2.这块空间的维护是用的两个寄存器,esp和ebp,ebp记录栈底地址,esp记录栈顶地址。
如图
在一个函数的调用过程中,会出现以下的过程,这里举一个简单的例子说明
#include <stdio.h>
int Add(int x, int y)
{
int z = 0;
z = x + y;
return z;
}
int main()
{
int a = 3;
int b = 5;
int ret = 0;
ret = Add(a, b);
printf("%d\n", ret);
return 0;
}
以上我举了一个简单的加法函数,我们可以用VS(观测函数栈帧使用越低的版本越容易观察)
我们打开VS调试的反汇编,查看相对应的汇编代码
int main()
{
00007FF7C47518D0 push rbp (首先把rbp寄存器压栈)
00007FF7C47518D2 push rdi (push rdi
表示将rdi
的当前值保存到栈上。这是为了在函数执行期间保护rdi
的值,以便在后续的代码中使用)
00007FF7C47518D3 sub rsp,148h (减去rsp的空间,表示将栈指针rsp
的值减去148h
(也可以写作0x148
或328
),即在栈上分配328
字节的空间)
00007FF7C47518DA lea rbp,[rsp+20h] (将rsp加上20h的空间,也就是分配“32”字节的偏移量
00007FF7C47518DF lea rcx,[__A7141C24_test@c (07FF7C4761008h)] (加载一个地址到“rcx”寄存器)
00007FF7C47518E6 call __CheckForDebuggerJustMyCode (07FF7C4751370h)
int a = 1;(调用函数,这里因为编译器优化的原因,在 这里就i已经知道 a是下面Add函数的参数,所以Add
函数的调用移到初始化a
的位置)
00007FF7C47518EB mov dword ptr [a],1 (变量声明初始化a,将常量1存储到a)
int b = 2;
00007FF7C47518F2 mov dword ptr [b],2 (变量声明初始化b,将常量2存储到b)
int ret = Add(a, b);
00007FF7C47518F9 mov edx,dword ptr [b] (将变量B存进edx寄存器)
00007FF7C47518FC mov ecx,dword ptr [a] (将变量A存进ecx寄存器)
00007FF7C47518FF call Add (07FF7C4751348h) (调用Add函数)
00007FF7C4751904 mov dword ptr [ret],eax
printf("%d", ret);
00007FF7C4751907 mov edx,dword ptr [ret]
00007FF7C475190A lea rcx,[string "%d" (07FF7C4759C24h)]
00007FF7C4751911 call printf (07FF7C4751195h)(call调用printf函数)
return 0;
00007FF7C4751916 xor eax,eax
}