c语言函数调用栈
-
程序的执行过程可以看作的连续的函数调用
-
函数调用过程通常使用堆栈实现
-
编译器使用堆栈传递函数参数,保存返回i地址,临时保存寄存器的原有值以备恢复以及存储本地局部变量
1.寄存器的分配
1.寄存器是处理器加工数据或运行程序的重要载体,用于存放程序执行中用到的数据和指令。
2.在x86处理器中,EIP(Instruction Pointer)是指令寄存器
-
图所示的前6个寄存器均可作为通用寄存器使用
-
某些指令可能以固定的寄存器作为源寄存器或目的寄存器,如一些特殊的算术操作指令imull/mull/cltd/idivl/divl要求一个参数必须在%eax中,其运算结果存放在%edx(higher 32-bit)和%eax (lower32-bit)中;又如函数返回值通常保存在%eax中
-
寄存器%eax、%ebx、%ecx和%edx,各自可作为两个独立的16位寄存器使用,而低16位寄存器还可继续分为两个独立的8位寄存器使用。编译器会根据操作数大小选择合适的寄存器来生成汇编代码。在汇编语言层面,这组通用寄存器以%e(AT&T语法)或直接以e(Intel语法)开头来引用,例如mov $5, %eax或mov eax, 5表示将立即数5赋值给寄存器%eax。
-
EIP(Instruction Pointer)是指令寄存器 指向处理器下条等待执行的指令地址(代码段内的偏移量),每次执行完相应汇编指令EIP值就会增加 ESP(Stack Pointer)是堆栈指针寄存器 存放执行函数对应栈帧的栈顶地址(也是系统栈的顶部),且始终指向栈顶 EBP(Base Pointer)是栈帧基址指针寄存器 存放执行函数对应栈帧的栈底地址,用于C运行库访问栈中的局部变量和参数 -
注意,EIP是个特殊寄存器,不能像访问通用寄存器那样访问它,即找不到可用来寻址EIP并对其进行读写的操作码(OpCode)。EIP可被jmp、call和ret等指令隐含地改变(事实上它一直都在改变)。
-
不同架构的CPU,寄存器名称被添加不同前缀以指示寄存器的大小。
x86架构用字母“e(extended)”作名称前缀,指示寄存器大小为32位
x86_64架构用字母“r”作名称前缀,指示各寄存器大小为64位。
- 编译器在将C程序编译成汇编程序时,应遵循ABI所规定的寄存器功能定义。同样地,编写汇编程序时也应遵循,否则所编写的汇编程序可能无法与C程序协同工作
2.栈帧结构
-
函数调用是嵌套的。
-
栈帧:每个未完成运行的函数占用一个独立的连续区域,好处是使时的递归成为了可能
-
栈帧是堆栈的逻辑片段,当调用函数时逻辑栈帧被压入堆栈, 当函数返回时逻辑栈帧被从堆栈中弹出。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。
-
EBP为帧基指针, ESP为栈顶指针,并在引用汇编代码时分别记为%ebp和%esp。
-
内存地址从栈底到栈顶递减,压栈就是把esp指针逐渐往低址移动的过程
-
函数调用以值传递时,传入的实参(locMain13)与被调函数内操作的形参(para13)两者存储地址不同,因此被调函数无法直接修改主调函数实参值(对形参的操作相当于修改实参的副本)。为达到修改目的,需要向被调函数传递实参变量的指针(即变量的地址)
-
注意,局部变量的布局依赖于编译器实现等因素。因此,当StackFrameContent函数中删除打印语句时,变量locVar3、locVar2和locVar1可能按照从高到低的顺序依次存储!而且,局部变量并不总在栈中,有时出于性能(速度)考虑会存放在寄存器中。数组/结构体型的局部变量通常分配在栈内存中
3.堆栈操作
函数调用具体步骤:
1)主调函数将被调函数所要求的参数,根据相应的函数调用约定,保存在运行栈中。该操作会改变程序的栈指针
x86平台将参数压入调用栈中,而x86——64平台具有16个通用的64位寄存器,故调用函数是前六个参数通常由寄存器传递,其余参数才通过栈传递
2)主调函数将控制权移交给被调函数。函数的返回地址保存在程序栈中(压栈操作隐含在call指令中)
返回地址:待执行的下次指令地址
3)如有必要,被调函数会设置桢基指针,并保存被调函数希望保存不变的寄存器的值
4)被调函数通过改变栈顶指针的值,为自己的局部变量在运行时栈中分配内存空间,并从桢基指针的位置处向低地址方向存放被调函数的局部变量和临时变量
5)被调函数执行自己任务,此时可能需要访问由主调函数传入的参数。若被调函数返回一个值,该值通常保存在一个指定寄存器中(如EAX)。
6)一旦被调函数完成操作,为该函数局部变量分配的栈空间将被释放
7)恢复步骤3中保存的寄存器值,包含主调函数的帧基指针寄存器。
8)被调函数将控制权交还主调函数(使用ret指令)。根据使用的函数调用约定,该操作也可能从程序栈上清除先前传入的参数。
9)主调函数再次获得控制权后,可能需要将先前的参数从栈上清除。在这种情况下,对栈的修改需要将帧基指针值恢复到步骤1之前的值。
压栈(push) | 栈顶指针ESP减小4个字节;以字节为单位将寄存器数据(四字节,不足补零)压入堆栈,从高到底按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4指向的地址单元 |
---|---|
出栈(pop) | 栈顶指针ESP指向的栈中数据被取回到寄存器;栈顶指针ESP增加4个字节。 |
调用(call) | 当前的指令指针EIP(该指针指向紧接在call指令后的下条指令)压入堆栈,以备返回时能恢复执行下条指令;然后设置EIP指向被调函数代码开始处,以跳转到被调函数的入口地址执行。 |
离开(leave) | 恢复主调函数的栈帧以准备返回。等价于指令序列movl %ebp, %esp(恢复原ESP值,指向被调函数栈帧开始处)和popl %ebp(恢复原ebp的值,即主调函数帧基指针 |
返回(ret) | 与call指令配合,用于从函数或过程返回。从栈顶弹出返回地址(之前call指令保存的下条指令地址)到EIP寄存器中,程序转到该地址处继续执行(此时ESP指向进入函数时的第一个参数)。若带立即数,ESP再加立即数(丢弃一些在执行call前入栈的参数)。使用该指令前,应使当前栈顶指针所指向位置的内容正好是先前call指令保存的返回地址。 |
第一个参数)。若带立即数,ESP再加立即数(丢弃一些在执行call前入栈的参数)。使用该指令前,应使当前栈顶指针所指向位置的内容正好是先前call指令保存的返回地址。 |