A quick reference for the X86_64, AARCH64, and RISCV64 ISAs.
| RISCV64 | AARCH64 | X86_64 | |
|---|---|---|---|
| General Purpose | X1-X31 | X0-X30 | RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8-R15 |
| Program Counter | PC | PC | RIP |
| Return Address | RA (X1) | LR (X30) | On Stack |
| Stack Pointer | SP (X2) | SP | RSP |
| Frame Pointer | FP (X8/ S0) | FP (X29) | RBP |
| Zero Register | ZERO (X0) | XZR / WZR | -- |
| Segmentation | -- | -- | CS, SS, DS, ES, FS, GS |
| RISCV64 | AARCH64 | X86_64 | |
|---|---|---|---|
| Arguments | A0-A7 (X10-X17) | X0-X7 | RDI, RSI, RDX, RCX, R8, R9 |
| Return Value | A0 (X10) | X0 | RAX |
| Callee-Saved | S0-S11, SP (X2) | X19-X30, SP | RBX, RBP, R12-R15, RSP |
| Caller-Saved | RA (X1), A0-A7, T0-T6 | X0-X18 | RAX, RCX, RDX, RSI, RDI, R8-R11 |
| RISCV64 | AARCH64 | X86_64 | |
|---|---|---|---|
| User Mode | U-Mode | EL0 | Ring 3 |
| Kernel Mode | S-Mode | EL1 | Ring 0 |
| Hypervisor Mode | -- | EL2 | -- |
| Machine / Firmware Mode | M-Mode | EL3 | -- |
| RISCV64 | AARCH64 | X86_64 | |
|---|---|---|---|
| Interrupts Enabled? | sstatus.sie, sie | DAIF (I,F) | RFLAGS.IF |
| Disable Interrupts | csrw sie, zero | msr daifset, 0xf | cli |
| Restore Interrupts | restore sie | restore daif | sti, if IF was set previously |
| RISCV64 | AARCH64 | X86_64 | |
|---|---|---|---|
| Page Table Pointer | satp | ttbr0_el1, ttbr1_el1 | cr3 |
| Paging Enabled | satp.mode | sctlr_el1.mmu | cr0.pg |
| RISCV64 | AARCH64 | X86_64 | |
|---|---|---|---|
| System Call | ecall | svc #X | syscall |
| Trap Return | sret | eret | iret, sysret |
| Flush TLB | sfence.vma zero, zero | tlbi vmalle1 | write to CR3 |
| RISCV64 | AARCH64 | X86_64 (Exceptions / Interrupts) | X86_64 (Syscalls) | |
|---|---|---|---|---|
| Exception Handler Base | stvec | vbar_el1 | Interrupt Descriptor Table (IDT) | LSTAR |
| Saved PC | sepc | elr_el1 | On stack (RIP) | RCX |
| Saved Privilege Level | sstatus.spp | spsr_el1.M | On stack (CS) | STAR (assumed ring 3) |
| Saved Interrupt Mode | sstatus.spie | spsr_el1.{I,F} | On stack (RFLAGS.IF) | R11 (saved RFLAGS.IF) |
| Exception Cause | scause | esr_el1 | On stack (sometimes) | -- |
| Saved SP | -- | -- | On stack (SS:RSP) | -- |
| New SP | -- | -- | tss.rsp0 (only on cpl 3 -> 0) | -- |
| Scratch Register | sscratch | -- | -- | gs |
In general, calling a function (call):
- Saves the address of the instruction after the
callsomewhere (either a register or the stack). This is called the "return address". - Sets the program counter register to the first address of whatever function we're calling.
And returning from a function (ret):
- Sets the program counter register to whatever return address we saved during the previous
call. This undoes thecallinstruction, bringing us to the instruction right after thecall.
call:
push next ripto stackrip<- function to call
ret:
rip<-popfrom stack
call (aka bl, "branch with link"):
X30 / LR<-pc + 4pc<- function to call
ret:
pc<-X30 / LR
call (aka jal, "jump and link"):
X1 / RA<-pc + 4pc<- function to call
ret:
pc<-X1 / RA
In general, when taking a trap into the kernel (via an exception, interrupt, or system call), the CPU will save:
- The previous privilege level, as we're switching into kernel mode and will need to remember what mode we used to be in.
- Whether interrupts were enabled, as we're likely going to disable them when entering the trap handler.
- What instruction we were running before the trap.
And when returning from a trap (or when starting a user process by running the "return from trap" instruction) the CPU will load:
- The new privilege level.
- Whether interrupts should be enabled.
- The address to jump to.
Each ISA puts these fields in different places but they all ultimately do the same thing.
On syscall:
rcx<- oldripr11<- oldrflagsrip<-lstarrflags<-rflags&~sfmaskcs<-star.syscall_csss<-star.syscall_cs + 8
On sysret (64-bit):
rip<-rcxrflags<-r11cs<-star.sysret_cs + 16ss<-star.sysret_cs + 8
On interrupt/ exception:
rsp<-tss.rsp0, only if we were in ring 3 before (otherwiserspunchanged)- push
ss - push
rsp - push
rflags - push
cs - push
rip - (for some exceptions) push an error code
rip<-idt[idx].offsetcs<-idt[idx].selector
On iret (64-bit):
- pop
rip - pop
cs - pop
rflags - pop
rsp - pop
ss
On system call (svc) / interrupt/ exception:
elr_el1<- oldpcesr_el1<- trap reasonspsr_el1<- oldpstatepc<-vbar_el1 + offset, whereoffsetis dependent on what kind of trap this is
On eret:
pc<-elr_el1pstate<-spsr_el1(sets privilege level (EL0 / EL1), interrupt mode (DAIF), among other things)
On system call (ecall) / interrupt/ exception:
sepc<- oldpcsstatus.spp<- old privilege modesstatus.spie<- oldsstatus.sie(interrupt enable for supervisor mode)sstatus.sie<- 0scause<- trap reasonstval<- (for some exceptions) extra information about the trap- privilege mode <- supervisor (
0b01) pc<-stvec
On sret:
pc<-sepc- privilege mode <-
sstatus.spp sstatus.sie<-sstatus.spie
Note that sstatus.sie is ignored in U mode and is treated as always 1.
- The 32 bit forms of registers refer to the lower 32 bits of their 64 bit counterparts. For example, EAX refers to the lower 32 bits of RAX.
- AARCH64 has multiple stack pointers, one for each exception level.
Which one is being used is controlled by
SPSel. - All RISCV64 registers have multiple names. All registers have a number (eg. X0-X31), but can also be referred to by a human-readable name (eg. A0 means argument 0, and is the same as X10).
- We are using the UNIX-style System-V ABI for X86_64, not the Microsoft x64 convention.
- sstatus.spie is the saved value of sstatus.sie, which only controls interrupt delivery in S mode. To control interrupt delivery in U mode, use the sie csr, which is not saved / restored during traps.
- If your ISA saves the return address in a register (ARM and RISCV), when you are writing some assembly function, if you call any other functions you must save and restore the return address register first! As performing a function call will change the return address and you won't be able to return to your caller when your function is done.