导语 | 在恣意一门编程言语中,函数调用基本上都是十分常见的操作;咱们都知道,函数是由调用栈完结的,不同的函数调用会切换上下文;可是,你是否好奇,关于一个函数调用而言,其底层到底是怎样完结的呢?本文解说了函数调用的底层逻辑完结。

一、汇编概述

既然要解说函数调用的底层逻辑完结,那么汇编言语咱们是绕不过的。

因而,首要来温习一下汇编相关的常识。

咱们都知道,核算机只能读懂二进制指令,而汇编便是一组特定的字符,汇编的每一条句子都直接对应CPU的二进制指令,比方:mov rax,rdx便是咱们常见的汇编指令。

汇编言语便是经过一条条的 助记符+操作数完结的,而且汇编指令经过汇编器(assemble,例如Linux下的as)转变为实践的CPU二进制指令。

(一)一个简略的汇编比方

上面讲的有些空洞,来看一个实践的比方:

; 将存放器rsp的值存储到存放器rbp中
mov rbp, rsp
; 将四个字节的4存储到地址为rbp-4的栈上
mov DWORD PTR [rbp-4], 4
; 将rsp的值减去16
sub rsp, 16

需求留意的是:汇编言语是和实践底层的CPU休戚相关的;上面的汇编格局运用的便是Intel的语法格局。

常见的汇编言语有两种天壤之别的语法:

  • Intel格局:optcode destination,source,相似于语法int i=4。

  • AT&T格局:optcode source,destination,直观了解为move from source to destination。

若将上面的Intel汇编改写为AT&T汇编,则为:

movq %rsp, %rbp
movl $4, -4(%rbp)
subq $16, %rsp

能够看到,AT&T汇编的别的一个特点是:有前缀和后缀。

比方:前缀%,$;后缀q,l等等。

这些前缀后缀有特别的意思,后文会解说,不同的格局侧重点不太相同。

(二)常用汇编指令

下面是一些十分常用的汇编指令,在后文中都会用到:

浅谈函数调用!

二、通用存放器概述

关于汇编言语,只是了解其语法内容是远远不够的!

因为汇编言语和CPU是休戚相关的,因而在硬件层面咱们还需求关注CPU的通用存放器。

在所有CPU系统架构中,每个存放器一般都是有主张的运用办法的,而编译器也一般依照CPU架构的主张来运用这些存放器,因而咱们能够认为这些主张是强制性的。

(一)8086架构(16bit)

让咱们把视线首要转移到8086

下图展现了在8086 CPU中的各个存放器:

浅谈函数调用!

首要包含下面几类存放器:

  • 通用存放器:均可用来存放地址和数据。

  • 指针和变量存放器:用来存放,某一段内地址偏移量,用来形成操作数地址,首要用来再仓库操作或许变址操作中运用。

  • 段存放器:因为存储器空间是分段的,所以这些段存放器则是每个段的首地址。

  • 指令指针:IP用来存放将要履行的下一条指令再现在代码段的偏移量,将这个偏移量+段存放器中存放的基地址,就找到了下一条指令的地址。

  • 标志位存放器:用来存放核算成果的特征,这些标志位常常被用作接下来程序运转的条件。

  • 8086处理器内部有8个16位的通用存放器,也便是CPU内部的数据单元,分别是:AX、BX、CX、DX、SP、BP、SI、DI。

这些存放器的作用首要是:暂存核算机进程中的数据。

别的,AX、BX、CX、DX这四个存放器又能够分为两个8位的存放器来运用,分别是AH、AL、BH、BL、CH、CL、DH、DL。

留意:其中H标明高位(high),L标明低位(low)的意思。

下面来看下操控单元:

IP存放器便是指令指针存放器(Instruction Pointer Register),指向代码段中下一条指令的方位;CPU会依据它不断地从内存的代码段中取出指令并加载到CPU的指令行列中,然后交给运算单元去履行。CS、DS、SS、ES这四个存放器都是16位存放器,用来存储进程的地址空间信息。

比方:

  • CS是代码段存放器(Code Segment Register),经过它能够找到代码在内存中的方位。

  • DS是数据段存放器(Data Segment Register),经过它能够找到数据在内存中的方位。

  • SS是栈存放器(Stack Register),栈是程序运转进程所需求的一种数据结构,首要用于记录函数调用的关系。

  • ES是一个附加段存放器(Extra Segment Register),当发现段存放器不够用的时分,你能够考虑运用ES段存放器。

