基本逻辑
riscv call conversion的协议在这里。所谓call conversion是指函数调用过程中二进制
接口的定义。如果一个程序都相同的编译器构建,自然二进制的接口是一样,但是,程序难免
要使用各种库,这样程序构建的时候就是直接链接库的二进制,双方的二进制接口就要对齐。
其实,只要是不同的二进制程序之间有相互关联都存在二进制接口对齐的问题,必须定义相关
的准则,比如一个内核模块和内核之间的ABI问题,在一个内核版本上编译好的内核模块在不
同的内核上可能就不能使用,这就需要发行版(这里讨论的是Linux的发行版)在一定版本内
ABI是要做到兼容的,比如redhat的发行版在大版本是一定会做到ABI兼容的。
call conversion定义的就是函数调用之间的准则,比如函数调用的时候怎么传递函数入参,
怎么保存寄存器等等。下面的章节逐一介绍下。
寄存器相关的约定
函数调用到了新函数,要使用通用寄存器进行计算,但是这些寄存器上还有发起调用函数的
数据,必须把通用寄存器上的值先保存起来,等新函数使用完寄存器后,返回发起调用函数
执行后续代码的时候再恢复回来原先寄存器上的值。这就有两种办法,一种是发起调用的函
数负责恢复和保存寄存器,一种是在被调函数里使用寄存器之前先保存旧值,使用完再把把
寄存器的旧值恢复回去,一般CPU都会把整个通用寄存器(GPR)分成这两种寄存器,前者叫作
caller saved registers, 后者叫做callee saved registers。
我们先看caller saved registers,从被调函数看,它可以放心的使用caller saved registers,
因为调用函数已经做了保存,调用函数负责在调用被调函数之前保存caller saved registers,
调用函数并不需要保存所有的caller saved registers,它只要保存自己使用的caller saved
registers。
被调函数只需要保存以及恢复它使用的callee saved registers就好,对于callee saved
registers需要先保存再使用。
riscv定义的caller saved register有ra、t0-t6、a0-a7,定义的callee saved register
有sp、s0-s11,浮点也有对应的caller/callee saved registers。t0-t6、a0-a7和s0-s11
的属性是直接定义的,但是ra和sp的属性是天然自带的。ra保存函数的返回地址,涉及到的
指令是: jal rd, offset或者jalr rd, offset(rs1),比如用jal ra, offset实现函数调用,
jal把pc跳到pc + offset,并把jal的下一条指令的地址保存到ra,子函数要靠ra返回到父
函数的调用点,同理父函数的ra保存的是父函数的返回地址,所以在父函数在调用子函数之
前要保存父函数自己的返回地址。sp是栈指针,进入函数首先就要开栈,为函数的临时变量
准备存储空间,离开函数之前退栈,撤销函数的临时变量存储空间。
riscv定义的函数参数传递方式是使用a0-a7作为入参,使用a0-a1传递返回值,如果函数入参
寄存器放不下,使用调用函数栈传递参数,多余的参数从栈顶到栈底依此排列。
写个小程序,然后反汇编后对照的看下:
1 | int add(int a, int b) |
函数参数用栈传递的情况:
1 | int add(int a, int b, int c, int d, int e, int f, int g, int h, int i, int j) |