6.3. 函数调用约定
函数调用约定主要规定函数调用时参数如何传递、返回值如何返回。5.2节已经说明哪些寄存器可用于传递参数,哪些寄存器可保存返回值,但这些规则还需要进一步细化。例如,参数数量超过寄存器个数时如何处理,结构体参数如何传递,参数列表同时包含整型、浮点数和结构体时应遵循什么规则等。本节将逐项说明。
6.3.1. 函数参数传递
LoongArch ABI规定,基本数据类型作为函数参数时,使用的寄存器和传递方式会随参数数量和参数类型变化。常见情况如下。
标量作为参数传递
在计算机语言中,标量指不可继续分解的量,对应C语言中的基本数据类型和指针。按照LoongArch ABI,标量参数传递规则如下。
当一个标量位宽不超过XLEN,或一个浮点实数参数不超过FLEN时,使用单个参数寄存器传递;如果没有可用参数寄存器,则通过栈传递。
当一个标量位宽超过XLEN但不超过2×XLEN时,可以使用一对参数寄存器传递。低XLEN位放在编号较小的寄存器中,高XLEN位放在编号较大的寄存器中;如果没有可用参数寄存器,则整个标量通过栈传递;如果只剩一个寄存器可用,则低XLEN位通过寄存器传递,高XLEN位通过栈传递。
如果一个标量宽度大于2×XLEN,则通过引用传递,并在参数列表中用地址替代该参数。通过引用传递的实参可以被被调用方修改。
XLEN表示ABI中的整型寄存器宽度。LP32 ABI中XLEN=32位;LPX32/LP64 ABI中XLEN=64位。FLEN表示ABI中的浮点寄存器宽度,对于LP32、LPX32、LP64 ABI,FLEN均为64位。
表5-5的参数列表中,n表示整型数据类型(包括C语言中的byte、short、int、long),s表示单精度浮点数(float),d表示双精度浮点数(double)。n1,n2,n3分别表示第1、第2、第3个整型参数。
从表5-5可以归纳出基本传参规则:如果实参全是整型,则依次使用整型参数寄存器a0~a7,第1个参数用a0,第2个参数用a1,依此类推,如种类1所示;如果实参全是浮点数,包括单精度和双精度,则依次使用浮点参数寄存器fa0~fa7,如种类2所示;如果整型参数寄存器不够,剩余参数通过函数栈传递,如种类3所示;如果参数列表同时包含整数和浮点数,则分别顺序使用a0~a7和fa0~fa7,如种类4所示;如果浮点参数寄存器不足,而整型参数寄存器仍有空余,则剩余浮点参数可使用整型参数寄存器传递,如种类5所示。若所有参数寄存器都已用完,则通过函数栈传递。这种情况较少见,表5-5未单独列出。
下面通过几个小例子说明。
【例5.1】 整型和指针类型参数传递
ret = strncmp("Hello","Hello World", 5);
被调用函数strncmp有3个实参:两个字符串和一个整数。对应的参数结构和寄存器值如图5-2所示。
【例5.2】 当实参多于8个整型或指针时,将利用函数栈来传递剩余的参数
C语言函数声明和调用语句如下:
int test1(int v0,int v1,int v2,int v3,int v4,int v5, int v6,int v7,int v8,int v9);
test1(0,1,2,3,4,5,6,7,8,9);
被调用函数test1需要接收10个整型(int)参数。按照LoongArch整型寄存器使用约定,a0~a7传递前8个参数0~7,后两个参数8、9只能通过函数栈传递。对应的参数结构和寄存器值如图5-3所示。
注意,这里的sp表示调用者(Caller)函数的栈指针。
【例5.3】 浮点数类型参数的传递
C语言函数声明和调用语句如下:
double fadd(double v1,double v2);
fadd(2.50,5.00);
被调用函数fadd的实参是2个浮点数。其参数结构和寄存器值如图5-4所示。
【例5.4】 超过8个浮点数类型参数的传递
下面的C语言函数声明和调用语句包含10个浮点参数:
float ftest(float v1,float v2,float v3,float v4,float v5,float v6,float v7,float v8,float v9,float v10);
ftest(0.10,1.10,2.10,3.10,4.10,5.10,6.10,7.10,8.10,9.10);
被调用函数ftest的实参是10个浮点数。其参数结构和寄存器值如图5-5所示。
该示例说明,在LoongArch ABI中,当浮点参数寄存器不足而整型参数寄存器仍有剩余时,剩余浮点参数可以通过整型参数寄存器传递。
【例5.5】 __int128类型参数传递(标量等于2×XLEN示例)
使用C++语言函数声明和调用语句如下:
#include <bits/stdc++.h>
void test(__int128 x);
test(0x2221111111111111111);
C++支持128位整型__int128。在LA64架构下,它的位宽等于2×XLEN,因此调用函数test时需要使用2个寄存器传递该参数。其参数结构和寄存器值如图5-6所示。
标量位宽等于2×XLEN的情况并不少见。例如,在LA32架构下,寄存器宽度为32位(4字节),而C语言long long长度为64位(8字节);在LA32和LA64架构下,浮点寄存器宽度均为64位(8字节),而long double长度为128位(16字节)。
聚合体作为参数传递
聚合体与标量相对,指由一个或多个标量组合而成的数据对象,可对应C语言中的结构体、数组等。按照LoongArch ABI,聚合体参数传递规则如下。
如果一个聚合体宽度不超过XLEN位,则可以通过寄存器传递,并且它在寄存器中的字段布局与在内存中的字段布局一致;如果没有可用寄存器,则通过栈传递。
如果一个聚合体宽度超过XLEN但不超过2×XLEN位,则可以通过一对寄存器传递。若只有一个寄存器可用,则聚合体前半部分通过寄存器传递,后半部分通过栈传递;若没有可用寄存器,则整个聚合体通过栈传递。由填充产生的未使用位,以及从聚合体末尾到下一个对齐位置之间的位,取值均未定义。
如果一个聚合体宽度大于2×XLEN位,则通过引用传递,并在参数列表中替换为地址。传递到栈上的聚合体会按类型对齐和XLEN中的较大值对齐,但不超过栈对齐要求。
LoongArch ABI规定位域(Bitfield)按小端顺序排列。跨越其整型类型对齐边界的位域,会从下一个对齐边界开始存放。
对于空结构体(Struct)、联合体(Union)参数或返回值,C编译器会将它们视为非标准扩展并忽略;C++编译器则要求这些类型必须是已分配大小的类型(Sized Type)。
例如,在龙芯64位处理器(XLEN=8)上的C程序中,结构体作为实参时,如果大小等于8字节或16字节,结构体成员会展开到一个或两个参数寄存器中传递,这也称为值传递;如果结构体大于16字节,则把结构体数据存入函数栈,并通过参数寄存器传递其栈位置地址,这也称为引用传递。通过引用传递的实参可以被被调用方修改。
【例5.6】 小于2×XLEN的结构体作为参数传递
struct things{
char v1;
int v2;
int v3;
} = {'a',14,256};
void fun(things);
这里被调用函数fun的实参是结构体things,结构体大小为12字节。其参数结构和寄存器值如图5-7所示。
【例5.7】 大于2×XLEN的结构体作为参数传递
struct things{
char v1;
int v2;
int v3;
long v4;
} = {'a',14,256,6792};
void fun(things);
被调用函数fun的实参仍是结构体things。由于自然对齐要求,结构体大小变为24字节。在LA64架构下,该大小超过2×XLEN,因此只能通过栈传递结构体本体,并通过参数寄存器传递该结构体的起始地址,也就是结构体指针。对应的参数结构和寄存器值如图5-8所示。
当结构体中包含浮点数时,ABI约定与前述规则类似。如果结构体只包含一个浮点实数,则传递方式与单独的浮点实数参数相同。如果结构体只包含两个浮点实数,且二者都不超过FLEN位宽,并且至少有两个浮点参数寄存器可用(寄存器不要求对齐或成对),则该结构体通过两个浮点寄存器传递;否则,按整型调用规范传递。如果结构体只包含一个浮点复数,则传递方式与只包含两个浮点实数的结构体相同,该规则也适用于浮点复数参数。如果结构体只包含一个浮点实数和一个整型(或位域),无论二者顺序如何,只要整型不超过XLEN位宽且未扩展至XLEN位、浮点实数不超过FLEN位宽,并且至少有一个浮点参数寄存器和一个整型参数寄存器可用,就通过一个浮点寄存器和一个整型寄存器传递;否则,按整型调用规范传递。
【例5.8】 结构体位域排列方式示例
struct {int x:10; int y:12;} b1;
struct {short x:10; short y:12;} b2;
这两个结构体最终大小都是32位,但内部数据布局不同。结构体b1中,x位于[0:9],y位于[10:21],高位[22:31]未定义。结构体b2中,x位于[0:9],y位于[16:27],[28:31]和[10:15]未定义。具体如图5-9所示。
可变参数的传递
LoongArch ABI规定,在基本整型调用规范中,可变参数的传递方式与命名参数相同,但存在一个例外:如果某个可变参数需要2×XLEN位对齐,且大小不超过2×XLEN位,则通过一对对齐的寄存器传递,例如寄存器对中的第一个寄存器应为偶数;如果没有可用寄存器,则通过栈传递。一旦某个可变参数开始通过栈传递,后续所有参数也都通过栈传递,即使最后一个参数寄存器可能因对齐寄存器对规则而未被使用。
6.3.2. 函数返回值传递
函数返回值可以是整型、指针、浮点数(单精度或双精度)、结构体(枚举类型归为结构体),也可以没有返回值(void)。LoongArch ABI对返回值传递的规定如下。
当函数没有返回值时,不需要处理返回寄存器。
当返回类型为整型或指针时,返回值放在整型寄存器v0中。
当返回类型为浮点数(单精度或双精度)时,返回值放在浮点寄存器fv0中;当返回类型为
long double时,返回值放在fv0和fv1中。当返回类型为结构体(或枚举类型)时,需要继续根据结构体成员细分:若返回类型是包含一个或两个
float或double成员的结构体,fv0保存第一个成员,fv1保存第二个成员(如果有);若返回类型是包含一个或两个整型成员的结构体,v0保存第一个成员,v1保存第二个成员(如果有);若返回类型大于16字节,则通过引用方式返回。
下面通过几个例子说明。
【例5.9】 返回值类型为int
int fun(){
return 100;
}
函数fun返回int类型,返回值100放入v0即可,如图5-10所示。
【例5.10】 返回值类型为long double
long double func(){
return 3.1415l;
}
函数func返回long double类型,长度为16字节,因此需要fv0和fv1两个寄存器共同保存返回值,如图5-11所示。
【例5.11】 返回值类型为结构体
typedef struct{
char v1;
int v2;
long v3;
}Things;
Things fun(...);
函数fun的返回类型是结构体Things。该结构体包含v1、v2和v3三个成员,且大小不超过16字节。返回时使用v0和v1保存结构体数据,如图5-12所示。
如果结构体大于16字节,则通过引用方式返回数据。C语言示例如下:
typedef struct{
char v1;
int v2;
long v3;
long v4;
}Things;
Things fun(...);
此时结构体Things增加了成员v4,大小超过16字节。返回时使用v0保存结构体数据的地址引用,如图5-13所示。