怎样依据上述段存放器找到所需的地址呢?

CS和DS中都存放着一个段的开端地址,代码段的偏移值存放在IP存放器中,而数据段的偏移值放在通用存放器中;因为8086架构中总线地址是20位的,而段存放器和IP存放器以及通用存放器都是16位的,所认为了得到20位的地址,先将段存放器中开端地址左移4位,然后再加上偏移量,就得到了20位的地址;也正是因为偏移量是16位的,所以每个段最大的巨细是64K的。

别的,关于20位的地址总线来说,能拜访到的内存巨细最多也就只要2^20=1MB。

假如核算得到某个要拜访的地址是1MB+X,那么终究拜访的是地址X,因为地址线只能发送低20位的。

  • 关于标志位

8086CPU设置了一个:16位标志存放器PSW(也叫FR),其中规则了9个标志位,用来存放运算成果特征和操控CPU操作。

浅谈函数调用!

9个标志位能够分为两类大类:

  • 条件码
  • 操控标志位

其中条件码包含:

  • OF(Overflow Flag)溢出标志,溢出时为1,不然置0:标明一个溢出了的核算,如:结构和方针不匹配。
  • SF(Sign Flag)符号标志,成果为负时置1,不然置0。
  • ZF(Zero Flag)零标志,运算成果为0时置1,不然置0。
  • CF(Carry Flag)进位标志,进位时置1,不然置0;留意:Carry标志中存放核算后最右的位;
  • AF(Auxiliary carry Flag)辅助进位标志,记录运算时第3位(半个字节)发生的进方位。有进位时1,不然置0;
  • PF(Parity Flag)奇偶标志,成果操作数中1的个数为偶数时置1,不然置0;

操控标志位包含:

  • DF(Direction Flag)方向标志,在串处理指令中操控信息的方向。

  • IF(Interrupt Flag)中断标志。

  • TF(Trap Flag)陷井标志。

(二)x86架构

接着,让咱们步入32位机年代,来看看x86系统下的CPU存放器:

浅谈函数调用!

能够看到,为了使得运转在8086架构上的程序在移到32位架构之后也能履行,32位架构对8086架构进行了兼容:

  • 通用存放器从16位变成了32位,也便是8个32位的通用存放器;可是为了坚持兼容,仍然保存了16位和8位的运用办法,即:AH、AL等。

  • 指向下一条指令的指令指针存放器也从16位变成了32位,被称为EIP,可是相同兼容16位的运用办法。

  • 段存放器改动比较大:在32位架构中段存放器仍是16位,可是它不再标明段的开端地址,而是标明索引;32位架构中,引入了段描述符表,表格中的每一项都是段描述符(Segment Descriptor),记录了段在内存中的开端方位,而这张表则存放在内存的某个地址;那么,段存放器中存的便是对应段在段表中的方位,称为选择子(selector)。

关于选择子:

先依据段存放器拿到段的开端地址,再依据段存放器中保存的选择子,找到对应的段描述符,然后从这个段描述符中取出这个段的开端地址;就相当于由之前的直接找到段开端地址变成了直接找到段开端地址;这样改动之后,段开端地址会变得很灵敏。

可是这样就跟本来的8086架构不兼容了,因而为了兼容8086架构,32位架构中引入了实形式和保存形式:8086架构中的办法就称为实形式,32位这种形式就被称为维护形式。

当系统刚刚发动的时分,CPU是处于实形式的,这个时分和8086形式是兼容的;当需求更多内存时,进行一系列的操作,将其切换到维护形式,这样就能运用32位了。

形式能够了解为:CPU和操作系统的一起干活的形式:

  • 在实形式下,两者约好好了这些存放器是干这个的,总线是这样的,内存拜访是这样的。
  • 在维护形式下,两者约好好了这些存放器是干那个的,总线是那样的,内存拜访是那样的。

这样操作系统给CPU下命令,CPU依照约好好的,就能得到操作系统意料的成果,操作系统也依照约好好的,将一些数据结构,例如段描述符表放在一个约好好的当地,这样CPU就能找到。两者就能够合作工作了。

