Loading content...
In the previous page, we explored hardware interrupts—external signals from devices demanding CPU attention. But there's another equally important source of interrupts: the software itself.
Every time a program requests a file, allocates memory, or connects to a network, it must transition from unprivileged user mode to privileged kernel mode. This transition cannot happen through a simple function call—security demands a controlled entry point. Similarly, when a program divides by zero, accesses invalid memory, or executes an illegal instruction, the CPU must handle these exceptional conditions.
Software interrupts—also called traps, exceptions, or synchronous interrupts—provide this mechanism. Unlike hardware interrupts that arrive asynchronously at unpredictable moments, software interrupts are generated directly by the executing instruction stream, either intentionally or as a consequence of an error.
By the end of this page, you will understand the complete taxonomy of software-generated interrupts: traps (intentional), faults (recoverable), and aborts (fatal). You'll learn how the INT instruction triggers system calls, how the CPU responds to exceptional conditions, and how operating systems leverage these mechanisms for protection, virtual memory, and debugging.
The terms 'interrupt,' 'exception,' 'trap,' and 'fault' are often used interchangeably, but they have precise meanings in CPU architecture documentation. Understanding this taxonomy is essential for system programming.
Intel's Classification (x86/x64):
Intel broadly categorizes these events into:
Exceptions are further classified by their behavior:
| Category | Definition | Return Behavior | Examples |
|---|---|---|---|
| Fault | Exception detected before instruction completion | Returns to faulting instruction (re-execute) | Page fault, segment not present, divide error |
| Trap | Exception detected after instruction completion | Returns to instruction after the trap | Breakpoint (INT 3), system call (INT 0x80) |
| Abort | Catastrophic error, instruction may be incomplete | Does not return—process/system terminated | Machine check, double fault |
The return behavior determines handler design. Faults return to the faulting instruction—perfect for page faults where the handler can load the page, then the instruction transparently re-executes. Traps return to the next instruction—appropriate for system calls where the caller expects to continue after the kernel returns. Aborts indicate unrecoverable errors where the process (or system) must be terminated.
The Software Interrupt Spectrum:
Software interrupts span from completely intentional programmer actions to completely unexpected error conditions:
Intentional ←──────────────────────────────────────────────→ Unexpected
INT n Debug Undefined Page Machine
(syscall) breakpoint instruction fault check
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Trap Trap Fault Fault Abort
(expected) (debugging) (illegal) (possible) (catastrophic)
This spectrum influences how operating systems handle each type: intentional traps execute requested services, faults attempt recovery, and aborts trigger termination with diagnostic information.
The most important use of software interrupts is implementing system calls—the mechanism by which user programs request kernel services. System calls are intentional traps that create a controlled, secure transition from user mode to kernel mode.
Why System Calls Use Traps:
User programs run in Ring 3 (lowest privilege) and cannot directly execute privileged operations like:
To perform these operations, programs must politely ask the kernel. The trap instruction provides this controlled gateway:
INT 0x80 (Linux legacy) or SYSCALL (modern)IRET or SYSRET to return to user modeThe INT n instruction triggers a software interrupt using vector n. Linux historically used INT 0x80 for system calls:
1234567891011121314151617181920212223242526
; Linux x86 system call using INT 0x80; System call: write(1, "Hello", 5) section .data msg db "Hello", 0 section .textglobal _start _start: ; Set up system call arguments mov eax, 4 ; System call number: sys_write (4) mov ebx, 1 ; Argument 1: file descriptor (stdout) mov ecx, msg ; Argument 2: buffer address mov edx, 5 ; Argument 3: count (bytes to write) int 0x80 ; Trigger software interrupt ; CPU: saves state, switches to Ring 0 ; Kernel: handles syscall, returns here ; Return value is in EAX (bytes written or -errno) ; Exit system call mov eax, 1 ; System call number: sys_exit (1) xor ebx, ebx ; Exit code: 0 int 0x80INT 0x80 involves full interrupt handling machinery: saving all registers, pushing error code, stack switching. This overhead (~200-400 cycles) is excessive for frequent system calls. Modern systems use faster mechanisms.
Faults are exceptions detected before or during instruction execution that may be recoverable. The key characteristic is that when the handler returns, the CPU re-executes the faulting instruction. This enables transparent recovery mechanisms.
The Fault Lifecycle:
| Vector | Name | Cause | Recovery Action |
|---|---|---|---|
| #0 | Divide Error | Division by zero or overflow | Terminate process or deliver SIGFPE |
| #6 | Invalid Opcode | Undefined or reserved instruction | Terminate process or emulate instruction |
| #7 | Device Not Available | FPU/SSE instruction with FPU disabled | Lazy FPU restore or terminate |
| #11 | Segment Not Present | Segment selector references absent segment | Load segment or terminate |
| #12 | Stack Fault | Stack segment limit exceeded | Expand stack or terminate |
| #13 | General Protection | Protection violation (many causes) | Context-dependent—often terminate |
| #14 | Page Fault | Page not present, write to read-only, etc. | Demand paging, COW, or SIGSEGV |
Page faults (vector #14) are the most important fault type. They enable demand paging (load pages only when accessed), copy-on-write (share pages until modified), memory-mapped files (load from disk on access), and overcommit (allocate more memory than physically available). A system handling millions of page faults per second is normal—faults are features, not bugs.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
// Simplified page fault handler (Linux-inspired)// Vector 14, error code pushed on stack void page_fault_handler(struct interrupt_frame *frame, uint32_t error_code) { // CR2 register contains the faulting virtual address uintptr_t fault_addr = read_cr2(); // Decode error code int present = error_code & 0x01; // Page was present int write = error_code & 0x02; // Write access caused fault int user = error_code & 0x04; // Fault occurred in user mode int reserved = error_code & 0x08; // Reserved bit violation int instruction = error_code & 0x10; // Instruction fetch caused fault // Find the virtual memory area containing this address struct vm_area *vma = find_vma(current->mm, fault_addr); if (!vma || fault_addr < vma->vm_start) { // No mapping exists for this address // Send SIGSEGV to the process send_signal(current, SIGSEGV); return; } // Check permissions if (write && !(vma->vm_flags & VM_WRITE)) { // Write to read-only mapping if (vma->vm_flags & VM_MAYWRITE) { // Copy-on-write: duplicate the page handle_cow_fault(vma, fault_addr); return; } send_signal(current, SIGSEGV); return; } if (!present) { // Page not in physical memory if (vma->vm_file) { // Memory-mapped file: read page from file handle_file_fault(vma, fault_addr); } else if (vma->vm_flags & VM_ANON) { // Anonymous mapping: allocate zero page handle_anonymous_fault(vma, fault_addr); } else if (is_swap_entry(fault_addr)) { // Page was swapped out: read from swap handle_swap_fault(vma, fault_addr); } return; } // Other fault types: reserved bit, instruction fetch, etc. // Usually indicate serious problems send_signal(current, SIGSEGV);}Beyond system calls, traps serve a critical role in debugging. The CPU provides dedicated mechanisms for debuggers to pause execution, examine state, and single-step through code.
Breakpoint Trap (INT 3, Vector 3):
The single-byte INT 3 instruction (opcode 0xCC) is specially designed for debuggers. When a debugger sets a breakpoint:
1234567891011121314151617181920212223242526272829303132333435363738394041
// Demonstrating breakpoint mechanics in a debugger // How GDB sets a breakpoint at address 0x401000:// Original bytes: 55 48 89 E5 (push rbp; mov rbp, rsp)// After breakpoint: CC 48 89 E5 (int3; mov rbp, rsp) void debugger_set_breakpoint(pid_t child, void *addr) { // Read original instruction byte long original = ptrace(PTRACE_PEEKTEXT, child, addr, NULL); uint8_t saved_byte = original & 0xFF; // Store for later restoration save_breakpoint(addr, saved_byte); // Write INT 3 (0xCC) at the target address long modified = (original & ~0xFF) | 0xCC; ptrace(PTRACE_POKETEXT, child, addr, modified);} void debugger_handle_breakpoint(pid_t child) { // Process hit a breakpoint (received SIGTRAP) struct user_regs_struct regs; ptrace(PTRACE_GETREGS, child, NULL, ®s); // RIP points AFTER the INT 3 (1 byte), so subtract 1 void *breakpoint_addr = (void *)(regs.rip - 1); // Restore original instruction uint8_t saved_byte = get_saved_byte(breakpoint_addr); long current = ptrace(PTRACE_PEEKTEXT, child, breakpoint_addr, NULL); long restored = (current & ~0xFF) | saved_byte; ptrace(PTRACE_POKETEXT, child, breakpoint_addr, restored); // Move RIP back to re-execute the original instruction regs.rip--; ptrace(PTRACE_SETREGS, child, NULL, ®s); // Debugger now has control at the breakpoint print_registers(®s); wait_for_user_command();}Malware often detects debuggers by checking for INT 3 (0xCC) bytes in expected locations, examining debug registers (DR0-DR3), or measuring execution timing (single-stepping is slow). Security researchers and debugger developers engage in an ongoing arms race with malware authors.
Aborts represent the most severe exception class—errors so fundamental that the processor cannot guarantee the location of the faulting instruction or the integrity of program state. Recovery is generally impossible; the handler's role is to collect diagnostic information and terminate cleanly.
Key Abort Exceptions:
A Double Fault occurs when the CPU encounters an exception while trying to handle a previous exception. This creates a recursive failure that the normal exception mechanism cannot resolve.
If a Double Fault handler itself faults, a Triple Fault occurs. The CPU has no more recovery options—it shuts down or resets. This manifests as an instant reboot with no error message. Debugging triple faults requires serial console output, hardware debuggers, or analyzing the last successful log entries.
Some exceptions push an error code onto the stack before transferring control to the handler. This error code provides additional information about what caused the exception. Understanding error codes is essential for implementing exception handlers.
Exceptions with Error Codes:
Not all exceptions push error codes. The following exceptions do:
| Bit | Name | Clear (0) | Set (1) |
|---|---|---|---|
| 0 | P (Present) | Non-present page | Protection violation |
| 1 | W/R (Write) | Read access | Write access |
| 2 | U/S (User) | Supervisor mode | User mode |
| 3 | RSVD | Not reserved bit violation | Reserved bit set in PTE |
| 4 | I/D | Not instruction fetch | Instruction fetch (NX violation) |
| 5 | PK | Not protection key | Protection key violation |
| 6 | SS | Not shadow stack | Shadow stack access |
| 15 | SGX | Not SGX | SGX-related fault |
123456789101112131415161718192021222324252627282930313233343536373839
// Decoding x86 page fault error code struct page_fault_info { bool present; // Page was present (protection violation) bool write; // Caused by write access bool user; // Occurred in user mode bool reserved_bit; // Reserved bit set in page table bool instruction_fetch; // Caused by instruction fetch (NX) bool protection_key; // Protection key violation bool shadow_stack; // Shadow stack access uintptr_t fault_addr; // Address that caused the fault}; void decode_page_fault(uint32_t error_code, struct page_fault_info *info) { info->present = (error_code & (1 << 0)) != 0; info->write = (error_code & (1 << 1)) != 0; info->user = (error_code & (1 << 2)) != 0; info->reserved_bit = (error_code & (1 << 3)) != 0; info->instruction_fetch = (error_code & (1 << 4)) != 0; info->protection_key = (error_code & (1 << 5)) != 0; info->shadow_stack = (error_code & (1 << 6)) != 0; // CR2 contains the faulting linear address info->fault_addr = read_cr2();} void print_page_fault_info(struct page_fault_info *info) { printk("Page Fault at address 0x%lx\n", info->fault_addr); printk(" Cause: %s\n", info->present ? "protection violation" : "page not present"); printk(" Access: %s\n", info->write ? "write" : "read"); printk(" Mode: %s\n", info->user ? "user" : "kernel"); if (info->reserved_bit) printk(" Reserved bit violation in page table!\n"); if (info->instruction_fetch) printk(" Attempted to execute non-executable page\n"); if (info->protection_key) printk(" Protection key violation\n");}Exception handlers must account for the error code when accessing saved registers. Exceptions WITH error codes have a different stack layout than exceptions WITHOUT error codes. A common kernel programming bug is using the wrong offset to access saved RIP or RFLAGS, leading to crashes or security vulnerabilities.
Floating-point and SIMD operations have their own exception mechanisms. These exceptions are essential for IEEE 754 floating-point compliance and numerical computing.
x87 FPU Exceptions:
The legacy x87 FPU can generate exceptions for invalid operations, but these are typically masked—the FPU continues with a default result (NaN, infinity) instead of trapping. The FPU status word records which exceptions occurred.
| Exception | Cause | Masked Result |
|---|---|---|
| Invalid Operation (#I) | NaN operand, 0/0, ∞-∞, sqrt(-1) | Quiet NaN |
| Divide by Zero (#Z) | Non-zero / zero | Signed infinity |
| Overflow (#O) | Result too large for format | Infinity or MAX |
| Underflow (#U) | Result too small (subnormal) | Zero or subnormal |
| Precision (#P) | Result not exactly representable | Rounded result |
| Denormal Operand (#D) | Subnormal source operand | Process normally |
SSE/AVX SIMD Exceptions (#XM, Vector 19):
SSE and newer SIMD instruction sets use the MXCSR register (SSE Control/Status) for exception control. When an unmasked exception occurs and SIMD exceptions are enabled (CR4.OSXMMEXCPT), vector 19 is raised.
Lazy FPU Context Switching (#NM, Vector 7):
The x87 FPU and SIMD registers are large (512+ bytes for AVX-512). Saving/restoring them on every context switch is expensive. The Device Not Available exception (#NM) enables lazy context switching:
Modern CPUs with xsave/xrstor instructions and adequate memory bandwidth often use 'eager' FPU switching (always save/restore) rather than lazy switching. The #NM overhead and complexity can exceed the cost of always saving state, especially with frequent context switches or security concerns (Spectre-style attacks).
We've explored the world of software-generated interrupts—synchronous events that arise from the executing instruction stream. These mechanisms are fundamental to operating system design, enabling protection boundaries, virtual memory, debugging, and error handling.
What's Next:
Now that we understand both hardware interrupts and software exceptions, we'll explore how the CPU actually handles these events. The next page covers Interrupt Handling: the precise sequence of CPU actions from interrupt recognition through handler execution and return, including the critical concepts of context saving, stack switching, and the IRET instruction.
You now understand software interrupts and exceptions: their taxonomy, mechanisms, and roles in system operation. Traps enable system calls, faults enable virtual memory, and aborts handle catastrophic failures. This knowledge is essential for understanding OS-application boundaries and error handling. Next, we'll see exactly how the CPU processes these interrupts.