过程调用对应C语言中的函数,是一系列工作流的抽象过程调用的机器级实现。当函数被嵌套调用时(在函数内部调用另一个函数),计算机底层的过程调用主要有以下三种机制:控制转移。当程序执行到代码段的某个位置时,中断当前序列执行,切换到另一个代码段执行。执行完另一个代码片段后,返回到上一条语句继续执行。本质上,代码执行地址更改并切换数据传输。在调用函数时,一些参数必须传递给函数,而在函数返回时,一些函数计算结果也可能以返回值的形式返回给原函数。本质上就是将数据传入新进程,再传回原进程进行内存管理。进程进行时如何分配内存空间;进程返回后如何销毁存储在内存中的局部变量。栈的访问栈是计算机中一个重要的数据结构,底层进程的调用依赖于这个数据结构。由于这种结构的重要性,处理器本身就支持这种数据结构。栈上有特殊的操作。指令栈是内存中连续的线性空间。栈底在高地址,栈顶在低地址,堆栈从高地址向低地址增长。rsp寄存器存储栈顶元素的地址。栈上有两个基本操作(都是单操作数指令)和pushq指令。操作数可以是立即数或寄存器。运算过程分为三步:取出操作数中的数据;rsp的值减8,栈向低地址方向增长;将src中取出的数据存入更新后的rsppopq指令指向的内存位置,操作数只能是寄存器。操作过程也是三步:根据当前rsp寄存器指向的地址,从地址中取出数据;将取出的数据放入操作数;将rsp寄存器的值加8,完成栈的缩减过程。值得注意的是,在popq的pop操作中,并没有删除数据,而是将数据取出来放到操作数中,相应的位置仍然保存着原来的数据。只是因为栈的边界缩小了,数据虽然还在,但是已经在栈外了。以后入栈的时候会用新的数据覆盖这个数据。要完成push操作,流程调用中的控制传递需要依赖栈的数据结构。有两种主要类型的指令调用和重新调用。过程调用指令的操作数是被调用过程的入口地址,执行两步工作:将返回地址压入堆栈;跳转到操作数指向的位置,继续执行下一条指令。返回地址是紧跟在调用指令之后的指令的地址。跳转过程与跳转指令很相似,就是将指令计数器(即rip寄存器)设置为目标位置对应的地址,即可完成跳转。弹出输入返回地址;结合示例调用指令跳转到返回地址的位置。call指令执行前,rsp寄存器存放0x120,rip存放call指令的地址;当调用指令执行时,它首先被压入堆栈。将rsp寄存器减8,然后放入call指令后的地址(即返回地址),然后将rip寄存器设置为被调用进程的入口地址(400550)。ret进程返回ret指令执行前rip寄存器的值。它是ret指令(400557)的地址。rsp中存放的栈顶地址与刚进入进程时相同,即call指令的返回地址;,作为下一个跳转的地址数据传输,进程调用中的数据传输分为两部分:一是过程调用时的传入参数,二是过程返回时的传出返回值。如何传入参数:在X8664位系统中传递参数时,首选寄存器进行存储。对于过程调用的前六个参数,存放在寄存器中,顺序为rdi、rxi、rdx、rcx、r9、r10;其余参数使用栈传递,最后一个参数最接近栈底,地址最高,最先入栈,第七个参数在栈顶(仅指X86的64位系统,其他系统不同)。x86的32位系统完全使用栈传递参数,不使用寄存器:因为x8632位系统只有8个寄存器,没有足够多的通用寄存器来传递参数,所以完全通过由堆栈。使用栈传递时,需要先访问参数,寄存器不需要访问内存,可以提高性能。返回值只存放在rax的一个寄存器中,这也解释了为什么C语言和C++的返回值只能有一个栈。Localstorageontop:过程中的存储管理如今,大多数编程语言都支持递归,例如c、c++、Java、Pascal、c#。支持递归语言的基本要求是代码可以重入,即一个过程或函数可以在调用不返回之前再次调用。可重入性决定了代码内部的局部变量不能在同一个进程中共享,即进程可能被多次调用,但每个进程都有自己的实例,每个进程的内部状态是不同的。因此,在每次过程调用时,我们需要一个特殊的空间来存储每个过程的状态。状态包括调用过程时的参数、过程内部定义的局部变量、返回地址等。结合栈的数据结构,进程状态有了生命周期的概念。当过程被调用时,过程状态被创建并用于存储状态;当过程返回时,过程状态被破坏。又因为调用过程时调用者在内部调用被调用者,所以被调用者的过程会先于调用者的过程返回。因此,当嵌套调用多个过程时,最后返回的过程应该是第一个被调用的过程。这类似于堆栈的先进后出特性。因此,我们将堆栈与过程调用结合起来。当调用过程时,每个实例的状态都存储在堆栈中;当过程返回时,实例的状态被释放。栈中存储的进程的状态称为帧,与实例结合称为栈帧。函数调用过程和栈帧结合分析,了解栈帧的形成和销毁??。当程序进入yoo函数时,会在栈中分配一个yoo函数的栈帧,rsp寄存器指向栈顶,rbp寄存器指向当前栈帧的底部。虽然在最新版本的X8664位编译器中,rbp寄存器没有这个功能,但是我们还是可以通过设置一些编译选项让它发挥作用。当程序继续调用函数时,栈会继续为调用的函数分配栈帧,rsp和rbp寄存器中存放的地址也会随之变化;当函数返回时,栈帧会被释放,相应的rsp和rbp寄存器也会被释放。栈帧的分配和回收栈帧中存放的数据主要有四类参数。当流程中的参数超过6个时,就会入栈传递。栈上传递的参数属于栈帧局部变量返回地址暂存空间的一部分。它可能不会被使用,但是当分配一个栈帧时,有时栈帧的一部分管理是在机器语言层面管理的,而不是由处理器来实现的。在将高级语言程序编译成指令时,编译器会自动在指令中插入栈帧管理指令。栈帧管理指令主要有两部分:刚进入进程时,如果需要建立一个栈帧,就会有一条栈帧空间申请指令(包括call指令,因为返回地址存放在call中)在进程返回前被插入,如果栈帧建立,会插入栈帧空间释放指令(包括ret指令,因为ret释放返回地址)结合实例理解栈帧的分配和回收前两条指令call-incr是插入栈帧空间的应用指令。rsp-16将rsp寄存器向低地址方向移动16位,这被解释为分配给call-incr的大小为16字节的堆栈帧。然后12513被分配给栈帧。参数应该首先分配给寄存器。12513为什么分配给内存:接下来调用incr函数进行v1变量的寻址操作。如果存放在寄存器中,则不能进行寻址操作。由于寻址操作,v1变量必须分配到内存中。incr函数完成后,会插入一条栈帧空间回收指令。rsp+16完成栈帧回收。从图中可以看出,在call-incr过程中还有8个字节没有被使用,也就是临时空间。值得注意的是,incr进程没有栈帧初始化和回收指令。这是因为incr函数没有嵌套调用其他函数,也不需要使用局部变量,所以不需要使用内存,所以没有分配栈帧。可以看出,编译器并不一定要分配函数的栈帧,而是按需分配。对于叶节点函数,如果不需要使用内存,通常不分配栈帧。关注公众号,让我们携手共进
