Search code examples
cpu-architecture

how cpu detect exceptions during execution?


How CPU during pipeline execution knows that the instruction that it is executing having some exception and which handler needs to be called. and who update pc with that handlers address?


Solution

  • The internal implementation details of how exceptions are detected and handled are of course specific to each different microarchitecture.

    One thing that's pretty much universal is that precise exception handling (execute everything before the execption, execute nothing after the exception) requires in-order retirement of instructions, even in an out-of-order CPU. If you let an instruction commit and leave the out-of-order core before earlier instructions, you couldn't roll it back if an earlier instruction took an exception.

    This happens easily in an in-order CPU, but is the reason why out-of-order CPUs need a large ROB, larger than the out-of-order scheduler for instructions that are still waiting for their inputs.

    Wikipedia's Classic RISC pipeline article has some interesting things to say in the Exceptions section, which are generic enough to apply to typical in-order pipelines, not a specific implementation.

    Exceptions are different from branches and jumps, because those other control flow changes are resolved in the decode stage. Exceptions are resolved in the writeback stage. When an exception is detected, the following instructions (earlier in the pipeline) are marked as invalid, and as they flow to the end of the pipe their results are discarded. The program counter is set to the address of a special exception handler, and special registers are written with the exception location and cause.


    As Margaret says, each different exception is assigned a number, and the OS puts handler addresses into a table.

    On x86, for example, 0x06 is the exception-number for for invalid opcode. When that's detected, the CPU loads the entry from the IVT (real mode) or IDT (protected or 64-bit mode).

    Before actually jumping there, it pushes the current IP/EIP/RIP and other exception-return stuff on the kernel stack. After that, the CPU internally the PC to the handler address.

    The base address of the table is stored internally in the CPU (updated with LIDT), but the power-on default location is 0000:0000H (the start of physical memory).

    Anyway, the table-lookup and actually jumping to the exception handler is pretty much separate from how the pipeline detects exceptions.


    In an out-of-order CPU, if an exception is detected in something that's being speculatively executed (e.g. code after a conditional branch), it's not actually taken until it's known to be non-speculative. i.e. until the branch is actually executed and found to go in the direction that branch-prediction predicted.

    If it turns out the CPU was speculating down the wrong path, the exception is squashed.

    Often, this is done internally by just marking an instruction (or uop) to fault if it reaches retirement. Mis-speculated instructions by definition come after something that should have run differently earlier, and in-order retirement forces the earlier thing to be correctly resolved before it can retire. (e.g. a branch that was mispredicted, a divide that should have faulted before execution got to a load from a bad address, or whatever.)

    Spectre and Meltdown vulnerabilities shed a lot of light on this mechanism. See Out-of-order execution vs. speculative execution for an explanation of how only detecting exceptions at retirement combined with design choices for what actually happens for certain will-fault loads resulted in speculative execution of later instructions with data this code doesn't architecturally have permission to read.

    (If that later code is written to cause a cache miss that populates a cache line at an address that depends on the secret data, that's one way to turn secrets into microarchitectural state, which can be read into architectural state by timing loads from cache later. Aka a cache-read timing side-channel to communicate from mis-speculated code to normal code that can retire successfully. This is the basis of Meltdown. Spectre is somewhat similar, but you don't get to just write the mis-speculated code, you have to trick branch prediction into jumping to existing code. Meltdown is pretty trivial to fix with a hardware design change, but Spectre not so much because we can't disable branch speculation without killing performance, and the loads inside the mis-speculated spectre "gadget" are architecturally allowed for the code running them.)

    BTW, recovery from branch mis-speculation is typically started before the branch reaches retirement in modern CPUs (fast recovery as soon as an execution unit checks the prediction), because branch mispredictions do happen in real code so making them less expensive helps a lot. Especially with huge ROB sizes, draining it for every mispredict would be a lot worse, especially when e.g. a loop counter was ready long before the loop body that was looping over an array getting cache misses. Branch mis-speculation (misprediction) doesn't involve taking an actual exception, but the speculative control-flow problem is not fundamentally different from exceptions for a CPU pipeline. Special mechanisms exist for branches because they're performance-relevant.