【C++】函数执行过程

目录

 

内存

对于程序,

调用栈

函数调用过程通常使用堆栈实现,

每个用户态进程对应一个调用栈结构(call stack)。

栈帧结构

【扩展阅读】

函数调用过程


内存

 

我们的程序代码是在操作系统提供的内存中执行的, 一般而言,

但是,大多数操作系统会把4G的内存空间进行划分,比如linux通常会默认把高地址的1G空间分配给内核(内核空间给内核线程执行特权操作,比如维护打开文件表等),剩余的作为内存空间。

内存空间又大致分为堆空间,栈空间,代码区等。其中,

: 用户维护函数调用上下文由高地址向低地址生长,通常以M为单位,由操作系统维护。

  [不能申请占用过大的内存的局部变量,会导致栈爆掉而core.如果变量太大,可以考虑放到全局变量区或者使用堆]

动态申请的内存,即使用new or malloc等分配到的内存,可以比栈大很多,需用户自己释放

  [new/delete,malloc/free成对出现,否则会导致内存泄露,可以使用查找内存泄露的工具监控或者自己写代码监控]

代码区:存放代码的内存映像

保留区:禁止访问的一些区域,比如NULL

  [使用*访问置为NULL的指针会出错,也需要小心野指针]

对于程序,

编译器会对其分配一段内存,在逻辑上可以分为代码段,数据段,堆,栈 ,其中

代码段:保存文本程序,指令指针EIP指向代码段(可读可写不可执行)

数据段:保存初始化的全局变量和静态变量(可读可写不可执行)

BSS:保存未初始化的静态变量和全局变量;

堆:用于动态分配内存

栈:保存局部变量,函数参数,函数需要保存的寄存器值,函数的调用信息等,向地址减小的方向增长(可读可写课执行)

调用栈


函数调用过程通常使用堆栈实现,

每个用户态进程对应一个调用栈结构(call stack)。


编译器使用堆栈传递函数参数、保存返回地址、临时保存寄存器原有值(即函数调用的上下文)以备恢复以及存储本地局部变量。

内存地址从栈底到栈顶递减,压栈就是把ESP指针逐渐往地低址移动的过程。而结构体tStrt中的成员变量memberX地址=tStrt首地址+(memberX偏移量),
即越靠近tStrt首地址的成员变量其内存地址越小。因此,结构体成员变量的入栈顺序与其在结构体中声明的顺序相反。

 

栈帧结构

栈帧是一个函数被调用或者说函数在执行时的状态表现,其中存储了函数执行除指令代码之外其他所有的属于该函数的信息(不包括静态变量和全局变量)

函数调用经常是嵌套的,在同一时刻,堆栈中会有多个函数的信息。每个未完成运行的函数占用一个独立的连续区域,称作栈帧(Stack Frame)。栈帧是栈的逻辑片段,当调用函数时逻辑栈帧被压入堆栈, 当函数返回时逻辑栈帧被从堆栈中弹出。栈帧存放着函数参数,局部变量及恢复前一栈帧所需要的数据等。

使用栈帧的一个好处是使得递归变为可能,因为对函数的每次递归调用,都会分配给该函数一个新的栈帧,这样就巧妙地隔离当前调用与上次调用。

 栈帧的边界由栈帧基地址指针EBP和堆栈指针ESP界定(指针存放在相应寄存器中)。EBP指向当前栈帧底部(高地址),在当前栈帧内位置固定;ESP指向当前栈帧顶部(低地址),当程序执行时ESP会随着数据的入栈和出栈而移动。因此函数中对大部分数据的访问都基于EBP进行——改变栈底指针从而访问不同函数的数据;

函数调用时的入栈顺序 
实参N~1—>主调函数返回值—>主调函数帧基指针EBP—>被调函数局部变量1~N

EBP指针在当前函数运行过程中(未调用其他函数时)保持不变。在函数调用前,ESP指针指向栈顶地址,也是栈底地址。在函数完成现场保护之类的初始化工作后,ESP会始终指向当前函数栈帧的栈顶,此时,若当前函数又调用另一个函数,则会将此时的EBP视为旧EBP压栈,而与新调用函数有关的内容会从当前ESP所指向位置开始压栈。

【扩展阅读】

函数局部变量布局方式

与函数调用约定规定参数如何传入不同,局部变量以何种方式布局并未规定。编译器计算函数局部变量所需要的空间总数,并确定这些变量存储在寄存器上还是分配在程序栈上(甚至被优化掉)——某些处理器并没有堆栈。局部变量的空间分配与主调函数和被调函数无关,仅仅从函数源代码上无法确定该函数的局部变量分布情况。

基于不同的编译器版本(gcc3.4中局部变量按照定义顺序依次入栈,gcc4及以上版本则不定)、优化级别、目标处理器架构、栈安全性等,相邻定义的两个变量在内存位置上可能相邻,也可能不相邻,前后关系也不固定。若要确保两个对象在内存上相邻且前后关系固定,可使用结构体或数组定义。

函数调用过程

 函数调用时的具体步骤如下:
 
 1) 主调函数将被调函数所要求的参数,根据相应的函数调用约定,保存在运行时栈中——该操作会改变程序的栈顶指针。
 
 注:x86平台将参数压入调用栈中。而x86_64平台具有16个通用64位寄存器,故调用函数时前6个参数通常由寄存器传递,其余参数才通过栈传递。
 

 2) 主调函数将控制权移交给被调函数(使用call指令)。函数的返回地址(待执行的下条指令地址)保存在程序栈中(压栈操作隐含在call指令中)。
 
 3) 若有必要,被调函数会设置帧基指针,并保存被调函数希望保持不变的寄存器值。
 
 4) 被调函数通过修改栈顶指针的值,为自己的局部变量在运行时栈中分配内存空间,并从帧基指针的位置处向低地址方向存放被调函数的局部变量和临时变量。
 
 5) 被调函数执行自己任务,此时可能需要访问由主调函数传入的参数。若被调函数返回一个值,该值通常保存在一个指定寄存器中(如EAX)。
 
 6) 一旦被调函数完成操作,为该函数局部变量分配的栈空间将被释放。这通常是步骤4的逆向执行。
 
 7) 恢复步骤3中保存的寄存器值,包含主调函数的帧基指针寄存器。
 
 8) 被调函数将控制权交还主调函数(使用ret指令)。根据使用的函数调用约定,该操作也可能从程序栈上清除先前传入的参数。
 
 9) 主调函数再次获得控制权后,可能需要将先前的参数从栈上清除。在这种情况下,对栈的修改需要将帧基指针值恢复到步骤1之前的值。
 
 步骤3与步骤4在函数调用之初常一同出现,统称为函数序(prologue);步骤6到步骤8在函数调用的最后常一同出现,统称为函数跋(epilogue)。函数序和函数跋是编译器自动添加的开始和结束汇编代码,其实现与CPU架构和编译器相关。除步骤5代表函数实体外,其它所有操作组成函数调用。

函数的形参是从右向左入栈的,就是最右边的参数地址最高,最左边的参数地址最低,这是为了便于EBP寻址

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值