The Interrupt Stack Frame
For exception and interrupt handlers, however, pushing a return address would not suffice, since interrupt handlers often run in a different context (stack pointer, CPU flags, etc.). Instead, the CPU performs the following steps when an interrupt occurs:
- Aligning the stack pointer: An interrupt can occur at any instructions, so the stack pointer can have any value, too. However, some CPU instructions (e.g. some
SSE instructions) require that the stack pointer is aligned on a 16 byte boundary, therefore the CPU performs such an alignment right after the interrupt.
- Switching stacks (in some cases): A stack switch occurs when the CPU privilege level changes, for example when a CPU exception occurs in an user mode program. It is also possible to configure stack switches for specific interrupts using the so-called Interrupt Stack Table (described in the next post).
- Pushing the old stack pointer: The CPU pushes the values of the stack pointer (
rsp) and the stack segment (
ss) registers at the time when the interrupt occurred (before the alignment). This makes it possible to restore the original stack pointer when returning from an interrupt handler.
- Pushing and updating the RFLAGS register: The
RFLAGSregister contains various control and status bits. On interrupt entry, the CPU changes some bits and pushes the old value.
- Pushing the instruction pointer: Before jumping to the interrupt handler function, the CPU pushes the instruction pointer (
rip) and the code segment (
cs). This is comparable to the return address push of a normal function call.
- Pushing an error code (for some exceptions): For some specific exceptions such as page faults, the CPU pushes an error code, which describes the cause of the exception.
- 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. It then invokes this handler by loading the values into the rip and cs registers.
So the interrupt stack frame looks like this:
Behind the Scenes
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. Here is a short overview of the things that the
x86-interrupt calling convention takes care of:
- 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-interruptcalling 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 handlers functions through the normal ret instruction. Instead, the iretq instruction must be used.
- Handling the error code: The error code, which is pushed for some exceptions, makes things much more complex. It changes the stack alignment (see the next point) and needs to be popped off the stack before returning. The
x86-interruptcalling convention handles all that complexity. However, it doesn’t know which handler function is used for which exception, so it needs to deduce that information from the number of function arguments. That means that the programmer is still responsible to use the correct function type for each exception.
- Aligning the stack: There are some instructions (especially SSE instructions) that 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-interruptcalling convention takes care of this by realigning the stack in this case.