assemblylinux-kernelx86-64interrupt

x86-64 Kernel Interrupt Handler: Preserving GPRs when Transferring Control to Original Handler


I was asked in a course to make a custom handler for invalid opcode interrupt. There is a function I call in the handler, which if it returns zero I am supposed to pass control back to the original handler. I have a global variable void *old_ili_handler which holds the original INVALID OPCODE handler address.
My issue comes from the fact that I need to pop all the GPRs I pushed in the prologue before jumping or moving to the original handler; but doing that means any changes done to registers after popping and before the jump itself, mean I change the original register values which I'm not supposed to (I think?) as the handler is supposed to return the CPU to its state before the interrupt. This means I can't just do movq old_ili_handler(%rip), %r8 followed by jmp %r8 since that would make R8 lose its original value. What am I doing wrong? Is there another way to do this that I'm overlooking?

If it helps, my prologue looks like this:

my_ili_handler:
    push %rax
    push %rbx
    ; ... all 15 GPRs ...
    push %r15

And then in the section where I want to pass control, this is what I currently have:

    pop %r15
    pop %r14
    ; ... all 15 GPRs ...
    pop %rax                         ; RAX now has its original value
    movq old_ili_handler(%rip), %r8  ; This would overwrite R8's original value
    jmp *%r8                         ; Jumps to original handler

Solution

  • You're using indirect jmp with a register-direct addressing, but it supports any addressing mode. (In machine code, it uses the same Modr/M encoding as instructions like add reg, reg/mem.)

    jmpq *old_ili_handler(%rip) - A mem-indirect jump loads a new RIP from static storage.

    So you can "tailcall" anything that's pointed-to by static / global variables, or other memory you can access using registers. (Including segment regs, like jmp *%gs:32 or whatever, but that probably only makes sense if you've done swapgs first if you're in the syscall entry point or something. Probably not useful for this, just mentioning it as another sort of thing you can do in general.)

    If performance didn't matter, you could also push an address and then ret to it, after restoring registers. (This and future rets will mis-predict due to this ret not matching a call.) Could be useful if you didn't have static or thread-local storage to use as a temporary, and you needed to compute a target address that could be different every time. (In kernel mode you don't have a red-zone so you can't just use stack space below RSP, otherwise storing there and jmp *-8(%rsp) would be viable to not disturb the interrupt / exception frame. Unless interrupts are disabled and NMIs are impossible or handled with a different kernel stack via TSS tricks.)