下面是x86平台下一些存放器的调用特别约好:

浅谈函数调用!

作为通用存放器,进程调用中,调用者栈帧需求存放器暂存数据,被调用者栈帧也需求存放器暂存数据。

为防止调用进程中数据不会被损坏丢失,C/C++编译器恪守如下约好的规则:

浅谈函数调用!

当发生函数调用时,子函数内一般也会运用到通用存放器,那么这些存放器中之前保存的调用者(父函数)的值就会被掩盖!为了避免数据掩盖而导致从子函数回来时存放器中的数据不可康复,CPU 系统结构中就规则了通用存放器的保存办法。

假如一个存放器被标识为Caller Save, 那么在进行子函数调用前,就需求由调用者提早保存好这些存放器的值,保存办法一般是把存放器的值压入仓库中,调用者保存完结后,在被调用者(子函数)中就能够随意掩盖这些存放器的值了。

假如一个存放被标识为Callee Save,那么在函数调用时,调用者就不必保存这些存放器的值而直接进行子函数调用,进入子函数后,子函数在掩盖这些存放器之前,需求先保存这些存放器的值,即这些存放器的值是由被调用者来保存和康复的。

具体来讲:

当该函数是处于调用者人物时,假如该函数履行进程中发生的暂时数据会已存

储在%eax,%edx,%ecx这些存放器中,那么在其履行call指令之前会将这些存放器的数据写入其栈帧内指定的内存区域,这个进程叫做调用者保存约好(Caller Save)。

当该函数是处于被调用者人物时,那么在其运用这些存放器%ebx,%esp,%edi之前,那么该函数会保存这些存放器中的信息到其栈帧指定的内存区域,这个进程叫被调用者保存约好;%eax总会被用作回来整数值。

%esp,%ebp总被分别用着指向当时栈帧的顶部和底部,首要用于在当时函数推出时,将他们还原为原始值;往往会在栈帧开端处保存上一个栈帧的ebp,而esp是全栈的栈顶指针,一直指向栈的顶部。

注:在x86-64架构下也是相似的约好!

(三)x86-64架构

  • 存放器约好

终究便是咱们现在主流的x86-64架构了;

关于x86-64架构,最常用的有16个64位通用存放器,各存放器及用途如下所示:

浅谈函数调用!

从上面的表能够看到,除了扩展本来存在的通用存放器,x64架构还引入了8个新的通用存放器:r8-r15。

这些存放器虽然都能够用,可是仍是做了一些规则,如下:

函数回来值存放的存放器:rax。

  • rax一起也用于乘法和除法指令中。在imul指令中,两个64位的乘法最多会发生128位的成果,需求rax与rdx共同存储乘法成果,在div指令中被除数是128位的,相同需求rax与rdx共同存储被除数
  • rsp是仓库指针存放器,一般会指向栈顶方位,仓库的pop和push操作便是经过改动rsp的值即移动仓库指针的方位来完结的。
  • rbp是栈帧指针,用于标识当时栈帧的开端方位。
  • %rdi,%rsi,%rdx,%rcx,%r8,%r9六个存放器用于存储函数调用时的6个参数(假如有6个或6个以上参数的话)。
  • rbx被标识为 “miscellaneous registers”,归于通用性更为广泛的存放器,编译器或汇编程序能够依据需求存储任何数据。
  • rbx、rbp、r12、r13、r14、r15:这些存放器由被调用者负责维护,在回来的时分要康复这些存放器中原本的值。

一起,和上面x32架构相似这儿也要差异Caller Save和Callee Save存放器,即存放器的值是由 调用者保存 仍是由 被调用者保存。

  • 函数传参优化

在x32的年代,通用存放器少,参数传递都是经过入栈(汇编指令push)完结的(当然也有运用存放器传递的,比方著名的C++ this指针运用ecx存放器传递,不过能用的存放器毕竟不多),相对CPU存放器来说,拜访太慢,函数调用的功率就不高;

而在x86-64年代,存放器数量多了,CPU就能够利用额外的存放器rdi、rsi、rdx、rcx、r8、r9来存储参数!

存放器传参的优点是速度快,减少了对内存的读写次数。

注:多于6个的参数,仍然仍是经过入栈完结传递。

