Java教程

函数

本文主要是介绍函数,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

一、传参

C语言有两种传递参数的方法,分别是值传参指针传参,但其实本质都是值传参,只不过指针传参传递的值是指针罢了。

编译器会在函数调用时,对传入的参数进行复制,所以函数使用的参数和传递的参数不是同一个实体。

指针传参虽然指针本身无法被修改,但是可以修改指针指向的值,所以如果想要修改一个值,可以传递这个值的地址。

二、函数的调用约定

虽然 C 语言对于函数的调用没有规定,但对于编译器而言,每一个函数在被调用时,应该以怎样的方式通过机器指令来实现其调用过程,却存在着相应的事实标准。

调用约定规定了函数调用时需要关注的一系列问题,包括:如何将实惨传递给被调用函数,如何将返回值从被调用函数中返回,如何管理寄存器,如何管理栈内存等等。

 

在 Unix 和类 Unix 系统上,使用名为 System V AMD64 ABI(后简称 “SysV”)的调用约定。

1.函数传递

SysV约定规定,在调用函数时,对于整型和指针类型的实参,需要分别使用 rdi、rsi、rdx、rcx、r8、r9,按函数定义时参数从左到右传值,如下图所示:

 

 

 如果一个函数的传递参数超过了六个,多出来的参数按照从右往左的顺序被逐个压入栈中,如下图所示:

2.返回值传递

SysV约定规定,函数返回值为整数,且小于等于 64 位时,通过寄存器 rax 传递,当返回值大于 64 位小于 128 位时,用 rax 存储低 64 位, rdx 存储高64位,由于不知道如何表示超过 64 位的整型,所以只能看到小于 64 位的返回值用 rax 存储:

 

 

 至于浮点数,返回值用 xmm0 寄存器存储:

3.寄存器使用

SysV 调用约定对寄存器的使用也作出了规定:对于寄存器 rbx、rbp、rsp,以及 r12 到 r15,若被调用函数需要使用它们,则需要该函数在使用之前将这些寄存器中的值进行暂存,并在函数退出之前恢复它们的值(callee-saved)。而对于其他寄存器,则根据调用方的需要,自行保存和恢复它们的值(caller-saved)

4.堆栈清理

函数在结束调用前,通过 leave 指令清理自身堆栈。清理的本质是恢复函数刚进入时 rsp 和 rbp 寄存器的值。

也可以通过以下方法进行清理:

mov rsp, rbp

pop rbp

5.其他约定

1.函数在被 call 指令调用前,需要保证栈顶于 16 字节对齐。

2.从栈顶向上保留 128 字节的 “Read Zone”

Read Zone:位于栈顶向上(低地址方向)的一段固定长度的内存段,这块区域通常可以被函数调用栈中的“叶子”函数(即不再调用其他函数的函数)使用。这样,在需要额外的栈内存时,就能在一定条件下省去先调整栈内存大小的过程。

3.不同于用户函数的调用过程,系统调用(System Call)函数需使用寄存器 rdi、rsi、rdx、r10、r8、r9 传递参数。

二、栈帧

函数调用过程伴随着栈内存中数据的不断变化,每一个函数在调用时,都会在栈内存中呈现出类似的数据结构。而通过这种方式划分出来的,对应于每一次函数调用的栈内存数据块,我们一般称它为“栈帧”。栈帧中存放有与每个函数调用相关的返回地址、实参、局部变量、返回值,以及暂存数据的寄存器值等信息。

rsp 寄存器决定栈顶的位置,也决定了进程能够使用的栈的大小。

每一个函数被调用过程中产生的栈帧都会按照顺序存放在栈内存中,当函数嵌套的层级足够深时,会产生“栈溢出”错误。尾递归优化可以解决该问题。

三、尾递调用归优化

递归函数是一种在满足条件的前提下可以自己调用自己的函数,尾递归调用优化是指在一定下,编译器会直接使用跳转指令代替函数调用指令。但前提是,递归调用语句必须作为函数返回前的最后一条语句

 

 尾递归优化在函数体较小,递归调用次数很多的情况下比较明显,此时函数执行时间先对于栈帧的创建和销毁时间要小很多。

 

这篇关于函数的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!