正如RISC-V调用约定文档所说:
当在堆栈上传递两倍于指针字大小的原始参数时,它们是 自然对齐。当它们被传递到整数寄存器时,它们驻留在偶数-奇数对齐的位置 寄存器对,偶数寄存器保存最低有效位。以 RV32 为例, 函数 void foo(int, long long) 的第一个参数在 a0 中传递,第二个参数在 a2 中传递, a3. a1 中没有传递任何内容。
为什么不直接使用
a1
和 a2
而不是 a2
和 a3
?所以我们可以通过寄存器传递更多的参数。
通常,这样做是为了让简单的代码可以将所有寄存器刷新到内存并获得您所期望的内容,就好像内存首先被用来传递对齐良好的参数一样——这对于可变参数来说通常很重要功能。
但是,我们应该注意,对于非可变参数函数,clang 和 gcc 都没有直接遵循这一点。 他们都使用
a0
表示 int
和 a1
,a2
表示 long long
(其中 a1
是 a2
的低阶,long long
是高阶)。
long long square(int num, long long foo) {
return foo + 100;
}
结果
square:
addi a0, a1, 100
sltu a1, a0, a1
add a1, a1, a2
ret
铿锵:https://godbolt.org/z/9Pez4r
海湾合作委员会:https://godbolt.org/z/b4dMsr
只有当我们使用可变参数时,我们才会看到编译器跳过
a1
:
long long square(int num, ...);
int test () {
square ( 100, (long long) 200 );
}
结果
test:
addi a0, zero, 100
addi a2, zero, 200
mv a3, zero
tail square
就其价值而言,该文档是 2015 年的早期规范,并且显然已已弃用(尽管它似乎是 Google 首次针对“RISC-V 调用约定”进行的攻击)。 最新的调用约定已删除了奇偶寄存器对齐的要求,因此标准行为正是您所建议的:
2×XLEN 位宽的标量在一对参数寄存器中传递,低序 XLEN 位在较低编号的寄存器中,高序 XLEN 位在较高编号的寄存器中。如果没有可用的参数寄存器,则标量按值在堆栈上传递。如果只有一个寄存器可用,则低阶 XLEN 位将在寄存器中传递,高阶 XLEN 位将在堆栈中传递。
但有一个例外:该规则仍然适用于 variadic 参数:
在基本整数调用约定中,可变参数的传递方式与命名参数相同,但有一个例外。具有 2×XLEN 位对齐且大小最多为 2×XLEN 位的可变参数在 aligned 寄存器对中传递(即,该对中的第一个寄存器是偶数编号),或者如果没有,则按值传递到堆栈可用。
因此,如果函数被声明为
int f(int x, long long y)
,那么它将使用 a0
表示 x
,使用 {a2,a1}
表示 y
,有效地仅使用三个寄存器 a0-a2
,但如果它声明为 int f(int x, ...)
(并传递一个 int 和一个 long long),int 将使用 a0
,long long 将使用 {a3,a2}
,并且 a1
将被跳过。
至于为什么会存在这样的奇偶对齐寄存器对的要求,这些是被一些扩展使用的,例如Zilsd扩展(RV32中的64位加载/存储),其中RV32的32个32位寄存器出于此扩展的目的,被视为 16 个 64 位寄存器(每个寄存器对应一对 32 位寄存器的奇偶数)。 因此,一般来说,当涉及这些扩展时,将 2×XLEN 位变量存储在奇偶对齐寄存器对中更为实用。