因而在x86_64位机器上编程时,需求留意:

  • 为了功率尽量运用少于6个参数的函数。
  • 传递比较大的参数,尽量运用指针,因为存放器只要64位。

留意:具体运用栈仍是用存放器传参数,这个不是编程言语决议的,而是编译器在编译生成CPU指令时决议的。假如编译器非要在x64架构CPU上运用线程栈来传参那也不是不可,这个对高档言语是无感知的。

(四)x86-64存放器的向下兼容

上述的存放器名字都是64位的名字,关于每个存放器,咱们还能够只运用它的一部分,并运用另一个新的名字:

浅谈函数调用!

浅谈函数调用!

下面这些存放器或许也会需求用到其他存放器:

  • 8个80位的x87存放器(%st0~st7),用于浮点核算。
  • 8个64位的MMX存放器,用于MMX指令(多媒体指令),这8个存放器跟 x87存放器在物理上是相同的存放器。
  • 16个128位的SSE存放器,用于SSE指令。
  • RIP指令存放器,保存指令地址。
  • flags(rflags-64位,eflags-32位)存放器。每个位用来标识一个状况。比方,这些标识符或许用于比较和跳转的指令。

和上面所述的x86架构相似,在x86-64架构下也存在实形式;更多关于 x86-64 处理器架构:

c.biancheng.net/view/3460.h…

www.cnblogs.com/mazhimazhi/…

更多关于CPU存放器前史见: zhuanlan.zhihu.com/p/272135463

三、函数调用结构

上文简略温习了一下汇编和存放器相关的内容。下面来正式来看看函数调用的底层是怎样完结的!

注:这儿的阐明采用的是:

  • 编译器:GCC 12.1。
  • 优化级别为-O0。
  • 汇编指令为intel架构。

(一)函数调用

子函数调用时,调用者与被调用者的栈帧结构如下图所示:

浅谈函数调用!

在子函数调用时,需求切换上下文使得当时调用栈进入到一个新的履行中:

  • 父函数将调用参数从后向前压栈:由函数调用者完结(上文中的Caller逻辑)。
  • 将回来地址压栈保存:call指令完结。
  • 跳转到子函数开端地址履行:call指令完结。
  • 子函数将父函数栈帧开端地址(%rpb)压栈:由函数被调用者完结(上文中的Callee逻辑);
  • 将%rbp的值设置为当时%rsp的值,即将%rbp指向子函数栈帧的开端地址:由函数被调用者完结(上文中的Callee逻辑),完结函数上下文的切换。

保存回来地址和保存上一栈帧的%rbp都是为了函数回来时,康复父函数的栈帧结构(保存函数调用上下文)。

在运用高档言语进行函数调用时,由编译器主动完结上述整个流程;乃至关于”Caller Save”和“Callee Save”存放器的保存和康复,也都是由编译器主动完结的。

需求留意的是:父函数中进行参数压栈时,顺序是从后向前进行的(调用栈空间都是从大地址向小地址延伸,这一点刚好和堆空间相反)。

这一行为并不是固定的,是依赖于编译器的具体完结的。

至少在GCC中,运用的是从后向前的压栈办法,这种办法便于支撑相似于printf(“%d,%d”,i,j) 这样的运用变长参数的函数调用。

以下面的函数为例:

void func() {}
void my_func() {
    func();
}

对应的汇编为:

func():
        push    rbp
        mov     rbp, rsp
        nop
        pop     rbp
        ret
my_func():
        push    rbp
        mov     rbp, rsp
        call    func()
        nop
        pop     rbp
        ret

在函数my_func和func中:开端的两句便是由编译器默许生成的切换上下文句子(函数my_func中也存在这个句子是因为它终究也会被其他函数s调用)。

当my-func函数调用func函数时:

  • 首要,履行call指令,保存回来地址,并跳转至func函数开端地址(这儿没有压栈调用参数是因为func入参为空)。
  • 随后,在func函数中,运用push rbp和mov rbp,rsp保存上下文,随后开端履行func函数中的逻辑。
  • 因为没有代码,且没有回来值,此次为nop指令。
  • 终究,康复上下文,并回来(函数回来鄙人文中介绍)。

函数最初的push rbp和mov rbp,rsp又叫做函数的序言(prologue),简直每个函数一开端都会该指令。

