ARMv7 Exception Handling

Exception handling should be one of the first things you write when writing an OS. I didn't do that. But luckily I had QEMU holding my hand throughout debugging, without which, I can imagine debugging would become quite the nightmare. The sad thing was, I didn't even write exception handling for debugging assistance. I thought I could write demand paging for page faults in kernelspace which wasn't as trivial as I thought. Anyways, back to exceptions.

An exception is a condition which requires a processor to halt normal execution and transfer control to a dedicated piece of software called an exception handler. Depending on the exception, a different exception handler will be used. On ARMv7, the different types of exceptions include: reset, interrupts, aborts, and exception generating instructions.

A special branch table of eight consecutive words called a vector table is the method in which control is transferred to an exception handler. The order of its entries, called exception vectors is significant as a processor uses an offset from the vector table's base to transfer control to the appropriate exception vector.

.global vector_table
vector_table:
  b do_reset_stub
  b do_undefined_instruction_stub
  b do_supervisor_call_stub
  b do_prefetch_abort_stub
  b do_data_abort_stub
  b .
  b do_irq_interrupt_stub
  b do_fiq_interrupt_stub

Depending on whether a processor implements the Security Extensions, the semantics of a vector table can change. Some processors allow the vector table to reside at any address, while others require it to be at a fixed location. Furthermore, some processors have many vector tables, while others only have one. I was running my OS on a system with a Cortex-A15 processor which implemented the Security Extensions, therefore, I had a few more things to account for.

A combination of the SCTLR and VBAR registers determine the vector table's base address. To achieve a similar memory layout to Linux, I choose to place my vector table at VECTOR_TABLE_VADDR or 0xffff0000. Well I say place, but what I really mean map. The below code below does that and works similar to userspace's mmap:

void map_vector_table() {
  create_mapping(VECTOR_TABLE_VADDR, virt_to_phys((uint32_t)&vector_table_begin), &interrupts_end - &vector_table_begin, BLOCK_RWX);
}

When an exception is taken, the processor changes into a different mode depending on the exception's type. There are nine modes on ARMv7: User, System, Supervisor, Abort, Undefined, FIQ, IRQ, and Monitor. The kernel runs in supervisor mode and all exceptions are handled in supervisor mode.

Most modes have a set of banked registers, meaning that changes to those registers don't reflect across other modes; they are copies. This effectively allows each mode to maintain its own state. However, this also means that you need to take extra care when handling exceptions; we don't want the stack pointer to be pointing to just any address!

As well as changing modes, depending on the exception, some additional information is provided to the exception handler. For example, most exceptions provide return information to the exception handler. This additional information ensures the proper handling of exceptions and restoration of control flow.

Let's make things concrete. Consider the below code where an IRQ exception is generated in foo:

void foo() {
  generate_irq_exception();
}

void bar() {
  foo();
}

If we were to look at the processor's register information in QEMU's monitor just before the exception is generated, then it might look something like below:

R00=c00362a8 R01=c00362a8 R02=02000000 R03=ffc03000
R04=1c000402 R05=80000000 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=c0013fd4
R12=c0013f58 R13=c0013fd0 R14=c000a8ac R15=c000a86c
PSR=20000013 --C- A S svc32

Some of these registers have special meanings. Importantly, R13 is the stack pointer, R14 is the link register which normally stores a function's return address, R15 is the program counter, and PSR is just QEMU's current program status register, cpsr. The svc32 at the bottom means that we're in in supervisor mode.

The state of all the registers from before must be preserved. Because as expected, immediately after the exception is generated, the processor's register information changes:

R00=c00362a8 R01=c00362a8 R02=02000000 R03=ffc03000
R04=1c000402 R05=80000000 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=c0013fd4
R12=c0013f58 R13=c0013ff8 R14=c000a884 R15=ffff0018
PSR=20000192 --C- A S irq32

As mentioned previously, we know that the processor will transfer control to an appropriate exception vector in the vector table. In this case, it's the IRQ exception vector which in turn branches to do_irq_interrupt_stub:

do_irq_interrupt_stub:
  /*
    We're in IRQ mode. "lr" is the return address which we must decrement to
    return to the instruction which triggered the exception. We push this
    return address and the SPSR to the supervisor mode stack.
  */
  sub lr, #4
  srsfd sp!, #0x13

  /* Switch to supervisor mode */
  cps #0x13

  /* Preserve the supervisor mode state. */
  push {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, fp, ip, lr}

  ldr r0, =do_irq_interrupt
  blx r0

  /* Restore the supervisor mode state and return. */
  pop {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, fp, ip, lr}
  rfefd sp!

The first comment doesn't really explain why we must decrement the link register. I mentioned that additional information can be provided to the exception handler. In this case the link register is updated to store the value of the program counter when the exception was generated. However, when read, the program counter is eight bytes, or two instructions ahead of the current instruction. Therefore, to get the next instruction, we subtract four. The only non-trivial instructions are srs, save return state, and rfe, return from exception. For our purposes they are the inverse of each other. The srs instruction pushes the current program status register and program counter to the stack at a a specified mode, and rfe pops them.

With everything preserved on the stack, we then call do_irq_request to handle the exception properly:

void do_irq_interrupt() {
  // Handle the exception properly.
}

Right before the return from exception instruction, if we were to look at our registers they would look something like below:

R00=c00362a8 R01=c00362a8 R02=02000000 R03=ffc03000
R04=1c000402 R05=80000000 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=c0013fd4
R12=c0013f58 R13=c0013fc8 R14=c000a8ac R15=ffff006c
PSR=20000193 --C- A S svc32

The only thing which is wrong is the link register and the current program status register. However, if we were to look at the stack, which the instruction is about to pop into the program counter, and current program status register, it would look something like the below:

c0013fc8: 0xc000a880 0x20000013 0xc0013fdc 0xc000a8ac

So after popping those two values, execution resumes in foo, as if nothing happened.

The above example was rather reductive. In a mature operating system, unlike the toy one I've been writing, far more would actually happen. However, I guess transferring control and returning it as if nothing happened could be considered the essence of exception handling. A glorified function call.