函数调用过程,栈帧的一点理解

本文深入解析IA32架构下程序寄存器组的使用约定,包括主调函数与被调函数保存寄存器的区别,以及栈帧的结构与管理方式。阐述了如何通过EBP寄存器定位栈帧边界,实现数据访问和函数调用后的栈帧恢复。

栈帧图例一张

寄存器理解

程序寄存器组是唯一能被所有函数共享的资源。虽然某一时刻只有一个函数在执行,但需保证当某个函数调用其他函数时,被调函数不会修改或覆盖主调函数稍后会使用到的寄存器值。因此,IA32采用一套统一的寄存器使用约定,所有函数(包括库函数)调用都必须遵守该约定。
根据惯例,寄存器%eax、%edx和%ecx为主调函数保存寄存器(caller-saved registers),当函数调用时,若主调函数希望保持这些寄存器的值,则必须在调用前显式地将其保存在栈中;被调函数可以覆盖这些寄存器,而不会破坏主调函数所需的数据。寄存器%ebx、%esi和%edi为被调函数保存寄存器(callee-saved registers),即被调函数在覆盖这些寄存器的值时,必须先将寄存器原值压入栈中保存起来,并在函数返回前从栈中恢复其原值,因为主调函数可能也在使用这些寄存器。
此外,被调函数必须保持寄存器%ebp和%esp,并在函数返回后将其恢复到调用前的值,亦即必须恢复主调函数的栈帧。当然,这些工作都由编译器在幕后进行。不过在编写汇编程序时应注意遵守上述惯例。

栈帧理解

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

因此这里对于EBP寄存器的理解尤为关键,因为EBP位置固定,可以通过EBP寄存器的值 + offset 取得主调函数的返回地址(即下一条待执行指令的地址)、取得传入的参数的值等。

至于每一次函数调用都会先执行:

push %ebp       // 此为将ebp寄存器的值入栈,里面存的是主调函数的栈底地址
mov %esp, %ebp  // 此为将栈顶地址(esp寄存器的值)存入ebp寄存器
复制代码

是因为esp寄存器的值一直在变(随着局部变量、临时变量等的加入),ebp寄存器的值在本次函数栈帧中一直保持不变,且始终都是被调函数栈帧的栈底,因此可以通过ebp + offset 获取相关数据信息。同时在vs编译器中,会在被调函数返回时,执行:

mov %ebp, %esp   // 此为将上面ebp寄存器存的值放入esp寄存器,也即esp当前指向了上面说的栈底,此时,
                 // 局部变量占用的栈空间被释放,但变量内容未被清除
pop %ebp         // 主调函数的帧基指针%ebp出栈,即恢复主调函数栈底,与上面的push %ebp对应,
                 // 便是恢复了主调函数的栈帧。此时,栈顶指针%esp指向主调函数栈顶,亦即返回地址存放处,
                 // 主调函数即可继续后面的指令运转了
复制代码

还有另一种情况是不执行mov %ebp, %esp,而是被调函数自己通过清除局部变量等数据来移动esp,直至旧的 %ebp的地方,再 pop %ebp,相比之下没上一种好理解。因为不清楚编译器是否知道该清除多少数据才能到旧的%ebp处,待验证。

关于EBP的理解,参考文章:EBP寄存器的理解EBP寄存器的理解2
关于栈帧的详细理解参考:栈帧
还可以通过书籍《程序员的自我修养》补充理解。

图侵删,未完待续。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值