C语言有两种传递参数的方法,分别是值传参和指针传参,但其实本质都是值传参,只不过指针传参传递的值是指针罢了。
编译器会在函数调用时,对传入的参数进行复制,所以函数使用的参数和传递的参数不是同一个实体。
指针传参虽然指针本身无法被修改,但是可以修改指针指向的值,所以如果想要修改一个值,可以传递这个值的地址。
虽然 C 语言对于函数的调用没有规定,但对于编译器而言,每一个函数在被调用时,应该以怎样的方式通过机器指令来实现其调用过程,却存在着相应的事实标准。
调用约定规定了函数调用时需要关注的一系列问题,包括:如何将实惨传递给被调用函数,如何将返回值从被调用函数中返回,如何管理寄存器,如何管理栈内存等等。
在 Unix 和类 Unix 系统上,使用名为 System V AMD64 ABI(后简称 “SysV”)的调用约定。
SysV约定规定,在调用函数时,对于整型和指针类型的实参,需要分别使用 rdi、rsi、rdx、rcx、r8、r9,按函数定义时参数从左到右传值,如下图所示:
如果一个函数的传递参数超过了六个,多出来的参数按照从右往左的顺序被逐个压入栈中,如下图所示:
SysV约定规定,函数返回值为整数,且小于等于 64 位时,通过寄存器 rax 传递,当返回值大于 64 位小于 128 位时,用 rax 存储低 64 位, rdx 存储高64位,由于不知道如何表示超过 64 位的整型,所以只能看到小于 64 位的返回值用 rax 存储:
至于浮点数,返回值用 xmm0 寄存器存储:
SysV 调用约定对寄存器的使用也作出了规定:对于寄存器 rbx、rbp、rsp,以及 r12 到 r15,若被调用函数需要使用它们,则需要该函数在使用之前将这些寄存器中的值进行暂存,并在函数退出之前恢复它们的值(callee-saved)。而对于其他寄存器,则根据调用方的需要,自行保存和恢复它们的值(caller-saved)
函数在结束调用前,通过 leave 指令清理自身堆栈。清理的本质是恢复函数刚进入时 rsp 和 rbp 寄存器的值。
也可以通过以下方法进行清理:
mov rsp, rbp
pop rbp
1.函数在被 call 指令调用前,需要保证栈顶于 16 字节对齐。
2.从栈顶向上保留 128 字节的 “Read Zone”
Read Zone:位于栈顶向上(低地址方向)的一段固定长度的内存段,这块区域通常可以被函数调用栈中的“叶子”函数(即不再调用其他函数的函数)使用。这样,在需要额外的栈内存时,就能在一定条件下省去先调整栈内存大小的过程。
3.不同于用户函数的调用过程,系统调用(System Call)函数需使用寄存器 rdi、rsi、rdx、r10、r8、r9 传递参数。
函数调用过程伴随着栈内存中数据的不断变化,每一个函数在调用时,都会在栈内存中呈现出类似的数据结构。而通过这种方式划分出来的,对应于每一次函数调用的栈内存数据块,我们一般称它为“栈帧”。栈帧中存放有与每个函数调用相关的返回地址、实参、局部变量、返回值,以及暂存数据的寄存器值等信息。
rsp 寄存器决定栈顶的位置,也决定了进程能够使用的栈的大小。
每一个函数被调用过程中产生的栈帧都会按照顺序存放在栈内存中,当函数嵌套的层级足够深时,会产生“栈溢出”错误。尾递归优化可以解决该问题。
递归函数是一种在满足条件的前提下可以自己调用自己的函数,尾递归调用优化是指在一定下,编译器会直接使用跳转指令代替函数调用指令。但前提是,递归调用语句必须作为函数返回前的最后一条语句。
尾递归优化在函数体较小,递归调用次数很多的情况下比较明显,此时函数执行时间先对于栈帧的创建和销毁时间要小很多。