它和函数终究的pop rbp和ret(epilogue)起到维护函数的调用栈的作用。

接下来,水到渠成的咱们来看一下函数的回来进程。

(二)函数回来

函数回来时,咱们只需求得到函数的回来值(保存在%rax中),之后就需求将栈的结构康复到函数调用之差的状况,并跳转到父函数的回来地址处持续履行即可。

因为函数调用时现已保存了回来地址和父函数栈帧的开端地址,要康复到子函数调用之前的父栈帧,咱们只需求履行以下两条指令:

pop rbp
ret

首要履行pop rbp指令,直接将调用栈地址康复至调用函数之前的状况。

随后经过ret指令跳转至回来地址处并履行。

(三)数据参数传递

  • 函数参数传递概述

在函数调用中,另一个需求关注的便是函数参数的传递:入参传递以及回来值传递。

函数在核算的时分,存储数据的当地总共有三个:

  • 存放器;
  • 内存:栈空间、堆(heap)空间、静态区。
  • 程序自身:只读的程序数据片段,比方int i=4,这个4存储于程序自身,在汇编里边又叫当即数(immediate number)。

知道了数据的存储当地,那么数据的传递就分为以下四个方面:

  • 从内存到存放器;
  • 从存放器到内存;
  • 从当即数到存放器;
  • 从当即数到内存。

留意:数据不能从内存直接传递到内存,假如需求从内存传递到内存,要以存放器为中介!

一起需求留意的是:数据是有巨细的!

比方:一个word是两个字节(16bit),double words是四个字节(32bit),quadruple words是八个字节(64bit)。

所以传递数据的时分,要知道传递的数据巨细:

Intel格局的汇编会在数据前面阐明数据巨细:比方 mov DWORD PTR [rbp-4],4,意思是将一个4字节的4存储到栈上(地址为rbp-4)。

