riscv架构printf函数传参研究

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 2XLEN 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 2XLEN-bit alignment and size at most 2XLEN 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 2XLEN 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寄存器传递

参数,全部使用寄存器传参,而不使用栈。提高了效率。

此条目发表在RISCV分类目录。将固定链接加入收藏夹。

发表评论

电子邮件地址不会被公开。