栈介绍
基本栈介绍
栈是一种后进先出的数据结构,其主要的操作主要有压栈(push)与出栈(pop)两种操作。
程序操作栈时,是操作当前sp指向的位置
- 压栈时,sp由高地址向低地址移动,后将数据存入sp指向的位置
- 出栈时,先将sp指向的数据弹出,然后sp由低地址向高地址移动
函数调用栈
C语言函数调用栈
程序的执行可以看作是连续的函数调用。当一个函数执行完毕时,程序要回到调用指令的下一条指令处继续执行。而这个函数调用的过程常使用堆栈实现,每个用户态进程对应一个调用栈结构。编译器使用堆栈传递函数参数、保存返回地址、临时保存寄存器原有值(即函数调用的上下文)以备恢复以及存储本地局部变量。
1 寄存器分配
寄存器是处理器加工数据或运行程序的重要载体,用于存放程序执行中用到的数据和指令
Intel 32位体系结构(IA32)处理器包含了8个四字节寄存器。对于寄存器%eax、%ebx、%ecx、%edx,各自又可作为两个独立的16位寄存器使用,而其中,低16位寄存器还可以继续分为两个独立的8位寄存器使用
在x86处理器中:
- EIP是指令寄存器,指向处理器下条等待执行的指令地址(代码段内的偏移量),每次执行完相应汇编指令EIP值就会增加
- ESP是堆栈指针寄存器,存放执行函数对应栈帧的栈顶地址(也是系统栈的顶部),且始终指向栈顶
- EBP是栈帧基址指针寄存器,存放执行函数对应栈帧的栈底地址,用于C运行库访问栈中的局部变量和参数
[!info] 栈帧的概念
栈帧是用于实现函数调用的数据结构,又称过程活动记录。每个未运行完的函数对应一个栈帧,保存函数的返回地址、局部变量、执行环境信息及函数参数
所以通俗一点讲,栈帧就是程序调用函数时,栈上占用的空间
[!caution] EIP特殊寄存器
EIP作为一个特殊寄存器,不能像访问其它通用寄存器一样访问它,找不到可用来寻址EIP并对其进行读写的操作码,但是EIP可以被jmp、call和ret等指令隐含地改变
[!info] 栈帧指针寄存器
为了访问函数的局部变量,必须能定位每个变量。局部变量的位置在进入函数时就已经确定了,理论上可以通过ESP加偏移量的方式引用,但是ESP会在函数执行期间随着压栈和出栈而改变。用ESP加偏移量来访问变量还是有些不太方便
因此,许多编译器使用栈帧指针寄存器FP来记录栈帧基地址。局部变量和函数参数都可以通过帧指针引用,因为它们到FP的 距离不会受到压栈和出栈的影响,也称帧指针为局部基指针
而在Intel CPU中,寄存器BP用作帧指针,以FP地址为基准,函数参数的偏移量是正值,而局部变量的偏移量是负值
2 寄存器使用约定
程序寄存器组是唯一能被所有函数共享的资源。虽然某一时刻只有一个函数在执行,但需保证当某个函数调用其他函数时,被调函数不会修改或覆盖主调函数稍后会使用到的寄存器值
寄存器%eax、%edx和%ecx为主调函数保存寄存器,当函数调用时,若主调函数希望保持这些寄存器的值,则必须在调用前显式地将其保存在栈中;被调函数可以覆盖这些寄存器,而不会破坏主调函数所需的数据。寄存器%ebx、%esi和%edi为被调函数保存寄存器,即被调函数在覆盖这些寄存器的值时,必须先将寄存器原值压入栈中保存起来,并在函数返回前从栈中恢复其原值,因为主调函数可能也在使用这些寄存器。此外,被调函数必须保持寄存器%ebp和%esp,并在函数返回后将其恢复到调用前的值,亦即必须恢复主调函数的栈帧(堆栈平衡)
3 栈帧结构
函数调用经常是嵌套的,在同一时刻,堆栈中会有多个函数的信息。每个未完成运行的函数占用一个独立的连续区域,称作栈帧。栈帧是堆栈的逻辑片段,当调用函数时逻辑栈帧被压入堆栈, 当函数返回时逻辑栈帧被从堆栈中弹出。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。
栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行。
故EBP为帧基指针, ESP为栈顶指针
函数入栈时的入栈顺序:
实参N1 -> 主调函数返回地址 -> 主调函数帧基指针EBP -> 被调函数局部变量1N
函数调用以值传递时,传入的实参(locMain13)与被调函数内操作的形参(para13)两者存储地址不同,因此被调函数无法直接修改主调函数实参值(对形参的操作相当于修改实参的副本)。为达到修改目的,需要向被调函数传递实参变量的指针(即变量的地址)
4 函数调用约定
创建一个栈帧的最重要步骤是主调函数如何向栈中传递函数参数。主调函数必须精确存储这些参数,以便被调函数能够访问到它们。函数通过选择特定的调用约定,来表明其希望以特定方式接收参数。此外,当被调函数完成任务后,调用约定规定先前入栈的参数由主调函数还是被调函数负责清除,以保证程序的栈顶指针完整性。
函数调用约定通常规定如下几方面内容:
- 函数参数的传递顺序和方式
最常见的参数传递方式是通过堆栈传递。主调函数将参数压入栈中,被调函数以相对于帧基指针的正偏移量来访问栈中的参数。对于有多个参数的函数,调用约定需规定主调函数将参数压栈的顺序(从左至右还是从右至左)。某些调用约定允许使用寄存器传参以提高性能。 - 栈的维护方式
主调函数将参数压栈后调用被调函数体,返回时需将被压栈的参数全部弹出,以便将栈恢复到调用前的状态。该清栈过程可由主调函数负责完成,也可由被调函数负责完成。 - 名字修饰(Name-mangling)策略
又称函数名修饰(Decorated Name)规则。编译器在链接时为区分不同函数,对函数名作不同修饰。
若函数之间的调用约定不匹配,可能会产生堆栈异常或链接错误等问题。因此,为了保证程序能正确执行,所有的函数调用均应遵守一致的调用约定。
5 参数传递
- 整型与指针参数,4字节传递
- 浮点数,8字节传递
- 结构体和联合体,为了满足4字节对齐,会以4字节的倍数传递
6 返回值传递
- 若返回值不超过4字节(如int、short、char、指针等类型),通常将其保存在EAX寄存器中,调用方通过读取EAX获取返回值
- 若返回值大于4字节而小于8字节(如long long或_int64类型),则通过EAX+EDX寄存器联合返回,其中EDX保存返回值高4字节,EAX保存返回值低4字节
- 若返回值为浮点类型(如float和double),则通过专用的协处理器浮点数寄存器栈的栈顶返回
- 若返回值为结构体或联合体,则主调函数向被调函数传递一个额外参数,该参数指向将要保存返回值的地址。即函数调用foo(p1, p2)被转化为foo(&p0, p1, p2),以引用型参数形式传回返回值。具体步骤可能为:a.主调函数将显式的实参逆序入栈;b.将接收返回值的结构体变量地址作为隐藏参数入栈(若未定义该接收变量,则在栈上额外开辟空间作为接收返回值的临时变量);c. 被调函数将待返回数据拷贝到隐藏参数所指向的内存地址,并将该地址存入%eax寄存器。因此,在被调函数中完成返回值的赋值工作
- 不要返回指向栈内存的指针,如返回被调函数内局部变量地址(包括局部数组名)。因为函数返回后,其栈帧空间被“释放”,原栈帧内分配的局部变量空间的内容是不稳定和不被保证的


