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
call
somewhere (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 thecall
instruction, bringing us to the instruction right after thecall
.
call
:
push next rip
to stackrip
<- function to call
ret
:
rip
<-pop
from stack
call
(aka bl
, "branch with link"):
X30 / LR
<-pc + 4
pc
<- function to call
ret
:
pc
<-X30 / LR
call
(aka jal
, "jump and link"):
X1 / RA
<-pc + 4
pc
<- 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
<- oldrip
r11
<- oldrflags
rip
<-lstar
rflags
<-rflags
&~sfmask
cs
<-star.syscall_cs
ss
<-star.syscall_cs + 8
On sysret
(64-bit):
rip
<-rcx
rflags
<-r11
cs
<-star.sysret_cs + 16
ss
<-star.sysret_cs + 8
On interrupt/ exception:
rsp
<-tss.rsp0
, only if we were in ring 3 before (otherwisersp
unchanged)- push
ss
- push
rsp
- push
rflags
- push
cs
- push
rip
- (for some exceptions) push an error code
rip
<-idt[idx].offset
cs
<-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
<- oldpc
esr_el1
<- trap reasonspsr_el1
<- oldpstate
pc
<-vbar_el1 + offset
, whereoffset
is dependent on what kind of trap this is
On eret
:
pc
<-elr_el1
pstate
<-spsr_el1
(sets privilege level (EL0 / EL1), interrupt mode (DAIF), among other things)
On system call (ecall
) / interrupt/ exception:
sepc
<- oldpc
sstatus.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.