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.
src/main.rs
fn main() {
    // Print so isn't optimized out.
    println!("{}", 1 / 0);
}

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 error

Permit

  • We can of course #[allow(unconditional_panic)]
  • (Or take user input, of course.)
src/main.rs
$ cat src/main.rs
fn main() {
    #[allow(unconditional_panic)]
    let _x = 1 / 0;
}

See a panic

  • Presumably this is hitting the CPU then a rustc provided 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 backtrace

Much Easier

  • We can just use a language that let’s us use the computer.
main.c
int main() {
        int x = 1 / 0;
        return 0;
}

Permitted

  • It will warn us but not stop us.
$ gcc main.c -o main
main.c: In function ‘main’:
main.c:2:19: warning: division by zero [-Wdiv-by-zero]
    2 |         int x = 1 / 0;
      |                   ^

Modernizing

  • Naturally, in a C class this would be banned through use of compiler options.
Makefile
CC = gcc # or clang
CFLAGS = -std=c89 -Wall -Wextra -Werror -Wpedantic -O2
  • 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 errors

Results

  • In any case, if you do use naive gcc you will get the following:
$ ./main
Floating point exception (core dumped)
  • This is of course ludicrously confusing.
    • There is no usage of floating point at all.

Clang

  • There are clang fans (vs. gcc)
  • Compilation is identical though the warning format differs.
$ clang main.c -o main
main.c:2:12: warning: division by zero is undefined [-Wdivision-by-zero]
        int x = 1 / 0;
                  ^ ~
1 warning generated.

Difference

  • clang does 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
2140340544

Aside

  • Checkmate clang fans.
  • That should definitely explode.
    • Not because the language is specified that way.
    • Because gcc does 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.

In computer science, a dispatch table is a table of pointers or memory addresses to functions or methods.

Aside

  • Dispatch tables are cool as all get-out.
  • I recently performed an advanced manuever (taught them in CS 151).
  • Video
  • Code
  • Python a functional language within my life time, oversoul willing.

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 ♪♫♪
fs = {
    "ADD" : lambda x , y : x + y ,
    "SUB" : lambda x , y : x - y ,
    "MUL" : lambda x , y : x * y ,
    "DIV" : lambda x , y : x / y ,
    "MOD" : lambda x , y : x % y ,
    "NEG" : lambda x , y :   - x ,
    "ODD" : lambda x , y : x % 2 ,
    "ZER" : lambda x , y :     0 ,
    "EXT" : lambda x , y : exit(),
}

Rust Style

  • Wait a minute wasn’t that our test runner????
tests/println.rs
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
    osirs::_test_runner(&[&at_top, &wraps, &scrolls, &colors, &newlines]);
    osirs::qemu_quit(osirs::QEMU_PASS);
    loop {}
}

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.

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:

…when the in instruction executes, the I/O privilege level (IOPL, as given by bit 13 of the status EFLAGS register) must be greater than or equal to the current privilege level (CPL, as given by bit 13 of the Code Segment CS register)

Named instructions

the in instruction

  • From blog:

To communicate with such an I/O port, there are special CPU instructions called in and out, which take a port number and a data byte

  • You used in via the x86_64 crate 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

There are basically two kinds of interrupts: ones that occur when code execution has encountered an Exception due to bad code, or ones that occur to handle events unrelated to currently executing code.

  • 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

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.

Steps

When an exception occurs, the CPU roughly does the following:

  1. Push some registers on the stack.
  2. Read the corresponding entry from IDT.
  3. Check entry or raise a double fault.
  4. Perhaps disable hardware interrupts.
  5. Update the CS (code segment).
  6. 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 InterruptDescriptorTable struct of the x86_64 crate.

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

First

  • HandlerFunc first:
type HandlerFunc = extern "x86-interrupt" fn(_: InterruptStackFrame);
  • That’s a function alright.
    • A type alias for an extern "x86-interrupt" fn type
    • Usually use extern for C, here we use it to interface with hardware.

Calling Functions

  • The x86-interrupt calling 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.

Easy Mode

  • By convention, a function call is invoked voluntarily by a compiler-inserted call instruction.
    • 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”

Fin