riscvriscv32

What determines how a 64-bit function argument is split between registers on RISC-V 32?


I'm currently trying to learn risc-v by use of the book "RISC-V Assembly Language Programming Using the ESP32-C3 and QEMU" by Warren Gay. In the book it states that when a 64-Bit value is passed on a 32-Bit platform the low-order 32 bits are loaded into an even numbered argument register and the high-order bits are passed into the next odd-numbered register. They also mention the example of how if an int followed by a long long is passed the int goes into register a0, the low-order 32 bits of the long long go to a2 and the high-order 32 bits go into a3. (Which is also the same example of a RISC-V Doc I found - page 2 here).

Then after explaining how arguments need to be stored on the stack if there are too many to fit in the registers there is an example program (main c program that calls an external function defined in RISC-V assembly with 9 arguments which alternate between int32_t and int64_t starting with int32_t). The whole program can also be found here. Just for context, the program's goal is to provide a function that returns the sum of the low order 32 bits (or in the case of int32_t all bits) of the function parameters. Inside of the RISC-V assembly code there is the following passage:

        add     a0,a0,a1        # Add low order arg2 (int64) to arg1
        add     a0,a0,a3        # Add arg3 to arg1
        add     a0,a0,a4        # Add low order arg4 (int64) to arg1
        add     a0,a0,a6        # Add arg5 to arg1
        add     a0,a0,a7        # Add low order arg6 (int64) to arg1

If I understood the given example of the book and the RISC-V Calling Convention this should be similar because this is also a function call where the first argument is an int and the second argument is a long long. In my mind this should mean that arg1 is stored in a0, a1 is empty, arg2 is stored in a2 (low-order) and a3 (high-order) respectively, arg3 is stored in a4, etc... Yet it looks like the program takes the low order bits of arg2 and arg6 out of odd-numbered registers which goes against the information I could find in the book and from the risc-v website.

On the other hand while looking for the solution for this question I found some info on riscv calling conventions on the RISC-V GitHub Non-ISA Specifications page under "Integer Calling Convention". There it states "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.". Below that there is an example of variadic arguments that aligns with the examples mentioned earlier, but based on the description, could it be that the arguments above are handled like scalars? (whatever that means in the context of RISC-V)

Sorry if this question sounds stupid, I'm just now starting to learn RISC-V and it's hard to try to build a foundation when there's information that seems contradicting.

I searched the Internet for an answer to this question. I expected the arguments to be stored in the registers as described yet it seems like that's not how it works.


Solution

  • did you just try it and look at the output?

    void fun ( 
    unsigned int,
    unsigned long long,
    unsigned int,
    unsigned long long,
    unsigned int,
    unsigned long long,
    unsigned int,
    unsigned long long,
    unsigned int,
    unsigned long long,
    unsigned int
    );
    
    void fun0 ( void )
    {
        fun(1,2,3,4,5,6,7,8,9,10,11);
    }
    
    Disassembly of section .text:
    
    00000000 <fun0>:
       0:   fc010113            addi    sp,sp,-64
       4:   00b00793            li  a5,11
       8:   02f12023            sw  a5,32(sp)
       c:   00000793            li  a5,0
      10:   00f12e23            sw  a5,28(sp)
      14:   00900793            li  a5,9
      18:   00f12823            sw  a5,16(sp)
      1c:   00a00713            li  a4,10
      20:   00000793            li  a5,0
      24:   00e12c23            sw  a4,24(sp)
      28:   00f12623            sw  a5,12(sp)
      2c:   00800713            li  a4,8
      30:   00700793            li  a5,7
      34:   00e12423            sw  a4,8(sp)
      38:   00f12223            sw  a5,4(sp)
      3c:   00012023            sw  zero,0(sp)
      40:   00600893            li  a7,6
      44:   00500813            li  a6,5
      48:   00400713            li  a4,4
      4c:   00000793            li  a5,0
      50:   00300693            li  a3,3
      54:   00200593            li  a1,2
      58:   00000613            li  a2,0
      5c:   00100513            li  a0,1
      60:   02112e23            sw  ra,60(sp)
      64:   00000097            auipc   ra,0x0
      68:   000080e7            jalr    ra # 64 <fun0+0x64>
      6c:   03c12083            lw  ra,60(sp)
      70:   04010113            addi    sp,sp,64
      74:   00008067            ret
    

    a0,a1,a3,a4,a6,a7

    unsigned int,       a0
    unsigned long long, a2,a1
    unsigned int,       a3
    unsigned long long, a5,a4
    unsigned int,       a6
    unsigned long long, stack,a7
    unsigned int,       stack
    unsigned long long, stack
    unsigned int,       stack
    unsigned long long, stack
    unsigned int        stack
    

    The lesson here is actually use the compiler and see what it is doing. In this case:

    -march=rv32i -mabi=ilp32
    

    From the gnu docs at least

    The valid calling conventions are: ‘ilp32’, ‘ilp32f’, ‘ilp32d’, ‘lp64’, ‘lp64f’, and ‘lp64d’.

    So it is not like I can try another. (f float and d double I assume)

    Calling convention specs are definitely valuable, but if you want to mix C and assembly (or compiled language and assembly). Burn the candle from both ends to find the middle. End of the day what your toolchain is producing is the only thing that will work and you have to match that. Trying to understand why it is doing what it is doing goes back up into the spec. Generally you can tell from compiled code what is going on and do not need to refer to any spec, or perhaps a book or web page or SO answer, etc may have some summary.

    The compiler you are using that day with the settings you are using that day defines the calling convention. Compiler authors are in no way, shape, or form required to conform to an IP vendor nor other calling conventions, they are free to do whatever they want (so long as it works obviously). If they use a spec and how some of these are written no reason to assume that any two compiler vendors interpret the spec the same way, if they choose to use it, etc, etc, etc.

    clang seems to use the same abi, some minimal searches look like they match gnu, but you do not have to specify the abi on the command line.

    Disassembly of section .text:
    
    00000000 <fun0>:
       0:   fd010113            addi    sp,sp,-48
       4:   02112623            sw  ra,44(sp)
       8:   02812423            sw  s0,40(sp)
       c:   03010413            addi    s0,sp,48
      10:   00010513            mv  a0,sp
      14:   00b00593            li  a1,11
      18:   02b52023            sw  a1,32(a0)
      1c:   00000793            li  a5,0
      20:   00f52e23            sw  a5,28(a0)
      24:   00a00593            li  a1,10
      28:   00b52c23            sw  a1,24(a0)
      2c:   00900593            li  a1,9
      30:   00b52823            sw  a1,16(a0)
      34:   00f52623            sw  a5,12(a0)
      38:   00800593            li  a1,8
      3c:   00b52423            sw  a1,8(a0)
      40:   00700593            li  a1,7
      44:   00b52223            sw  a1,4(a0)
      48:   00f52023            sw  a5,0(a0)
      4c:   00100513            li  a0,1
      50:   00200593            li  a1,2
      54:   00300693            li  a3,3
      58:   00400713            li  a4,4
      5c:   00500813            li  a6,5
      60:   00600893            li  a7,6
      64:   00078613            mv  a2,a5
      68:   00000097            auipc   ra,0x0
      6c:   000080e7            jalr    ra # 68 <fun0+0x68>
      70:   02c12083            lw  ra,44(sp)
      74:   02812403            lw  s0,40(sp)
      78:   03010113            addi    sp,sp,48
      7c:   00008067            ret
    

    a0,a1,a3,a4,a6,a7...