Skip to main content
The OS is entirely event-driven at the hardware level. A periodic timer fires an IRQ on every tick, which is the sole mechanism that preempts the running process and invokes the scheduler.

ARM exception vector table

The vector table is placed at the start of .text and aligned to a 32-byte boundary. Each entry is a 32-bit branch instruction that redirects the processor to the appropriate handler:
root.s
.align 5
vector_table:
    b _start              @ 0x00 Reset
    b undefined_handler   @ 0x04 Undefined instruction
    b swi_handler         @ 0x08 Software interrupt (SWI)
    b prefetch_handler    @ 0x0C Prefetch abort
    b data_handler        @ 0x10 Data abort
    b .                   @ 0x14 Reserved (infinite loop)
    b irq_handler         @ 0x18 IRQ
    b fiq_handler         @ 0x1C FIQ
OffsetVectorHandlerNotes
0x00Reset_startBoot entry point
0x04Undefined instructionhangFatal — loops forever
0x08SWIhangNot used by this OS
0x0CPrefetch aborthangFatal
0x10Data aborthangFatal
0x14Reserved. (self-loop)Should never execute
0x18IRQirq_handlerTimer-driven scheduler entry
0x1CFIQhangNot used
hang is a simple infinite loop (b hang). Any unexpected exception causes the system to halt rather than exhibit undefined behaviour.

VBAR setup

The vector table is registered with the processor at boot via the Vector Base Address Register (VBAR):
root.s
_start:
    ldr r0, =vector_table
    mcr p15, 0, r0, c12, c0, 0
MCR p15, 0, r0, c12, c0, 0 writes r0 into CP15 register c12 (VBAR). After this instruction, all exceptions are dispatched through vector_table regardless of where it is located in memory.

IRQ and FIQ stack setup

Immediately after VBAR, _start configures separate stacks for IRQ and SVC modes:
root.s
    mrs r0, cpsr
    bic r1, r0, #0x1F
    orr r1, r1, #0x12      @ IRQ mode (0x12)
    orr r1, r1, #0x80      @ IRQ disabled
    msr cpsr_c, r1

    ldr sp, =_irq_stack_top     @ 4 KiB IRQ stack

    bic r1, r0, #0x1F
    orr r1, r1, #0x13      @ SVC mode (0x13)
    orr r1, r1, #0x80      @ IRQ disabled
    msr cpsr_c, r1

    ldr sp, =_stack_top         @ SVC stack

    bl main
The IRQ stack is a 4 KiB .bss region:
root.s
.section .bss
.align 8
irq_stack:
    .space 4096
_irq_stack_top:

enable_irq / disable_irq

IRQs are controlled by bit 7 of the CPSR (the I bit). When set, IRQs are masked; when clear, they are enabled.
root.s
enable_irq:
    mrs r0, cpsr
    bic r0, r0, #0x80    @ clear bit 7
    msr cpsr_c, r0
    bx  lr

disable_irq:
    mrs r0, cpsr
    orr r0, r0, #0x80    @ set bit 7
    msr cpsr_c, r0
    bx  lr
These are declared in os.h and called from C:
os.h
void enable_irq(void);
void disable_irq(void);

Timer configuration

timer_init() in timer.c programs the hardware timer and its interrupt controller to fire periodic IRQs.
timer.c
#define FREQ_QEMU  1 * 1000000

void timer_init(void) {
    PUT32(PLATFORM_TIMER_BASE + 0x08, 0);           // stop timer
    PUT32(PLATFORM_TIMER_BASE + 0x00, FREQ_QEMU);   // load value
    PUT32(PLATFORM_TIMER_BASE + 0x0C, 1);           // clear interrupt
    PUT32(PLATFORM_INTC_BASE  + 0x10, (1 << 4));    // unmask in VIC
    PUT32(PLATFORM_TIMER_BASE + 0x08, 0xE2);        // enable + periodic + IRQ
}
0xE2 enables the timer in 32-bit periodic mode with interrupt generation enabled.

Timer IRQ handler

When the timer fires, the interrupt is cleared and the interrupt controller is acknowledged before schedule() runs:
timer.c
void timer_irq_handler(void) {
    PUT32(PLATFORM_TIMER_BASE + 0x0C, 1);  // clear SP804 interrupt
    PUT32(PLATFORM_INTC_BASE  + 0x30, 1);  // acknowledge VIC
}

Critical sections in user processes

User processes call disable_irq() before any output and enable_irq() after to prevent the scheduler from preempting mid-print:
os.c
while (1) {
    disable_irq();
    PRINT("----From OS: hola\n");
    enable_irq();
}
If enable_irq() is never called after disable_irq(), no further context switches will occur and the system will be stuck in the process that last disabled interrupts.

ARM IRQ pipeline: the PC + 4 offset

When the ARM processor takes an IRQ, it sets:
lr_irq = PC_interrupted + 4
This is a consequence of the 3-stage fetch/decode/execute pipeline — the processor has already fetched one instruction past the instruction that was executing. To resume the interrupted instruction, the IRQ handler must subtract 4 before storing the PC in the PCB:
root.s
ldr  r5, [sp, #52]    @ lr_irq saved at handler entry
sub  r5, r5, #4       @ adjust for pipeline
str  r5, [r4, #PROC_PC]
Without this adjustment the process would resume one instruction too late, skipping the instruction that was in flight when the IRQ fired.

Build docs developers (and LLMs) love