printf函数,作为一个经典的函数,经常在c程序中,被使用。
printf函数和一般函数的区别在于,参数不定,因此要使用可变参数方式传参。从网上查阅的资料,得知,可变参数传参,是使用栈进行传参。将参数,压入栈中,后续使用参数,再从栈中将参数给弹出来。
但是在riscv架构中,上述规则是不成立的。通过网上查阅资料,以及自己试验,得出了riscv在可变参数传参时,传参的规则。
在github上,riscv-elf-psabi-doc仓库,有介绍riscv架构的ABI规则。链接如下:
https://github.com/riscv/riscv-elf-psabi-doc/blob/master/riscv-elf.md
一、ABI传参规则
在里面,提到了ABI规则下,寄存器传参的规则:
name |
ABI助记符 |
意义 |
x0 |
zero |
zero |
x1 |
ra |
返回地址 |
x2 |
sp |
栈指针 |
x3 |
gp |
全局指针 |
x4 |
tp |
线程指针 |
x5-x7 |
t0-t2 |
临时寄存器 |
x8-x9 |
s0-s1 |
callee保存的寄存器 |
x10-x17 |
a0-a7 |
参数寄存器 |
x18-x27 |
s2-s11 |
callee保存的寄存器 |
x28-x31 |
t3-t6 |
临时寄存器 |
所以可以看出,a0-a7这几个寄存器,用来传递参数。
在文档中,有描述:
The base integer calling convention provides eight argument registers, a0-a7, the first two of which are also used to return values. Scalars that are at most XLEN bits wide are passed in a single argument register, or on the stack by value if none is available. When passed in registers, scalars narrower than XLEN bits are widened according to the sign of their type up to 32 bits, then sign-extended to XLEN bits. Scalars that are 2✕XLEN bits wide are passed in a pair of argument registers, with the low-order XLEN bits in the lower-numbered register and the high-order XLEN bits in the higher-numbered register. If no argument registers are available, the scalar is passed on the stack by value. If exactly one register is available, the low-order XLEN bits are passed in the register and the high-order XLEN bits are passed on the stack. |
riscv提供了8个寄存器,用于传参:
-
第一个参数,用a0传递
-
第二个参数,用a1传递
-
第三个参数,用a2传递
-
第四个参数,用a3传递
-
第五个参数,用a4传递
-
第六个参数,用a5传递
-
第七个参数,用a6传递
-
第八个参数,用a7传递
-
之后的参数,使用栈传递
对于程序返回值,使用a0和a1传递。
如果传参变量宽度为2* XLEN,那么使用成对的2个寄存器来传参,低寄存器,保存变量的第32位,高寄存器,用于保存变量的高32位。
但是可变参数的传参,有如下说明:
in the base integer calling convention, variadic arguments are passed in the same manner as named arguments, with one exception. Variadic arguments with 2✕XLEN-bit alignment and size at most 2✕XLEN bits are passed in an aligned register pair (i.e., the first register in the pair is even-numbered), or on the stack by value if none is available. After a variadic argument has been passed on the stack, all future arguments will also be passed on the stack (i.e. the last argument register may be left unused due to the aligned register pair rule). |
对于可变参数的参数,如果参数宽度为 2 * XLEN, 那么使用的成对寄存器,低寄存器必须是偶数寄存器,高寄存器为奇数寄存器。
对于可变参数,是通过va_list,获取可变参数。riscv对va_list,有如下有些说明:
The va_list type is void*. A callee with variadic arguments is responsible for copying the contents of registers used to pass variadic arguments to the vararg save area, which must be contiguous with arguments passed on the stack. The va_start macro initializes its va_list argument to point to the start of the vararg save area. The va_arg macro will increment its va_list argument according to the size of the given type, taking into account the rules about 2✕XLEN aligned arguments being passed in "aligned" register pairs. |
从描述,可以得出,va_list在被处理时,会将参数寄存器,拷贝到内部的 vararg save area。因此可以得出,在riscv架构,可变参数的传参,主要是通过寄存器来传递的。多的参数,才通过栈传递。
二、RV32浮点传参
先看传递浮点数,代码如下:
int main() { float a = 2.3; float b = 4.54; float c = 4.54;i float d = 4.54; float e = 4.54; float f = 4.54; printf("arg value : %lf, %lf, %lf, %lf, %lf, %lf", a, b, c, d, e, f); }
使用riscv32-gcc编译器编译,其对应汇编:
1019c: e707a787 flw fa5,-400(a5) # 1de70 <__clzsi2+0x92> 101a0: fef42627 fsw fa5,-20(s0) 101a4: 67f9 lui a5,0x1e 101a6: e747a787 flw fa5,-396(a5) # 1de74 <__clzsi2+0x96> 101aa: fef42427 fsw fa5,-24(s0) 101ae: 67f9 lui a5,0x1e 101b0: e747a787 flw fa5,-396(a5) # 1de74 <__clzsi2+0x96> 101b4: fef42227 fsw fa5,-28(s0) 101b8: 67f9 lui a5,0x1e 101ba: e747a787 flw fa5,-396(a5) # 1de74 <__clzsi2+0x96> 101be: fef42027 fsw fa5,-32(s0) 101c2: 67f9 lui a5,0x1e 101c4: e747a787 flw fa5,-396(a5) # 1de74 <__clzsi2+0x96> 101c8: fcf42e27 fsw fa5,-36(s0) 101cc: 67f9 lui a5,0x1e 101ce: e747a787 flw fa5,-396(a5) # 1de74 <__clzsi2+0x96> 101d2: fcf42c27 fsw fa5,-40(s0) 101d6: fec42787 flw fa5,-20(s0) 101da: 42078653 fcvt.d.s fa2,fa5 101de: fe842787 flw fa5,-24(s0) 101e2: 420785d3 fcvt.d.s fa1,fa5 101e6: fe442787 flw fa5,-28(s0) 101ea: 42078553 fcvt.d.s fa0,fa5 101ee: fe042787 flw fa5,-32(s0) 101f2: 420787d3 fcvt.d.s fa5,fa5 101f6: fdc42707 flw fa4,-36(s0) 101fa: 42070753 fcvt.d.s fa4,fa4 101fe: fd842687 flw fa3,-40(s0) 10202: 420686d3 fcvt.d.s fa3,fa3 10206: a836 fsd fa3,16(sp) // 变量 f写入到栈 10208: a43a fsd fa4,8(sp) // 变量 e写入到栈 1020a: a03e fsd fa5,0(sp) // 变量 d写入到栈 1020c: faa43427 fsd fa0,-88(s0) 10210: fa842803 lw a6,-88(s0) // 获取 c变量低32位 10214: fac42883 lw a7,-84(s0) // 获取 c 变量高32位 10218: fab43427 fsd fa1,-88(s0) 1021c: fa842703 lw a4,-88(s0) // 获取 b变量低32位 10220: fac42783 lw a5,-84(s0) // 获取 b 变量高32位 10224: fac43427 fsd fa2,-88(s0) 10228: fa842603 lw a2,-88(s0) //获取a变量低32位 1022c: fac42683 lw a3,-84(s0) //获取a变量高32位 10230: 65f9 lui a1,0x1e 10232: e2058513 addi a0,a1,-480 # 1de20 <__clzsi2+0x42> 10236: 22c5 jal 10416 <printf> |
printf函数,对于单精度变量,会转换成双精度变量,进行传递。因为汇编的前面很大一部分,是在做这个操作。
关键在于最后的load。
-
printf字符串地址,使用a0寄存器传递
-
a变量,使用a2,a3寄存器传递,因为a是64位,使用的寄存器起始必须是偶数寄存器,所以从a2开始分配,跳过a1寄存器。
-
b变量,使用a4,a5寄存器传递
-
c变量,使用a6,a7寄存器传递
-
d变量,使用栈传递
-
e变量,使用栈传递
-
f变量,使用栈传递
所以,a1寄存器,在这里,没有用到传参功能
三、RV32定点传参
定点传参,c代码如下:
int main() { long long a0=5; int b0=6; long long c0=7; long long d0=8; int e0=9; int f0=10; printf("arg value : %d, %ld, %d, %d, %d, %d", a0, b0, c0, d0, e0, f0); }
使用riscv32-gcc编译,其反汇编如下:
10246: 4795 li a5,5 10248: 4801 li a6,0 1024a: fcf42823 sw a5,-48(s0) 1024e: fd042a23 sw a6,-44(s0) 10252: 4799 li a5,6 10254: fcf42623 sw a5,-52(s0) 10258: 479d li a5,7 1025a: 4801 li a6,0 1025c: fcf42023 sw a5,-64(s0) 10260: fd042223 sw a6,-60(s0) 10264: 47a1 li a5,8 10266: 4801 li a6,0 10268: faf42c23 sw a5,-72(s0) // 保存 变量 d0 1026c: fb042e23 sw a6,-68(s0) 10270: 47a5 li a5,9 10272: faf42a23 sw a5,-76(s0) // 保存 变量e0 10276: 47a9 li a5,10 10278: faf42823 sw a5,-80(s0) // 保存 变量 f0 1027c: fb042783 lw a5,-80(s0) 10280: c63e sw a5,12(sp) // 变量 f0 压栈 10282: fb442783 lw a5,-76(s0) 10286: c43e sw a5,8(sp) // 变量 e0 压栈 10288: fb842783 lw a5,-72(s0) 1028c: fbc42803 lw a6,-68(s0) 10290: c03e sw a5,0(sp) // 变量d0 低32位压栈 10292: c242 sw a6,4(sp) // 变量 d0 高32位压栈 10294: fc042803 lw a6,-64(s0) // 变量c0的低32位 10298: fc442883 lw a7,-60(s0) // 变量 c0的高32位 1029c: fcc42703 lw a4,-52(s0) // 变量b0 102a0: fd042603 lw a2,-48(s0) // 变量a0的低32位 102a4: fd442683 lw a3,-44(s0) // 变量a0的高32位 102a8: 67f9 lui a5,0x1e 102aa: e4c78513 addi a0,a5,-436 # 1de4c <__clzsi2+0x6e> 102ae: 22a5 jal 10416 <printf> |
从反汇编可以知道
-
printf字符串地址,使用a0寄存器传递
-
a0变量,使用a2,a3寄存器传递,因为a0变量是64位,使用的寄存器起始必须是偶数寄存器,所以从a2开始分配,逃过a1寄存器
-
b0变量,使用a4寄存器传递
-
c0变量,使用a6,a7寄存器传递,因为c0变量是64位,使用的寄存器起始必须是偶数寄存器,所以从a6开始分配,跳过a5寄存器
-
d0变量,使用栈传递
-
e0变量,使用栈传递
-
f0变量,使用栈传递
所以,a1寄存器和a5寄存器,在这里,没用用到传参功能。
四、RV64浮点传参
RV64下,寄存器位宽变为64位,因此传递一个64位值,不在需要两个寄存器进行拼接,而可以直接使用一个寄存器。所以反汇编会更简洁。
对于之前的浮点c程序,使用riscv64-gcc编译,其反汇编如下:
1024e: e2078853 fmv.x.d a6,fa5 10252: e20507d3 fmv.x.d a5,fa0 10256: e2058753 fmv.x.d a4,fa1 1025a: e20606d3 fmv.x.d a3,fa2 1025e: e2068653 fmv.x.d a2,fa3 10262: e20705d3 fmv.x.d a1,fa4 10266: 6571 lui a0,0x1c 10268: 08050513 addi a0,a0,128 # 1c080 <__clzdi2+0x3a> 1026c: 1be000ef jal ra,1042a <printf> |
可以看出,直接把浮点寄存器的值,传递给定点寄存器:
-
printf字符串地址,使用a0寄存器传递
-
a变量,使用a1寄存器传递
-
b变量,使用a2寄存器传递
-
c变量,使用a3寄存器传递
-
d变量,使用a4寄存器传递
-
e变量,使用a5寄存器传递
-
f变量,使用a6寄存器传递
参数,全部使用寄存器传参,而不使用栈。提高了效率。
五、RV64定点传参
RV64下,寄存器位宽变为64位,因此传递一个64位值,不在需要两个寄存器进行拼接,而可以直接使用一个寄存器。所以反汇编会更简洁。
对于之前的定点c程序,使用riscv64-gcc编译,其反汇编如下:
102a4: fb042703 lw a4,-80(s0) 102a8: fb442783 lw a5,-76(s0) 102ac: fcc42603 lw a2,-52(s0) 102b0: 883a mv a6,a4 102b2: fb843703 ld a4,-72(s0) 102b6: fc043683 ld a3,-64(s0) 102ba: fd043583 ld a1,-48(s0) 102be: 6571 lui a0,0x1c 102c0: 0b050513 addi a0,a0,176 # 1c0b0 <__clzdi2+0x6a> 102c4: 166000ef jal ra,1042a <printf> |
可以看出:
-
printf字符串地址,使用a0寄存器传递
-
a0变量,使用a1寄存器传递
-
b0变量,使用a2寄存器传递
-
c0变量,使用a3寄存器传递
-
d0变量,使用a4寄存器传递
-
e0变量,使用a5寄存器传递
-
f0变量,使用a6寄存器传递
参数,全部使用寄存器传参,而不使用栈。提高了效率。