当前位置: 首页 > 后端技术 > PHP

【PHP7源码学习】2019-04-12C语言函数调用栈

时间:2023-03-30 03:47:28 PHP

baiyan完整视频:https://segmentfault.com/a/11...简介我们知道在函数调用的过程中,需要Push先出栈,等函数运行完再出栈,回到原函数的调用位置继续往下执行代码。那我们举个例子来清楚的看一下C语言函数调用入栈的过程:intbar(intc,intd){inte=c+d;返回e;}intfoo(inta,intb){returnbar(a,b);}intmain(void){foo(2,5);return0;}在具体分析之前,我们首先需要了解一下寄存器的基本概念:寄存器是CPU区域上的一块存储,访问速度比普通内存高几个数量级。为了提高程序运行的效率,程序运行过程中产生的数据往往存储在寄存器中。寄存器的种类很多:数据寄存器、变址寄存器、指针寄存器、段寄存器、指令指针寄存器、标志寄存器等,它们分别用来存放不同类型的数据。下面一一介绍:数据寄存器。数据寄存器主要用于保存运算次数和运算结果等信息,从而节省读取操作数所需的占用总线和访问内存的时间。RAX、RBX、RCX、RDX与EAX、EBX、ECX、EDX与AX、BX、CX、DX分别称为64位、32位、16位数据寄存器(通用寄存器)。变址寄存器变址寄存器主要用于存储存储单元在段中的偏移量。它们可以用来实现各种内存操作数的寻址方式,为访问不同地址形式的存储单元提供方便。寄存器RSI、RDI与ESI、EDI与SI、DI分别称为64位、32位、16位变址寄存器(IndexRegister)。指针寄存器指针寄存器主要用于存储栈内存的地址,可用于实现各种内存操作数的寻址方式,为访问不同地址形式的存储单元提供方便。寄存器RBP、RSP、EBP、ESP、BP、SP分别称为64位、32位、16位指针寄存器(PointerRegister),可分为两类:(1)BP为基指针(BasePointer)寄存器,指向栈底,可用于直接访问栈中的数据;(2)SP是栈指针(StackPointer)寄存器,只能用来访问栈顶。段寄存器段寄存器是根据内存分段的管理方式设置的。内存单元的物理地址由段寄存器的值和一个偏移值组成,这样可以将两个位数较少的值组合成一个可以访问更大物理空间的内存地址,CS,DS,ES,SS,FS,GS。指令指针寄存器指令指针寄存器存储下一条要执行的指令在代码段中的偏移量。在具有指令预取的系统中,下一条要执行的指令通常已经预取到指令队列中,除非发生分支条件。因此,在理解它们的功能时,并没有考虑到指令队列的存在。RIP、EIP、IP(InstructionPointer)分别是64位、32位、16位的指令指针寄存器。我们重点关注这两个寄存器:RBP(指向栈底)/RSP(指向栈顶)。使用gdb查看C函数调用的压栈过程接下来我们使用gdb的disassemble命令查看函数执行的栈帧:我们观察红框中的部分,当前正在执行的是main函数,函数foo还没有被执行。call,在第10行代码下的两条指令执行前,当前main函数的执行栈帧如下:寄存器RBP(%rbp)的值指向栈底,而寄存器RBP(%rbp)的值寄存器RSP(%rsp)指向栈顶,当前没有其他函数被压入栈中。执行push%rbp指令:将RBP寄存器的值压入栈中,保存调用者(caller)的地址,以便后面执行调用函数后可以正确返回。执行入栈指令后,栈顶指针需要相应移动。执行后的栈帧结构如下:执行mov%rsp,%rbp指令:将寄存器RSP的值赋给寄存器RBP,执行后的栈帧结构如下:第11行代码中gdb图会先用变址寄存器ESI和EDI来保存函数调用的参数值2和5。因为这里的参数是要传给foo函数供foo函数使用的,所以需要暂时存放在这里。然后,使用callq指令进行实际的函数调用。我们使用gdb的s命令进入foo函数的执行栈帧:观察第六行代码的前两条汇编指令,和前面的push操作完全一样,我们直接画出来:接下来,观察第三条汇编指令:sub$0x8,%rsp,表示将RSP寄存器的值减去0x8,然后将结果赋值给RSP寄存器。由于栈的增长方向是从高地址到低地址,所以需要做减法,腾出一段内存空间。sub操作后的栈帧如下:接下来观察第4条和第5条汇编指令,它们将EDI和ESI变址寄存器中的值2和5复制到rbp指针的起始位置,并偏移-0x4和-0x8地址。那么,为什么要从寄存器复制到函数foo的执行栈帧呢?因为只有这样才能在函数内部更方便的使用这两个变量:接下来我们看上图中gdb第7行的代码,也就是returnbar(a,b)的代码,它是也是一个函数调用。同样传入的参数也是2和5,这两个参数也需要暂存,会传递到bar函数的栈帧中,供bar函数内部使用。上面gdb图中,先有两条mov指令,将foo函数栈帧上的值2和5复制到EDX和EAX寄存器中,然后再复制到之前熟悉的ESI和EDI寄存器中暂存,sothat然后复制到bar函数的栈帧中使用。此时foo函数执行完毕,接着会执行callq指令执行下一个函数bar的调用,进入bar函数执行栈帧的部分:我们先看前两行第一行代码,也就是我们非常熟悉的,就是栈操作的一个入口。然后接下来的两行将EDI寄存器中存储的值2和ESI寄存器中存储的值5一起复制到bar函数的栈帧中,即rbp偏移量-0x14和-0x18的地址(这里注意0x14是十进制的20,0x18是24),我们可以画出当前的栈帧结构:继续执行第二行代码,inte=c+d。前两行将栈帧上的值2和5复制到数据寄存器中,准备运行。第三行实际执行加法运算,结果会存入EAX寄存器,第四行将EAX寄存器中的数据存入栈帧偏移rbp-0x4的地址。return后,由于当前bar函数的调用栈已经被销毁,所以会将运算结果7复制回EAX寄存器,等待外层调用接收返回值进行后续使用。此时注意bar函数的局部变量已经不需要保存了,所以这里只保存了一次加法运算的结果,因为外层的foo函数可能还需要用到这个结果。然后,我们注意到它最后有一个pop指令。由于当前函数bar是最后一个被调用的函数,因此bar函数应该从堆栈中弹出。当前栈帧结构如下:bar函数出栈后,我们回到foo函数的调用栈:注意左边箭头的方向,当前执行的是leaveq指令。这条leaveq指令也是一条出栈指令,继续移动foo函数出栈,以此类推,直到最外层的main函数也出栈,程序结束。出栈的栈帧这里就不详细画了,相信大家看到这里就明白了。注意,当我们在自己的gdb中时,需要重点关注rbp和rsp这两个指针寄存器的值所指向的内存地址,以及函数的参数和返回值如何在函数之间的调用过程中顺利传递。视频中也提到了PHP递归入栈的过程。限于篇幅,这里就不一一列举了。有兴趣的同学可以参考视频中gdb的步骤。