而AT&T格局是经过指令的后缀来阐明,相同的指令为movl 4,−4(4, -4(%rbp);而且存储的当地,AT&T汇编是经过前缀来差异,比方%q前缀标明存放器,标明当即数,()标明内存。

学习了数据的传递办法之后,让咱们看看函数的调用习气。

  • 函数参数传递约好

之前咱们简略学习了一下Caller和Callee的差异,在这儿咱们会深化的学习。

首要,什么是函数调用约好?

在Caller调用Callee时,要将参数(arguements)传递给Callee,一个函数能够接收多个参数,而Caller与Callee之间约好的每个参数的应该怎样传递便是调用习气;这样,Callee才能到指定的方位获取到相应的参数。

比方下面的代码:

int square(int num) {
    return num * num;
}
int main() {
    int i = 4;
    int j = square(i);
}

在main函数中调用square,参数i是怎样传递到square中的?

上面的代码对应的汇编如下:

square(int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        imul    eax, eax
        pop     rbp
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     DWORD PTR [rbp-4], 4
        mov     eax, DWORD PTR [rbp-4]
        mov     edi, eax
        call    square(int)
        mov     DWORD PTR [rbp-8], eax
        mov     eax, 0
        leave
        ret

经过上面的汇编,咱们能够知道:

在main里边,4先存到栈上(mov DWORD PTR [rbp-4],4),然后存在edi里边(mov eax,DWORD PTR [rbp-4]、mov edi,eax),而sqaure函数直接就从edi里边读取4的值了!

这就阐明:参数4是经过存放器edi传给了callee (sqaure) 。

或许有同学会认为,从代码看,参数不是直接就传给了sqaure吗?实践上,在汇编中,这个变量i是不存在的,只要存放器和内存,因而咱们需求约好好入参i的值存在哪里。

下面让咱们来具体看看这些约好、常见存放器负责传递的参数以及一些作用(前文扼要介绍了一些):

浅谈函数调用!

在上面的列表中:

  • 蓝色的是callee-owned、绿色背景的是caller-owned。
  • callee-owned标明:callee能够自由地运用这些存放器,掩盖已有的值;假如caller要运用这些存放机,那么它在调用callee前,要把这些存放器保存好;例如:假如存放器%rax的值caller想要保存,那么在调用函数之前,caller需求赋值这个值到“安全”的当地。
  • caller-owned标明:假如callee要运用这些存放器,那么它就要保存好这些存放器的值,而且回来到caller的时分要将这些值康复;caller-owned的存放器一般用于caller需求在函数之间保存的部分状况。
  • 一共有六个通用的存放器用于传递参数;按顺序传递需求通用存放器传递的参数,假如通用存放器运用完了,那么就运用栈来传递。

一起,假如函数回来比较大的目标,那么第一个参数rdi会用来传递存储这个目标的地址(这个地址是由caller分配的)。

有了这些基础,咱们就更简单了解C++中的copy elision了。

相关阅读:

  • 深化了解C++中的move和forward

  • Copy/move elision: C++ 17 vs C++ 11

四、常见操控结构

在知道了函数参数是怎样传递的之后,咱们来更升一级。

下面依据具体代码来看一看咱们经常运用的if、for、while等操控结构在底层是怎样完结的。

if,while循环等操控结构,在汇编里边,都是基于断定句子,跳转句子完结的:做一个核算,查看相应的flag,然后依据flag的值确定要跳转到哪里。

比方下面的if句子:

int multiply(int j) {
    if (j > 6) {
        return j*2;
    } else {
        return j*3;
    }
}

对应的汇编句子如下:

multiply(int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        cmp     DWORD PTR [rbp-4], 6
        jle     .L2
        mov     eax, DWORD PTR [rbp-4]
        add     eax, eax
        jmp     .L3
.L2:
        mov     edx, DWORD PTR [rbp-4]
        mov     eax, edx
        add     eax, eax
        add     eax, edx
.L3:
        pop     rbp
        ret

最前面和终究两条命令便是函数调用中的上下文切换,这个在前文中现已具体阐明晰。

函数的逻辑从第三条句子真实开端:

mov DWORD PTR [rbp-4],edi标明将存放器edi中的4个字节的值(DWORD PTR)移至 [rbp-4] 对应内存地址中。

这儿和上面所叙述的参数传递的约好是坚持一致的,因为咱们的入参j是int类型,只要32位,因而运用的是edi存放器来传递的参数。

随后,运用cmp指令将内存中的数和当即数6进行比较(即,j>6),此指令会改动标志存放器%eflags的状况。

然后jle会利用标志存放器%eflags中的状况进行跳转:

  • 假如j<=6,跳转至.L2。
  • 不然持续向下履行(对应j>6的场景)。

无论是向下履行仍是跳转至.L2履行,终究两者都会履行至.L3并回来。

下面再来看一个for循环的比方:

int add(int j) {
    int ret = 0;
    for (int i = 0; i < j; ++i) {
        ret+= i;
    }
    return ret;
}

对应的汇编如下:

add(int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-20], edi
        mov     DWORD PTR [rbp-4], 0
        mov     DWORD PTR [rbp-8], 0
        jmp     .L2
.L3:
        mov     eax, DWORD PTR [rbp-8]
        add     DWORD PTR [rbp-4], eax
        add     DWORD PTR [rbp-8], 1
.L2:
        mov     eax, DWORD PTR [rbp-8]
        cmp     eax, DWORD PTR [rbp-20]
        jl      .L3
        mov     eax, DWORD PTR [rbp-4]
        pop     rbp
        ret

从上面的汇编咱们能够看到,入参j依旧是由存放器edi传递,并存储在了内存[rbp-20]中。

随后两行分别初始化了参数ret:[rbp-4]、i:[rbp-8]。

紧接着,指令直接跳转至.L2处,首要比较了[rbp-8]和[rbp-20]中的值(即比较i和j):假如i<j则跳转至.L3处履行。

这儿的判别是符合for循环的逻辑的:在进入for循环之前首要会判别一次条件。

.L3代码块是for循环的真实逻辑:

; ret += i;
mov     eax, DWORD PTR [rbp-8]
add     DWORD PTR [rbp-4], eax
; ++i
add     DWORD PTR [rbp-8], 1

其他操控结构的逻辑也是相似的,这儿不再赘述了!

五、总结

本文首要扼要温习了汇编以及通用存放器相关的内容,随后进入到文章主题:函数调用。在函数调用中叙述了函数调用中的调用和回来细节、上下文切换维护、函数传递等内容。终究稍微引申了函数中常见操控结构的底层完结。

作者简介:

张凯,腾讯后台开发工程师。