Oh maybe that’s why we have to use extern "C" all the time.
Rust: Possibly up to half of a language.
C Calling Convention
preserved registers
scratch registers
rbp, rbx, rsp, r12, r13, r14, r15
rax, rcx, rdx, rsi, rdi, r8, r9, r10, r11
callee-saved
caller-saved
Compiled C code will necessarily respect this convention (or it isn’t C code).
Exceptions
In contrast to function calls, exceptions can occur on any instruction.
In most cases, we don’t even know at compile time if the generated code will cause an exception.
For example, the compiler can’t know if an instruction causes a stack overflow or a page fault.
Can’t Backup
Exceptions can’t use a calling convention that relies on caller-saved registers for exception handlers.
Instead, preserve all registers.
The x86-interrupt calling convention is such a calling convention.
It guarantees that all register values are restored to their original values on function return.
Recall
HandlerFunc definition:
type HandlerFunc =extern"x86-interrupt"fn(_: InterruptStackFrame);
That’s a function alright.
A type alias for an extern "x86-interrupt" fn type
Usually use extern for C, here we use it to interface with hardware.
In Practice
The compiler only backs up the registers that are overwritten by the function.
In e.g. conditional branches, can “just in time” pop registers to stack immediately before using them.
More overhead but more efficient.
This way, very efficient code can be generated for short functions that only use a few registers.
Stack Frames
On a normal function call (using the call instruction), the CPU pushes the return address before jumping to the target function.
On function return (using the ret instruction), the CPU pops this return address and jumps to it.
Visually
The “stack frame” of a normal function call looks like this:
Interrupt Frames
For exception and interrupt handlers: - Pushing a return address would not suffice, - Interrupt handlers often run in a different context (stack pointer, CPU flags, etc.). - Providing return values is not ideal. - Whatever code executed during the interrupt is not anticipating alterations to its register state
Step 0
Saving the old stack pointer
The CPU reads the stack pointer (RSP) and stack segment (SS) register values to a safe memory location.
Step 1
Saving the old stack pointer
Aligning the stack pointer
Some CPU instructions require that the stack pointer be aligned on a 16-byte boundary, so the CPU performs such an alignment right after the interrupt.
Step 2
Saving the old stack pointer
Aligning the stack pointer
Switching stacks
An exception may require a change in privilege level (e.g. user to OS). Conditionally switch to the “Interrupt Stack Table” (next lecture).
Step 3
Saving the old stack pointer
Aligning the stack pointer
Switching stacks
Pushing the old stack pointer
The CPU pushes the stack pointer (RSP) and stack segment (SS) register values from step 0 to the stack to restore the stack pointer on return.
Step 3.1
Aligning the stack pointer
Switching stacks
Pushing the old stack pointer and the RFLAGS register:
On interrupt entry, the original RFLAGS is probably not appropriate - save it and get a new one.
Step 3.2
Aligning the stack pointer
Switching stacks
Push in order RSP, SS, RFLAGS and the instruction pointer:
On return, it is important to restore not just data (the stack pointer) but also instruction pointer (RIP and the code segment CS).
Step 3.3
Aligning the stack pointer
Switching stacks
Push in order RSP, SS, RFLAGS, RIP, CS and… conditionally an error code.
For some specific exceptions, such as page faults, the CPU pushes an error code, which describes the cause of the exception.
Step 4
Aligning the stack pointer
Switching stacks
Push in order RSP, SS, RFLAGS, RIP, CS etc.
Invoking the interrupt handler
The CPU reads the address and the segment descriptor of the interrupt handler function from the corresponding field in the IDT and loads the values into RIP and CS.
Visually
The “interrupt stack frame”:
Crates for Days
Handwaving
The x86-interrupt calling convention is a powerful abstraction that hides almost all of the messy details of the exception handling process.
However, sometimes it’s useful to know what’s happening behind the curtain.
Briefly, what we handwave.
Retrieving the arguments
Most calling conventions expect that the arguments are passed in registers
This is not possible for exception handlers since we must not overwrite any register values before backing them up on the stack
Instead, the x86-interrupt calling convention is aware that the arguments already lie on the stack at a specific offset
Returning using iretq
Since the interrupt stack frame completely differs from stack frames of normal function calls, we can’t return from handler functions through the normal ret instruction
So instead, the iretq instruction must be used
You’ll never guess what q stands for…
Quad word, it’s the 64 bit iret
Handling the error code
Conditional error codes complicate matters:
Conditional arguments.
Stack alignment.
Return pops.
So code must use the correct function type for each exception
Luckily, the InterruptDescriptorTable type does so.
Aligning the stack
Some instructions (especially SSE instructions) require a 16-byte stack alignment
The CPU ensures this alignment whenever an exception occurs, but for some exceptions it destroys it again later when it pushes an error code
The x86-interrupt calling convention takes care of this by realigning the stack in this case
Read more
There is a “hard mode” version of exception handling.