函数调用的过程以及栈中的状态
过程(函数)调用时的栈(运行时栈)
运行时栈
每个进程都有自己的栈空间,x86-64的栈向低地址方向增长,而栈栈指针%rsp指向栈顶元素。可以使用pushq和popq(汇编指令)将数据存入栈中或是从栈中取出,将栈指针减小一个适当的量可以为没有指定初始值的数据在站上分配空间。类似地,可以通过增加栈指针来释放空间。当x86-64过程需要的存储空间超出寄存器能够存放的大小时,就会在栈上分配空间。这个部分称为过程的栈帧(stack fram)。
为了提供空间和时间效率,x86-64过程只分配自己所需要的的栈帧部分。例如许多过程有6个或者更少的参数,那么所有的参数都可以通过寄存器传滴滴。因此,上图中画出的某些栈帧部分可以省略。实际上,许多函数甚至根本不需要栈帧。当所有的局部变量都可以保存在寄存器中,而且该函数不会调用任何其他函数(有时称为叶子过程,此时把过程钓鱼功能看做树结构)时,就可以这样处理。
过程调用的汇编指令
函数(过程)调用的过程就是将参数及返回地址传入栈帧的过程,同时也将程序计数器转换到函数里面的第一条指令。
汇编指令中,call指令的作用就是调用一个过程,将返回地址(调用返回后的下一条指令地址)压入占中,设置程序计数器PC为过程的第一条指令的地址。而ret指令的作用是从过程中返回,它将从栈中弹出栈顶元素(此时是返回的指令地址,也即是call指令最后压入的地址),将程序计数器设为这个地址。
参数传递
x86-64中,可以通过寄存器最多传递6个整形(例如整数和指针)参数,寄存器的使用时有特殊顺序的,根据传入参数的顺序,会分配对应顺序的寄存器。寄存器使用的名字取决于要传递的数据类型的大小,如下表所示。
如果一个函数有大于6个整形参数,超过6个的部分就要通过栈来传递。假设过程P调用过程Q,有n个整型参数,且n>6,那么P的代码分配的栈帧必须要哦能容纳7到n号参数的存储空间,而第7个参数位于栈顶。
有些时候局部数据必须存放在内存中
常见情况包括:
- 寄存器不足够存放所有的本地数据
- 对一个局部变量使用地址运算符’&’,因此必须能够为它产生一个地址
- 某些局部变量是数组或结构,因此必须能够通过数组或结构引用被访问到。
函数(过程)调用的举例
对于以下函数:
我们考虑程序运行到调用proc时,分析如何完成proc的调用,以下是汇编代码:
首先对于call_proc中的局部变量,我们可以看到,在调用proc函数的时候,对x1,x2,x3,x4都进行了取址操作,因此需要在栈上分配空间。对照汇编代码,首先减小栈指针%rsp以分配空间,先后对x1,x2,x3,x4分配内存并复制。然后从proc的参数列表自右向左的顺序设置参数,将第8,7个参数依次压入占中,然后将第6,5,4,3,2,1个参数分别保存在对应的寄存器中,接着执行call proc指令,该指令将返回地址(proc调用完的后一条指令)压入栈中,并将程序计数器设置为proc的第一条指令。程序自此进入proc函数执行过程。
通俗的说函数调用过程
首先将按参数列表从右到左的顺序压入栈中(超过6个参数的部分)或保存在寄存器中(前6个参数),调用call指令,将返回地址压入栈中,将程序计数器PC设置为被调用过程的第一条指令地址,随后将控制移交给被调用过程,运行被调用过程中的指令。
参考文献
深入理解计算机系统