一、调用约定
函数调用约定是指在不同的编译器中,函数调用时,都会有一套怎样给被调函数传递参数以及怎样从被调函数中返回结果值的约定。这套约定的背后,遵循的原则是:将参数和返回值要么放在栈上,要么放在寄存器和栈的组合上。现代处理器体系结构基本上都是采用寄存器和栈的组合,即给定数目的寄存器用于存放参数和返回值,当寄存器的数目不够用时,再将多出来的参数或返回值放在栈上面。
要注意的是,调用约定并不是编程语言本身规定的内容,而是与实现编程语言的编译器相关的,不同的编译器对函数调用的实现可能是不同的,包括如何传递参数和返回结果,以及如何清理栈,等等,具体该如何定义,还需要结合目标机器的体系结构,做合理高效的定义。
二、整型调用规范
参数传递既有标量类型也有复合类型,首先要区分:
- 标量类型:算术类型和指针类型统称为标量类型,且一份标量类型的数据只能包含一个值。如:整数类型(
int、short、long等)、字符类型(char、wchar_t等)、枚举类型(enum)、浮点类型(float、double等)、布尔类型(bool)以及指针类型(char*)都属于标量类型。 - 复合类型:一份复合类型的数据可以包含多个标量类型的值,也可以包含其他复合类型的值。如:结构体(
struct)、数组、字符串都属于复合类型,注意字符串作为参数时的传递,有字符数组和指针两种方式,注意区分。
整型调用规范约束如下:
-
基本整型调用规范提供了8个参数寄存器
$a0-$a7用于参数传递,前两个参数寄存器$a0和$a1也用于返回值。 -
若一个标量宽度至多
XLEN位(对于LP32 ABI,XLEN=32,对于LPX32/LP64,XLEN=64),则它在单个参数寄存器中传递,若没有可用的寄存器,则在栈上传递。若一个标量宽度超过XLEN位,不超过2*XLEN位,则可以在一对参数寄存器中传递,低XLEN位在小编号寄存器中,高XLEN位在大编号寄存器中;若没有可用的参数寄存器,则在栈上传递标量;若只有一个寄存器可用,则低XLEN位在寄存器中传递,高XLEN位在栈上传递。若一个标量宽度大于2*XLEN位,则通过引用传递,并在参数列表中用地址替换。用栈传递的标量会对齐到类型对齐(Type Alignment)和XLEN中的较大者,但不会超过栈对齐要求。当整型参数传入寄存器或栈时,小于XLEN位的整型标量根据其类型的符号扩展至32位,然后符号扩展为XLEN位。当浮点型参数传入寄存器或栈时,比XLEN位窄的浮点类型将被扩展为XLEN位,而高位为未定义位。 -
若一个聚合体(
Struct或者Array)的宽度不超过XLEN位,则这个聚合体可以在寄存器中传递,并且这个聚合体在寄存器中的字段布局同它在内存中的字段布局保持一致;若没有可用的寄存器,则在栈上传递。若一个聚合体的宽度超过XLEN位,不超过2*XLEN位,则可以在一对寄存器中传递,若只有一个寄存器可用,则聚合体的前半部分在寄存器中传递,后半部分在栈上传递;若没有可用的寄存器,则在栈上传递聚合体。由于填充(Padding)而未使用的位,以及从聚合体的末尾至下一个对齐位置之间的位,都是未定义的。若一个聚合体的宽度大于2*XLEN位,则通过引用传递,并在参数列表中被替换为地址。传递到栈上的聚合体会对齐到类型对齐和XLEN中的较大者,但不会超过栈对齐要求。 -
对于空的结构体(Struct)或联合体(Union)参数或返回值,C编译器会认为它们是非标准扩展并忽略;C++编译器则不是这样,C++编译器要求它们必须是分配了大小的类型(Sized Type)。
-
位域(Bitfield)以小端顺序排列。跨越其整型类型的对齐边界的位域将从下一个对齐边界开始。例如:
// 是一个32位类型,x为9-0位,y为21-10位,31-22位未定义。 struct {int x:10; int y:12;} // 是一个32位类型,x为9-0位,y为27-16位,31-28位和15-10位未定义。 struct {short x:10 ; short y:12;} -
通过引用传递的实参可以由被调用方修改。
-
浮点实数的传递方式与相同大小的聚合体相同,浮点型复数的传递方式与包含两个浮点实数的结构体相同。(当整型调用规范与硬件浮点调用规范冲突时,以后者为准。)
-
在基本整型调用规范中,可变参数的传递方式与命名参数相同,但有一个例外。
2*XLEN位对齐的可变参数和至多2*XLEN位大小的可变参数通过一对对齐的寄存器传递(寄存器对中的第一个寄存器为偶数),如果没有可用的寄存器,则在栈上传递。当可变参数在栈上被传递后,所有之后的参数也将在栈上被传递(此时最后一个参数寄存器可能由于对齐寄存器对的规则而未被使用)。 -
返回值的传递方式与第一个同类型命名参数(Named Value)的传递方式相同。如果这样的实参是通过引用传递的,则调用者为返回值分配内存,并将其地址作为隐式的第一个参数传递。
-
栈向下增长(朝向更低的地址),栈指针应该对齐到一个16字节的边界上作为函数入口。在栈上传递的第一个实参位于函数入口的栈指针偏移量为零的地方,后面的参数存储在更高的地址中。
-
在标准ABI中,栈指针在整个函数执行过程中必须保持对齐。非标准ABI代码必须在调用标准ABI过程之前重新调整栈指针。操作系统在调用信号处理程序之前必须重新调整栈指针;因此,POSIX信号处理程序不需要重新调整栈指针。在服务中断的系统中使用被中断对象的栈,如果连接到任何使用非标准栈对齐规则的代码,中断服务例程必须重新调整栈指针。但如果所有代码都遵循标准ABI,则不需要重新调整栈指针。
-
函数所依赖的数据必须位于函数栈帧范围之内。
-
被调用的函数应该负责保证寄存器
$s0-$s8的值在返回时和入口处一致。
三、浮点调用规范
浮点调用规范约束如下:
-
浮点参数寄存器共8个,为
$fa0-$fa7,其中$fa0和$fa1也用于传递返回值。需要传递的值在任何可能的情况下都可以传递到浮点寄存器中,与整型参数寄存器$a0-$a7是否已经用完无关。 -
本节其他部分仅适用于命名参数,可变参数根据整型调用规范传递。
-
在本节中,
FLEN指的是ABI中的浮点寄存器的宽度。ABI的FLEN宽度不能比指令系统的标准宽。 -
若一个浮点实数参数不超过
FLEN位宽,并且至少有一个浮点参数寄存器可用,则将这个浮点实数参数传递到浮点参数寄存器中,否则,它将根据整型调用规范传递。当一个比FLEN位更窄的浮点参数在浮点寄存器中传递时,它从1扩展到FLEN位。 -
若一个结构体只包含一个浮点实数,则这个结构体的传递方式同一个独立的浮点实数参数的传递方式一致。若一个结构体只包含两个浮点实数,这两个浮点实数都不超过
FLEN位宽并且至少有两个浮点参数寄存器可用(寄存器不必是对齐且成对的),则这个结构体被传递到两个浮点寄存器中,否则,它将根据整型调用规范传递。若一个结构体只包含一个浮点复数,则这个结构体的传递方式同一个只包含两个浮点实数的结构体的传递方式一致,这种传递方式同样适用于一个浮点复数参数的传递。若一个结构体只包含一个浮点实数和一个整型(或位域),无论次序,则这个结构体通过一个浮点寄存器和一个整型寄存器传递的条件是,整型不超过XLEN位宽且没有扩展至XLEN位,浮点实数不超过FLEN位宽,至少一个浮点参数寄存器和至少一个整型参数寄存器可用,否则,它将根据整型调用规范传递。 -
返回值的传递方式与传递第一个同类型命名参数的方式相同。
-
若浮点寄存器
$fs0-$fs11的值不超过FLEN位宽,那么在函数调用返回时应该保证它们的值和入口时一致。
下图程序用gcc -O2 fun.c -S得到汇编文件。对于第9个浮点参数,已经没有浮点参数寄存器可用,此时根据浮点调用规范第4条,剩下的参数按整型调用规范传递。因此,a9、a10、a11和a12分别用$a0-$a3这四个定点寄存器来传递,虽然这段代码引用的a9和a11实际上是浮点数。
// fun.c
extern abort();
int fun (double a1, double a2, double a3, double a4,
double a5, double a6, double a7, double a8, double a9, int a10, double a11, int a12){
if (a9 != a11)
abort();
return 0;
}
//gcc -O2 fun.c -S 生成汇编文件
fun:
movgr2fr.d $f0,$r4 // &f0是参数a9,从&r4获得
movgr2fr.d $f1,$r6 // &f1是参数a11,从&r6获得
fcmp.ceq.d $fcc0,$f0,$f1
bceqz $fcc0,.L8
or $r4,$r0,$r0
jr $r1
.align 3,54525952,4
.L8:
addi.d $r3,$r3,-16
st.d $r1,$r3,8
bl %plt(abort)
ld.d $r1,$r3,8
or $r4,$r0,$r0
addi.d $r3,$r3,16
jr $r1
下例是一个可变数量参数的情况,第一个固定参数是浮点参数,用$fa0,后续的可变参数根据浮点调用规范第2条全部按整型调用规范传递,因此不管是浮点还是定点参数,都使用定点寄存器。
struct Ss {
char c1, c2;
} a3 = {3, 4};
int fun (double a1, ...);
int test () {
return fun (1, (float) 2, a3, (long double) 5, (float) 6, (short) 7, (int) 8, (float) 9);
}
// 对应的参数传递
-----------------------------------------
low high
+-----------------------+
8 $a7 | promoted (double) 9 |
+-----------------------+
7 $a6 | 8 |
+-----------------------+
6 $a5 | promoted 7 |
+-----------------------+
5 $a4 | promoted (double) 6 |
+-----------------------+
4 $a3 | (long double) 5 high |
+-----------------------+
3 $a2 | (long double) 5 low |
+-----------------------+
2 $a1 | 3| 4| padding |
+-----------------------+
1 $a0 | promoted (double) 2 |
+-----------------------+
0 $fa0 | (double) 1 |
+-----------------------+
【参考书籍】
《计算机体系结构基础》第3版
《深入理解计算机系统》第3版
《计算机体系结构量化研究方法》第5版
本文详细介绍了函数调用约定的概念,包括整型和浮点型的调用规范,阐述了参数如何传递及返回值如何处理,并针对不同类型的参数给出了具体的传递规则。
1万+

被折叠的 条评论
为什么被折叠?



