和函数跋是编译器自动添加的开始和结束汇编代码,其实现与CPU架构和编译器相关。除步骤5代表函数实体外,其它所有操作组成函数调用。
?
? ? ?以下介绍函数调用过程中的主要指令。
?
? ? ?压栈(push):栈顶指针ESP减小4个字节;以字节为单位将寄存器数据(四字节,不足补零)压入堆栈,从高到低按字节依次将数据存入ESP-1、ESP-2、ESP-3、ESP-4指向的地址单元。
?
? ? ?出栈(pop):栈顶指针ESP指向的栈中数据被取回到寄存器;栈顶指针ESP增加4个字节。
?
?
?
图6 出栈入栈操作示意?
?
? ? ?可见,压栈操作将寄存器内容存入栈内存中(寄存器原内容不变),栈顶地址减小;出栈操作从栈内存中取回寄存器内容(栈内已存数据不会自动清零),栈顶地址增大。栈顶指针ESP总是指向栈中下一个可用数据。
?
? ? ?调用(call):将当前的指令指针EIP(该指针指向紧接在call指令后的下条指令)压入堆栈,以备返回时能恢复执行下条指令;然后设置EIP指向被调函数代码开始处,以跳转到被调函数的入口地址执行。
?
? ? ?离开(leave): 恢复主调函数的栈帧以准备返回。等价于指令序列movl %ebp, %esp(恢复原ESP值,指向被调函数栈帧开始处)和popl %ebp(恢复原ebp的值,即主调函数帧基指针)。
?
? ? ?返回(ret):与call指令配合,用于从函数或过程返回。从栈顶弹出返回地址(之前call指令保存的下条指令地址)到EIP寄存器中,程序转到该地址处继续执行(此时ESP指向进入函数时的第一个参数)。若带立即数,ESP再加立即数(丢弃一些在执行call前入栈的参数)。使用该指令前,应使当前栈顶指针所指向位置的内容正好是先前call指令保存的返回地址。
?
? ? ?基于以上指令,使用C调用约定的被调函数典型的函数序和函数跋实现如下:
?
?
?
指令序列
?
含义
?
函数序
?
(prologue)
?
push %ebp
?
将主调函数的帧基指针%ebp压栈,即保存旧栈帧中的帧基指针以便函数返回时恢复旧栈帧
?
mov %esp, %ebp
?
将主调函数的栈顶指针%esp赋给被调函数帧基指针%ebp。此时,%ebp指向被调函数新栈帧的起始地址(栈底),亦即旧%ebp入栈后的栈顶
?
sub , %esp
?
将栈顶指针%esp减去指定字节数(栈顶下移),即为被调函数局部变量开辟栈空间。为立即数且通常为16的整数倍(可能大于局部变量字节总数而稍显浪费,但gcc采用该规则保证数据的严格对齐以有效运用各种优化编译技术)
?
push
?
可选。如有必要,被调函数负责保存某些寄存器(%edi/%esi/%ebx)值
?
函数跋
?
(epilogue)
?
pop
?
可选。如有必要,被调函数负责恢复某些寄存器(%edi/%esi/%ebx)值
?
mov %ebp, %esp*
?
恢复主调函数的栈顶指针%esp,将其指向被调函数栈底。此时,局部变量占用的栈空间被释放,但变量内容未被清除(跳过该处理)
?
pop %ebp*
?
主调函数的帧基指针%ebp出栈,即恢复主调函数栈底。此时,栈顶指针%esp指向主调函数栈顶(esp?esp-4),亦即返回地址存放处
?
ret
?
从栈顶弹出主调函数压在栈中的返回地址到指令指针寄存器%eip中,跳回主调函数该位置处继续执行。再由主调函数恢复到调用前的栈
?
*:这两条指令序列也可由leave指令实现,具体用哪种方式由编译器决定。
?
? ? ?若主调函数和调函数均未使用局部变量寄存器EDI、ESI和EBX,则编译器无须在函数序中对其压栈,以便提高程序的执行效率。
?
? ? ?参数压栈指令因编译器而异,如下两种压栈方式基本等效:
?
extern CdeclDemo(int w, int x, int y, intz); ?//调用CdeclDemo函数
?
CdeclDemo(1, 2, 3, 4); ?//调用CdeclDemo函数
?
压栈方式一
?
压栈方式二
?
pushl 4 ?//压入参数z
?
pushl 3 ?//压入参数y
?
pushl 2 ?//压入参数x
?
pushl 1 ?//压入参数w
?
call CdeclDemo ?//调用函数
?
addl $16, %esp ?//恢复ESP原值,使其指向调用前保存的返回地址
?
subl ? $16, %esp //多次调用仅执行一遍
?
movl ?$4, 12(%esp) //传送参数z至堆栈第四个位置
?
movl ?$3, 8(%esp) //传送参数y至堆栈第三个位置
?
movl ?$2, 4(%esp) //传送参数x至堆栈第二个位置
?
movl ?$1, (%esp) //传送参数w至堆栈栈顶
?
call CdeclDemo ?//调用函数
?
? ? ?两种压栈方式均遵循C调用约定,但方式二中主调函数在调用返回后并未显式清理堆栈空间。因为在被调函数序阶段,编译器在栈顶为函数参数预先分配内存空间(sub指令)。函数参数被复制到栈中(而非压入栈中),并未修改栈顶指针,故调用返回时主调函数也无需修改栈顶指针。gcc3.4(或更高版本)编译器采用该技术将函数参数传递至栈上,相比栈顶指针随每次参数压栈而多次下移,一次性设置好栈顶指针更为高效。设想连续调用多个函数时,方式二仅需预先分配一次参数内存(大小足够容纳参数尺寸和最大的函数即可),后续调用无需每次都恢复栈顶指针。注意,函数被调用时,两种方式均使栈顶指针指向函数最左边的参数。本文不再区分两种压栈方式,"压栈"或"入栈"所提之处均按相应汇编代码理解,若无汇编则指方式二。
?
? ? ?某些情况下,编译器生成的函数调用进入/退出指令序列并不按照以上方式进行。例如,若C函数声明为static(只在本编译单元内可见)且函数在编译单元内被直接调用,未被显示或隐式取地址(即没有任何函数指针指向该函数),此时编译器确信该函数不会被其它编译单元调用,因此可随意修改其进/出指令序列以达到优化目的。
?
? ? ?尽管使用的寄存器名字和指令在不同处理器架构上有所不同,但创建栈帧的基本过程一致。
?
? ? ?注意,栈帧是运行时概念,若程序不运行,就不存在栈和栈帧。但通过分析目标文件中建立函数栈帧的汇编代码(尤其是函数序和函数跋过程),即使函数没有运行,也能了解函数的栈帧结构。通过分析可确定分配在函数栈帧上的局部变量空间准确值,函数中是否使用帧基指针,以及识别函数栈帧中对变量的所有内存引用。
?
?