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

3,033 阅读5分钟

栈帧图例一张

下文结合图例理解:

调用者的栈帧中,有一项是函数参数,这里指的是被调函数的形参,结合代码来看就是
void be_called_func(int param1, int param2)
{
    int local_x, local_y;
}
void caller()
{
    int local_a, local_b;
    be_called_func(local_a, local_b);
    xxx
}
函数参数处,放置的是param2(实际被赋值为local_b), param1(实际被赋值为local_a), 返回地址对应的是xxx这行代码的指令地址,
本地变量依次存放的是local_x, local_y。

注意:
值传递(passl-by-value):
被调函数的形式参数作为被调函数的局部变量处理,即在堆栈中开辟了内存空间以存放由主调函数放进来的实参的值
(也就是上文中的param1, param2),从而成为了实参的一个副本。

引用传递(pass-by-reference):
被调函数的形式参数虽然也作为局部变量在堆栈中开辟了内存空间,,但是这时存放的是由主调函数放进来的实参变量的地址。
被调函数对形参的任何操作都被处理成间接寻址,即通过堆栈中存放的地址访问主调函数中的实参变量。

参考阅读:《程序员的自我修养》章节10.2.2,讲解的非常好

寄存器理解

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

栈帧理解

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

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

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

push %ebp       // 此为被调函数将ebp寄存器的值入栈,里面存的是主调函数的栈底地址,这样当被调函数结束之后,会重新把栈里保存的
                // 该值重新放入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
关于栈帧的详细理解参考:栈帧
还可以通过书籍《程序员的自我修养》补充理解。

图侵删,未完待续。。。