assemblylinux-kernelsystem-callsriscvcalling-convention

Do RISC-V kernel-space calling conventions exist?


RISC-V user-space function calls calling conventions are clear to me. It's also clear that a0-a5 are used to pass arguments to kernel and a7 to store system call number before an ecall. What I can't find is some documentation about assumptions I can do after execution of ecall instruction.
The following lines are part of the output of objdump -d -M no-aliases libc.so.6 in the <__open64_nocancel> (glibc 2.35 on RISC-V Ubuntu 22, but the same thing happens on 2.37 and 2.39)

   ...
   ada12:       861a                    c.mv    a2,t1
   ada14:       00000073                ecall
   ada18:       77fd                    c.lui   a5,0xfffff
   ada1a:       02a7e963                bltu    a5,a0,ada4c <__open64_nocancel+0x7e>
   ada1e:       2501                    c.addiw a0,0
   ada20:       6722                    c.ldsp  a4,8(sp)
   ada22:       000e3783                ld      a5,0(t3)
   ...

As you can see the last instruction uses t3 as base address to access memory without being reinitialized after the ecall. My question is: what can I safely assume from this?
Can caller-saved registers from user-space calling conventions be safely used after ecall without saving and restoring the value across ecall? I know a0-a1 are used to store return values from the system call, but what about a2-a7? May I assume they are clobbered?

I was expecting that all temporary registers would be considered clobbered, but the given example makes me wonder about argument registers. Maybe that's different since they serve a specific purpose to the kernel.


Solution

  • Registers other than the return-value are unmodified by system-calls on Linux, except for a few special cases on some ISAs (like x86-64 syscall itself clobbers RCX and R11).

    The kernel system-call entry points save all user-space registers so ptrace (debugging) and other things that want a struct of the user-space regs can just work on a process that's blocked in a system call.


    The syscall(2) man page (https://man7.org/linux/man-pages/man2/syscall.2.html) has a table of register usage, with a note at the bottom:

    Note that these tables don't cover the entire calling convention - some architectures may indiscriminately clobber other registers not listed here.

    As far as I know, there aren't any extra clobbers for Linux's RISC-V system-call calling convention, just the return value in a0.

    Linux system-call retvals are only ever 1 register wide; I don't think there are any that return 64-bit on a 32-bit machine, so a1 is never part of the return value the way it would be in user-space. System calls like __NR_lseek only take 32-bit off_t on 32-bit systems (at least on i386 where I tested); you need __NR__llseek for off64_t 64-bit file positions. Instead of returning the new file position, it takes a pointer where it stores the result.

    And system calls like gettimeofday which deal with times take pointers, although time(2) returns time_t. But in the kernel ABI, time_t is still 32-bit on 32-bit systems; new system-calls were added for 64-bit times. (Is there any way to get 64-bit time_t in 32-bit programs in Linux?)


    On some ISAs including MIPS and SPARC, the kernel ABI for pipe(2) returns the two fds by value in two return-value registers, but RISC-V is not one of those ISAs. It only ever uses a single return-value register from system calls, as documented by syscall(2).
    (Thanks to @MarcoBonelli for pointing out this bit of documentation)

    Linux on some ISAs returns error status out-of-band, in a second (or third) register, like Alpha, MIPS, and PowerPC, instead of encoding it as -errno in the main return value with values unsigned-above -4096 being negated errno codes like -EFAULT. But RISC-V is not one of those ISAs either, so only ever a single register a0 is written as a system call return value. (Fun fact: FreeBSD / macOS use a separate error status on x86, in the carry flag, unlike Linux.)