设为首页 加入收藏

TOP

C语言函数调用栈的详细教程(四)
2018-06-28 19:43:46 】 浏览:566
Tags:语言 函数 调用 详细 教程
有必要,被调函数会设置帧基指针,并保存被调函数希望保持不变的寄存器值。

4) 被调函数通过修改栈顶指针的值,为自己的局部变量在运行时栈中分配内存空间,并从帧基指针的位置处向低地址方向存放被调函数的局部变量和临时变量。

5) 被调函数执行自己任务,此时可能需要访问由主调函数传入的参数。若被调函数返回一个值,该值通常保存在一个指定寄存器中(如EAX)。

6) 一旦被调函数完成操作,为该函数局部变量分配的栈空间将被释放。这通常是步骤4的逆向执行。

7) 恢复步骤3中保存的寄存器值,包含主调函数的帧基指针寄存器。

8) 被调函数将控制权交还主调函数(使用ret指令)。根据使用的函数调用约定,该操作也可能从程序栈上清除先前传入的参数。

9) 主调函数再次获得控制权后,可能需要将先前的参数从栈上清除。在这种情况下,对栈的修改需要将帧基指针值恢复到步骤1之前的值。

步骤3与步骤4在函数调用之初常一同出现,统称为函数序(prologue);步骤6到步骤8在函数调用的最后常一同出现,统称为函数跋(epilogue)。函数序和函数跋是编译器自动添加的开始和结束汇编代码,其实现与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指向主调函数栈顶(espesp-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(只在本编译单元内可见)且函数在编译单元内被直接调用,未被显示或隐式取地址(即没有任何函数指针指向该函数),此时编译器确信该函数不会被其它编译单元调用,因此可随意修改其进/出指令序列以达到优化目的。

尽管使用的寄存器名字和指令在不同处理器架构上有所不同,但创建栈帧的基本过程一致。

注意,栈帧是运行时概念,若程序不运行,就不存在栈和栈帧。但通过分析目标文件中建立函数栈帧的汇编代码(尤其是函数序和函数跋过程),即使函数没有运行,也能了解函数的栈帧结构。通过分析可确定分配在函数栈帧上的局部变量空间准确值,函数中是否使用帧基指针,以及识别函数栈帧中对变量的

首页 上一页 1 2 3 4 下一页 尾页 4/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇编程开发堆排序问题C语言版 下一篇c语言| |数一下 1到 100 的所有整..

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目