Exceptions
OS in Rust
Announcements
- Action Items:
- Graphics done.
- Integration testing ongoing.
Citations
- CPU exceptions are algorithmically interesting but technically tedious.
- CPU Exceptions
Background
Exceptions
- CPU exceptions occur in various erroneous situations
- Accessing an invalid memory address
- Dividing by zero
- We have to set up an interrupt descriptor table..
- This week, our kernel will be able to catch breakpoint exceptions.
Meaning
- An exception signals that something is wrong with the current instruction.
- Machine-level line-of-code.
- For example, divide by
0. - When an exception occurs, the CPU interrupts its current work and immediately calls a specific exception handler function, depending on the exception type.
- The OS manages these handler functions.
Rust
- Rust is pretty safe and generally doesn’t want to let you cause the easier CPU exceptions.
- The most famous is zero division.
rustc protects
- Modern languages won’t allow this.
$ cargo r
Compiling divzero v0.1.0 (/home/user/tmp/divzero)
error: this operation will panic at runtime
--> src/main.rs:3:20
|
3 | println!("{}", 1 / 0);
| ^^^^^ attempt to divide `1_i32` by zero
|
= note: `#[deny(unconditional_panic)]` on by default
error: could not compile `divzero` (bin "divzero") due to 1 previous errorPermit
- We can of course
#[allow(unconditional_panic)] - (Or take user input, of course.)
See a panic
- Presumably this is hitting the CPU then a
rustcprovided panic handler.
$ cargo r
Compiling divzero v0.1.0 (/home/user/tmp/divzero)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.17s
Running `target/debug/divzero`
thread 'main' (2292) panicked at src/main.rs:3:14:
attempt to divide by zero
note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceMuch Easier
- We can just use a language that let’s us use the computer.
Permitted
- It will warn us but not stop us.
Modernizing
- Naturally, in a C class this would be banned through use of compiler options.
- This yields:
$ gcc main.c -std=c89 -Wall -Wextra -Werror -Wpedantic -O2 -o main
main.c: In function ‘main’:
main.c:2:19: error: division by zero [-Werror=div-by-zero]
2 | int x = 1 / 0;
| ^
main.c:2:13: error: unused variable ‘x’ [-Werror=unused-variable]
2 | int x = 1 / 0;
| ^
cc1: all warnings being treated as errorsResults
- In any case, if you do use naive
gccyou will get the following:
- This is of course ludicrously confusing.
- There is no usage of floating point at all.
Clang
- There are
clangfans (vs.gcc) - Compilation is identical though the warning format differs.
Difference
clangdoes not trigger an exception.
$ cat main.c
#include <stdio.h>
int main() {
int x = 1 / 0;
printf("%d\n", x);
return 0;
}
user@cd-desk:~/tmp/divzero$ clang main.c -o main
main.c:4:12: warning: division by zero is undefined [-Wdivision-by-zero]
int x = 1 / 0;
^ ~
1 warning generated.
user@cd-desk:~/tmp/divzero$ ./main
267243840
user@cd-desk:~/tmp/divzero$ ./main
2140340544Aside
- Checkmate
clangfans. - That should definitely explode.
- Not because the language is specified that way.
- Because
gccdoes and therefore so should you.
Exceptions
Types
On x86, there are about 20 different CPU exception types. The most important are:
- Page Fault: A page fault occurs on illegal memory accesses. For example, if the current instruction tries to read from an unmapped page or tries to write to a read-only page.
- Pages are numerical regions of memory.
- We imagine, for example, missing the VGA buffer due to faulty arithmetic.
Types
On x86, there are about 20 different CPU exception types. The most important are:
- Invalid Opcode: This exception occurs when the current instruction is invalid, for example, when we try to use new SSE instructions on an old CPU that does not support them.
- If we naively accelerate graphics code this is an issue.
- Common in “early” (1995-2005) game design.
Types
On x86, there are about 20 different CPU exception types. The most important are:
- General Protection Fault: This is the exception with the broadest range of causes.
- Usually prevents a user from elevating their own privilege level by trying to overwrite a protected value that defines privilege level.
- Used to prevent virtual machine escape, safely sandbox browser code.
Types
On x86, there are about 20 different CPU exception types. The most important are:
- Double Fault: When an exception occurs, the CPU tries to call the corresponding handler function. If another exception occurs while calling the exception handler, the CPU raises a double fault exception.
- This exception also occurs when there is no handler function registered for an exception.
Types
On x86, there are about 20 different CPU exception types. The most important are:
- Triple Fault: If an exception occurs while handling a double fault, the CPU issues a fatal triple fault. We can’t catch or handle a triple fault.
- Most processors react by resetting themselves and rebooting the OS.
- Usually caused by some recursive fault pattern.
Interrupt Descriptor Table
- In order to catch and handle exceptions, we have to set up a so-called Interrupt Descriptor Table (IDT).
- Special case of a dispatch table.
Aside
Format
- Keys are exceptions; values are functions.
- Helpfully, exceptions are numeric so can implement as an array/vector/“table”
- The hardware uses this table directly, so we need to follow a predefined format.
- Each entry must have a 16-byte structure.
Python Style
- ♪♫♪ key-value storage my old friend ♫♪♫
- ♫♪♫ ive come to code with you again ♪♫♪
Rust Style
- Wait a minute wasn’t that our test runner????
Layout
| Type | Name | Description |
|---|---|---|
| u16 | Function Pointer [0:15] | The lower bits of the pointer to the handler function. |
| u16 | GDT selector | Selector of a code segment in the global descriptor table. |
| u16 | Options | (next slide) |
| u16 | Function Pointer [16:31] | The middle bits of the pointer to the handler function. |
| u32 | Function Pointer [32:63] | The remaining bits of the pointer to the handler function. |
| u32 | Reserved |
Options
| Bits | Name | Description |
|---|---|---|
| 0-2 | Interrupt Stack Table Index | 0: Don’t switch stacks, 1-7: Switch to the n-th stack in the Interrupt Stack Table when this handler is called. |
| 3-7 | Reserved | |
| 8 | 0: Interrupt Gate, 1: Trap Gate | If this bit is 0, interrupts are disabled when this handler is called. |
| 9-11 | must be one | |
| 12 | must be zero | |
| 13-14 | Descriptor Privilege Level (DPL) | The minimal privilege level required for calling this handler. |
| 15 | Present |
The Keys
- Each exception has a predefined IDT index.
- Set by the hardware designers.
- Invalid opcode exception has table index 6
- Page fault exception has table index 14
- \(\therefore\) hardware can automatically load the corresponding IDT entry for any exception.
Step 1
When an exception occurs, the CPU roughly does the following:
- Push some registers on the stack, including the instruction pointer and the FLAGS register.
- The instruction pointer keeps track of the current machine-level line-of-code
- It is like the stack pointer (which keeps track of the top of the stack of values.
- A special hardware variable.
- The instruction pointer keeps track of the current machine-level line-of-code
Aside: Hardware Security
FLAGS
- The FLAGS register (16 bit) and it’s cousins, EFLAGS (extended 32 bit) and RFLAGS (register 64 bit) are very important.
- Read more
- Whether a carry occurred.
- Whether a value is zero.
- Whether the sign is positive or negative.
- Whether a trap occurs (more soon).
- Whether interrupts are enabled (more soon).
On FLAGS
| Bit | Mask | Name | Description | Category | =1 | =0 |
|---|---|---|---|---|---|---|
| 0 | 0x0001 | CF | Carry flag | Status | CY (Carry) | NC (No Carry) |
| 1 | 0x0002 | — | Reserved, always 1 in EFLAGS | — | — | — |
| 2 | 0x0004 | PF | Parity flag | Status | PE (Parity Even) | PO (Parity Odd) |
| 3 | 0x0008 | — | Reserved | — | — | — |
| 4 | 0x0010 | AF | Auxiliary Carry flag | Status | AC (Auxiliary Carry) | NA (No Auxiliary Carry) |
| 5 | 0x0020 | — | Reserved | — | — | — |
On FLAGS
| Bit | Mask | Name | Description | Category | =1 | =0 |
|---|---|---|---|---|---|---|
| 6 | 0x0040 | ZF | Zero flag | Status | ZR (Zero) | NZ (Not Zero) |
| 7 | 0x0080 | SF | Sign flag | Status | NG (Negative) | PL (Positive) |
| 8 | 0x0100 | TF | Trap flag (single step) | Control | — | — |
| 9 | 0x0200 | IF | Interrupt enable flag | Control | EI (Enable Interrupt) | DI (Disable Interrupt) |
| 10 | 0x0400 | DF | Direction flag | Control | DN (Down) | UP (Up) |
On FLAGS
| Bit | Mask | Name | Description | Category | =1 | =0 |
|---|---|---|---|---|---|---|
| 11 | 0x0800 | OF | Overflow flag | Status | OV (Overflow) | NV (Not Overflow) |
| 12–13 | 0x3000 | IOPL | I/O privilege level (286+) | System | — | — |
| 14 | 0x4000 | NT | Nested task flag (286+) | System | — | — |
| 15 | 0x8000 | MD | Mode flag (NEC V-series only) | Control | Native Mode (186) | Emulation Mode (8080) |
My research
- I used RFLAGS in my research to get the doctorate that I do have.
- You can mostly understand this sentence now:
Named instructions
the
ininstruction
- From blog:
To communicate with such an I/O port, there are special CPU instructions called
inandout, which take a port number and a data byte
- You used
invia thex86_64crate to print to serial.
Privilege levels
current privilege level
- We haven’t dwelt on these but have noted, with respect to the general protection fault:
Used to prevent virtual machine escape, safely sandbox browser code.
Segments
- We will return to segments soon, but basically…
- Memory got too big to comfortably fit in a single register, so it got split into a “neighborhood” (segment) and offset (the stack pointer, instruction pointer, etc.).
- This is a big performance win since usually the high 32-48 bits are fixed within an executable.
Back to Work
Step 1
When an exception occurs, the CPU roughly does the following:
- Push some registers on the stack, including the instruction pointer and the FLAGS register.
- Naturally it is not important whether a carry occured immediately prior to an exception, and if it is you can read it from the stack.
Step 2
When an exception occurs, the CPU roughly does the following:
- Push some registers on the stack.
- Read the corresponding entry from the Interrupt Descriptor Table (IDT).
- For example, the CPU reads the 14th entry when a page fault occurs.
Step 3
When an exception occurs, the CPU roughly does the following:
- Push some registers on the stack.
- Read the corresponding entry from IDT.
- Check if the entry is present and, if not, raise a double fault.
- This is why the Rust option is so important to OS!
Step 4
When an exception occurs, the CPU roughly does the following:
- Push some registers on the stack.
- Read the corresponding entry from IDT.
- Check entry or raise a double fault.
- Disable hardware interrupts if the entry is an interrupt gate (bit 40 not set).
- Okay what does this mean.
Gate Types
- They become “visible” to CPU by appearing within some hardware “gate”.
Two Kinds
- Term some “Traps” - where execution is frozen, stopped, or “trapped” in some way.
- Term some “Interrupts” - some exogenous factor (generally I/O) causes the CPU to divert control flow elsewhere.
- Interrupts preempt other hardware interrupts, but exceptions don’t.
Steps
When an exception occurs, the CPU roughly does the following:
- Push some registers on the stack.
- Read the corresponding entry from IDT.
- Check entry or raise a double fault.
- Perhaps disable hardware interrupts.
- Load the specified GDT selector into the CS (code segment).
GDT
- Basically a table of memory segments.
- The “neighborhoods” of memory
- There are three kinds:
- Code segments: regions of memory that contain executable instructions.
- Data segments: used to store program data.
- System segments, like Task State Segment (TSS).
CS (Code Segment)
- The region where the currently executing code lives…
- Was: possibly anywhere
- Is now: wherever the OS keeps exception handlers.
- The CS (and other segments) contain more than just addresses!
Visually
- Easier visually.
- Read more
Takeaways
- The “DPL” is what I tend to regard as important.
- The DPL of the CS is the CPL, which determines if user mode vs. super mode.
- Descriptor Privilege Level
- Code Segment
- Current Privilege Level
- 2 bits, but both are always equal on every wildly known OS.
- The DPL of the CS is the CPL, which determines if user mode vs. super mode.
Steps
When an exception occurs, the CPU roughly does the following:
- Push some registers on the stack.
- Read the corresponding entry from IDT.
- Check entry or raise a double fault.
- Perhaps disable hardware interrupts.
- Update the CS (code segment).
- Jump to the specified handler function.
Handlers
An IDT Type
- No real benefit to making our own IDT.
- It’s hardware defined and we just want to implement exceptions.
- We’ll use
InterruptDescriptorTablestruct of thex86_64crate.
It’s big
#[repr(C)]
pub struct InterruptDescriptorTable {
pub divide_by_zero: Entry<HandlerFunc>,
pub debug: Entry<HandlerFunc>,
pub non_maskable_interrupt: Entry<HandlerFunc>,
pub breakpoint: Entry<HandlerFunc>,
pub overflow: Entry<HandlerFunc>,
pub bound_range_exceeded: Entry<HandlerFunc>,
pub invalid_opcode: Entry<HandlerFunc>,
pub device_not_available: Entry<HandlerFunc>,
pub double_fault: Entry<HandlerFuncWithErrCode>,
pub invalid_tss: Entry<HandlerFuncWithErrCode>,
pub segment_not_present: Entry<HandlerFuncWithErrCode>,
pub stack_segment_fault: Entry<HandlerFuncWithErrCode>,
pub general_protection_fault: Entry<HandlerFuncWithErrCode>,
pub page_fault: Entry<PageFaultHandlerFunc>,
pub x87_floating_point: Entry<HandlerFunc>,
pub alignment_check: Entry<HandlerFuncWithErrCode>,
pub machine_check: Entry<HandlerFunc>,
pub simd_floating_point: Entry<HandlerFunc>,
pub virtualization: Entry<HandlerFunc>,
pub security_exception: Entry<HandlerFuncWithErrCode>,
// some fields omitted
}Format
- The fields have the type
idt::Entry<F>- We introduced this previously - a function pointer and options, basically.
- The type parameter
Fdefines the expected handler function type.
First
HandlerFuncfirst:
- That’s a function alright.
- A type alias for an
extern "x86-interrupt" fntype - Usually use
externfor C, here we use it to interface with hardware.
- A type alias for an
Calling Functions
- The
x86-interruptcalling convention:- Usually, with functions, the processor and compile can anticipate a future call.
- The compiler loads certain values in certain registers.
- The processor avoids overwriting certain registers.
- Not so with interrupts.
- Usually, with functions, the processor and compile can anticipate a future call.
Easy Mode
- By convention, a function call is invoked voluntarily by a compiler-inserted
callinstruction.- This is less true than people would like it to be, but we can handwave for now.
- The compiler loads up a bunch of registers and presses the “call” button.
- The OS loads up a new memory location with new instructions, which use registers in the anticipated way.
Hard Mode
- Imagine if you had to write a function where you didn’t know:
- How many arguments would be provided.
- The types of the arguments.
- Whether heap-allocated arguments are valid pointers.
- Each of these can happen on an interrupt since the state of processor is not fixed.
Calling Conventions
- Return to the level of languages.
- Calling conventions specify the details of a function call.
- Where the arguments are, for example.
- Whether the function gets a new stack.
- What happens when you return.
- On x86_64 Linux, the following rules apply for C functions (specified in the System V ABI)
To be continued…
In bonus lecture “The Call”