I remember reading that in fact it was an important improvement from ARM32 to Aarch64 to have a dedicated stack register.
Not surprisingly Risc-V has also a dedicated stack register.
RV32I, RV64I, and RV128I (the base RISC-V architecture) don't have a dedicated stack register in the instruction set, and I'm not even sure if your code will even run faster on fancy implementations if you use x2 as the stack pointer in the standard way. However, the compressed instruction extension has "compressed" (16-bit-long) instructions that implicitly index from x2: C.LWSP, C.LDSP (on RV64C and theoretically RV128C), C.LQSP (RV128C only), C.FLWSP, and C.FLDSP; and corresponding store instructions. These instructions incorporate a 6-bit immediate offset field which is added to x2 to form the effective address.
As far as I know, that's the full extent to which RISC-V has a dedicated stack register: it has a compressed instruction format that uses x2 as a base register, but not in the base ISA, just a standard extension. There's no dedicated PUSH or POP instruction, no dedicated instruction for storing the link register into the stack, no dedicated instructions for incrementing or decrementing x2 (you do that with ADDI, which can be compressed as C.ADDI as long as the stack frame size is less than 32 bytes, which means it has to be 16 bytes in the standard ABI), not even autoincrement and autodecrement addressing modes.
Too lazy to look this up (or even figure out the relevant extension) but the obvious question to me would be how storing state for interrupts is handled?
They call the interrupt mechanism "traps", reserving "interrupt" for traps caused by asynchronous inputs, and (in recent versions of RISC-V) they're specified in the separate "The RISC-V Instruction Set Manual, Volume II: Privileged Architecture". I'm looking at version 1.12 (20211203).
Basically there are special registers for trap handling, which are CSRs: xscratch (a scratch register), xepc (the trapping program counter), xcause and xtval (which trap), and xip (interrupts pending). These come in four sets: x=s (supervisor-mode), x=m (machine-mode, with a couple of extras), x=h (hypervisor mode, which has some differences), and x=vs (virtual supervisor). You can't handle traps in U-mode, so in a RISC-V processor with trap handling and without multiple modes, you're always in M-mode. (See p.3, 17/155.)
I haven't done this but I suppose that what you're supposed to do in a mode-X trap handler is start by saving some user register to xscratch, then load a useful pointer value into that user register off which you can index to save the remaining user registers to memory.
I guess you know xscratch (and xepc, etc.) wasn't previously being used because you only use them during this very brief time and leave x-mode traps disabled until you finish using it. If all your traps are "vertical" (from a less-privileged mode like U-mode into a more-privileged mode like M-mode) you don't have to worry about this, because you'll never have another x-mode trap while running your x-mode trap handler.
I should probably check out how FreeBSD and Linux handle system calls on RV64.
dh` explained the following technique to me, as explained to him by jrtc27: upon entry to, say, an S-mode trap handler, you use CSRRW to swap the stack pointer in x2 with the sscratch register, if it's null you swap back, then push all the registers on the stack, then you can do real work.
> Not surprisingly Risc-V has also a dedicated stack register.
I don't think this is correct. It has a suggested SP register, which merely gets some special support in the C subset. But that's just an optional compression scheme and not really part of the ISA design.