Project 7: AArch64 Exception Level Lab
Observe and transition between AArch64 exception levels.
Quick Reference
| Attribute | Value |
|---|---|
| Difficulty | Level 4 |
| Time Estimate | 16-24 hours |
| Main Programming Language | Assembly + C (Alternatives: Rust) |
| Alternative Programming Languages | Rust |
| Coolness Level | Level 4 |
| Business Potential | Level 3 |
| Prerequisites | Concept 1: Profiles & Execution States, Concept 5: Boot & Exceptions |
| Key Topics | EL0-EL3, privilege transitions |
1. Learning Objectives
By completing this project, you will:
- Translate ARM concepts into observable outputs you can verify.
- Explain why each toolchain or hardware step is necessary.
- Detect and fix at least one realistic failure mode.
- Communicate the result clearly in a technical review or interview.
2. All Theory Needed (Per-Concept Breakdown)
Profiles & Execution States
Fundamentals ARM is a family of architectures organized into profiles optimized for different constraints. The A-profile targets application processors that run rich OSes (phones, laptops, servers), the M-profile targets microcontrollers with tight power and memory budgets, and the R-profile targets deterministic real-time systems. citeturn0search0turn0search1turn0search2 Each profile implies a different set of instructions, privilege models, and system features. Within a profile, ARM defines execution states (such as AArch64 or AArch32) that determine register width, instruction encoding, and address space. AArch64 is the 64-bit execution state introduced in ARMv8-A, while AArch32 is the 32-bit state; M-profile uses Thumb encodings for compact code density and simpler decode logic. citeturn0search2 This is why “ARM assembly” is not a single language: the same mnemonic can encode differently, or even be invalid, depending on profile and state.
Deep Dive The profile split is the most important high-level idea in ARM. A-profile cores are built to host complex operating systems with virtual memory, multi-core scheduling, and high performance. That means features like exception levels, MMUs, and richer instruction sets matter. In contrast, M-profile focuses on minimal latency, low power, and deterministic behavior: it strips away many features to reduce silicon cost and simplify real-time response. R-profile sits in-between: it retains more predictability than A-profile but includes stronger real-time guarantees than M-profile. citeturn0search0turn0search1 When you choose to write assembly, you’re implicitly choosing a profile, and that choice changes everything from the boot flow to the toolchain arguments you use.
Execution states deepen the split. In ARMv8-A, AArch64 brings a new 64-bit register file and 32-bit fixed-length instruction encoding (A64). AArch32 keeps the 32-bit model (A32/T32). This means that for A-profile hardware, your code must declare its intended execution state; otherwise, even valid mnemonics may assemble into the wrong encoding or fail. citeturn0search2 M-profile, by contrast, uses Thumb encodings by design, favoring compact instructions and simpler decode paths. These constraints are not academic. They drive register availability, calling convention differences, and even the structure of your interrupt handlers. If you write code that assumes AArch64 but run on Cortex-M, the encoding and semantics are incompatible.
Another subtle but critical effect of profile and state is the system-level context. A-profile expects multiple privilege levels and potentially a hypervisor. M-profile’s exception model is simpler, its vector table is fixed and immediate, and it typically boots directly into a single firmware image. R-profile targets systems where real-time guarantees trump throughput; this affects interrupt priority, memory latency assumptions, and peripheral access patterns. Understanding profile choice lets you reason about why an instruction exists, why a particular addressing mode is missing, and why certain system registers are visible or hidden.
Finally, architecture profiles determine the ecosystem around your work. A-profile benefits from abundant tooling, standardized ABIs, and OS integration, while M-profile leans on vendor SDKs, board-specific memory maps, and smaller toolchains. This guide intentionally spans both because many real-world systems combine them: a Linux-capable application processor for high-level features and a microcontroller for deterministic control. Once you see that split, you can design experiments and projects that map to the right target without confusion.
How this fits on projects
- Shapes target selection in P01 (Toolchain Pipeline Explorer) and P07 (Exception Level Lab).
- Determines encoding assumptions in P03 (Thumb Encoder/Decoder).
Definitions & key terms
- Profile: A family of ARM features optimized for a market segment (A/M/R). citeturn0search0turn0search1turn0search2
- Execution state: The architectural mode (AArch64, AArch32, Thumb) that defines register width and instruction encoding. citeturn0search2
- AArch64: 64-bit execution state introduced in ARMv8-A.
- AArch32: 32-bit execution state in ARMv8-A.
- Thumb: Compact instruction encoding used by M-profile.
Mental model diagram
ARM Architecture Evolution:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────────────────────────────────────┐
│ ARM Holdings (IP owner) │
│ Designs architectures, licenses to others │
└─────────────────────┬───────────────────────┘
│
┌────────────────────────────────────────┼────────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌────────────────┐ ┌────────────────┐
│ M-Profile │ │ A-Profile │ │ R-Profile │
│ Microcontrollers │ Applications │ │ Real-Time │
│ (Embedded) │ │ (Phones, PCs) │ │ (Automotive) │
└──────────────┘ └────────────────┘ └────────────────┘
│ │
┌──────┴──────┐ ┌────────────┼────────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌───────┐ ┌────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│Cortex │ │Cortex │ │Cortex-A7│ │Cortex-A │ │Cortex-A │
│ -M0+ │ │-M4/M7 │ │Cortex-A9│ │53/55/72 │ │76/78/X │
│ │ │ │ │(32-bit) │ │(64-bit) │ │(64-bit) │
└───────┘ └────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │ │ │
│ │ │ │ │
Thumb Thumb-2 ARM32 AArch64 AArch64
only + DSP + Thumb + NEON + SVE2
+ FPU
┌──────────────────────────────────────────────────────────────────────────────┐
│ YOUR TARGETS: │
│ │
│ Raspberry Pi Pico (RP2040) Raspberry Pi 3/4/5 │
│ ├─ Dual Cortex-M0+ cores ├─ Cortex-A53/A72/A76 cores │
│ ├─ ARMv6-M architecture ├─ ARMv8-A architecture (AArch64) │
│ ├─ Thumb instruction set ├─ A64 instruction set │
│ ├─ 16 registers (r0-r15) ├─ 31 registers (x0-x30) │
│ ├─ 133 MHz max clock ├─ 1.5-2.4 GHz clock │
│ └─ 264 KB RAM, no OS └─ 1-8 GB RAM, Linux capable │
└──────────────────────────────────────────────────────────────────────────────┘

How it works (step-by-step, with invariants and failure modes)
- Choose the target profile (A/M/R) based on system constraints and OS expectations.
- Select execution state (AArch64, AArch32, Thumb) based on ISA and toolchain output.
- Assemble and link with profile/state-specific flags; encoding mismatches yield invalid opcodes.
- Boot into the expected privilege level; if the firmware expects EL2 and you start at EL1, early setup fails.
- Validate on target or emulator; incorrect profile assumptions manifest as illegal instruction faults or boot hangs.
Minimal concrete example (pseudo-assembly, not runnable)
Select Target = {Profile: M, State: Thumb}
Assemble([LOAD R0, [ADDR]], Target)
If Target != CPU_State → Fault: Illegal Instruction
Common misconceptions
- “ARM assembly is one language” → It is a family with profile/state splits.
- “Thumb is only a compact mode” → It also shapes register access and available instructions.
- “AArch64 is just ARM32 with bigger registers” → It changes the register file and encoding model.
Check-your-understanding questions
- Why can Cortex-M code not run on a Cortex-A core without translation?
- What is the difference between AArch64 and AArch32?
- How does the profile choice affect your toolchain flags?
Check-your-understanding answers
- Cortex-M uses the M-profile with Thumb encodings and a different system model; Cortex-A expects A-profile with AArch64/AArch32 states.
- AArch64 is a 64-bit execution state with a new register file and A64 encoding; AArch32 is 32-bit with different encodings. citeturn0search2
- The assembler and linker must emit instructions for the correct ISA and object format; mismatches produce illegal opcodes or link errors.
Real-world applications
- Microcontroller firmware (M-profile) in sensors, robotics, and embedded control. citeturn0search0
- Application processors (A-profile) in mobile, desktop, and servers. citeturn0search2
Where you’ll apply it
- This project: see §3.1 and §5.4 in P07-aarch64-exception-level-lab.md
- P01 Toolchain Pipeline Explorer
- P03 Thumb Instruction Encoder/Decoder
- P07 AArch64 Exception Level Lab
References
- Arm M-profile overview. citeturn0search0
- Arm R-profile overview. citeturn0search1
- Arm A-profile overview and execution states. citeturn0search2
Key insights Your “ARM assembly” only makes sense once you name the profile and execution state.
Summary Profiles and execution states are the root of every other difference in ARM assembly. When you get this right, the rest of the system becomes predictable.
Homework/Exercises to practice the concept
- Pick two devices (one microcontroller, one phone) and identify their ARM profile and execution state.
- Write a one-paragraph explanation of why Thumb exists.
Solutions to the homework/exercises
- Example: RP2040 is M-profile with Thumb; a modern smartphone SoC is A-profile with AArch64.
- Thumb improves code density and decoder simplicity, which is crucial for small embedded systems.
Boot, Exceptions & Interrupts
Fundamentals Boot and exception handling define how control flow starts and changes when the system is interrupted. On Cortex-M, reset reads a vector table at a fixed address to obtain the initial stack pointer and reset handler. On AArch64, exception levels (EL0–EL3) define privilege and isolation across kernel, hypervisor, and secure monitor. citeturn0search6turn2search4 Interrupts are structured events with defined entry and exit behavior; when misunderstood, they cause the most common low-level failures (silent lockups, corrupted stacks, and unacknowledged interrupts).
Deep Dive Boot flow is architecture-specific, but it always starts with the hardware choosing a program counter and stack pointer. In Cortex-M, the vector table is a literal list of addresses at the start of flash (or a remapped location). The CPU loads the initial SP from offset 0 and the reset handler from offset 4; execution begins there. This is why vector tables are so critical: a single incorrect address prevents boot. In AArch64 systems, boot is more complex. Firmware (or a ROM) selects the initial exception level and execution state, then transfers control to your image. This can occur at EL2 or EL1 depending on platform; understanding the starting level is essential for setting up the MMU and interrupt controller. citeturn0search6turn2search4
Exceptions and interrupts are structured transitions. On Cortex-M, hardware automatically saves a register frame on the stack and switches to handler mode. This means your ISR is effectively running on a known stack layout; if you violate it, return from interrupt fails. AArch64 exceptions follow a different path: they trap into higher exception levels and use banked registers and exception vector tables that differ per EL. This makes exception handling on A-profile both more powerful and more complex. In practice, you must know which registers are saved by hardware and which you must save manually, and you must understand the difference between synchronous exceptions (e.g., illegal instruction) and asynchronous interrupts (e.g., timer). citeturn0search6turn2search4
Interrupt latency is also a systems-level trade-off. M-profile is designed for low-latency, deterministic responses, which is why it dominates microcontroller workloads. citeturn0search0 This is a critical difference from A-profile, where throughput and virtualization might be prioritized. When you design firmware, you need to decide which tasks are best done in an ISR versus in the main loop; an ISR that does too much can starve other interrupts and introduce jitter.
Finally, exceptions connect directly to debugging. Many “mysterious” crashes are just unhandled faults. On Cortex-M, a hard fault may indicate an invalid memory access or misaligned stack. On AArch64, synchronous exceptions reveal illegal instructions or permission violations. By understanding the exception model, you can interpret fault codes and correlate them to your assembly-level behavior, which is a core skill in systems programming and security analysis.
How this fits on projects
- Central to P05 (Vector Table Builder), P06 (Interrupt-Driven UART), and P07 (Exception Level Lab).
Definitions & key terms
- Vector table: Table of exception handler addresses used at reset or interrupt.
- Exception level: Privilege tier in AArch64 (EL0–EL3). citeturn0search6turn2search4
- ISR: Interrupt service routine.
- HardFault: Cortex-M fault handler for severe errors.
Mental model diagram
Cortex-M Boot Sequence:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Power Applied
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 1. CPU comes out of reset │
│ - All registers undefined (except SP and PC) │
│ - Processor in Thread mode, privileged │
│ - Using Main Stack Pointer (MSP) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. CPU reads address 0x00000000 (or VTOR) │
│ - Loads INITIAL STACK POINTER value │
│ - This value goes into SP/r13 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. CPU reads address 0x00000004 │
│ - Loads RESET HANDLER address │
│ - This value goes into PC/r15 │
│ - Bit 0 MUST be 1 (Thumb mode indicator) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. Execution begins at Reset_Handler │
│ - Your code starts running! │
│ - Stack is ready to use │
│ - All peripherals need initialization │
└─────────────────────────────────────────────────────────────────┘
Vector Table Structure (first 16 entries are standard Cortex-M):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Offset │ Exception # │ Contents
─────────┼───────────────┼────────────────────────────────────────
0x0000 │ - │ Initial Stack Pointer value
0x0004 │ 1 (Reset) │ Reset_Handler address (| 1 for Thumb)
0x0008 │ 2 (NMI) │ NMI_Handler address
0x000C │ 3 (HardFault)│ HardFault_Handler address
0x0010 │ 4 │ Reserved (M0+ doesn't use)
... │ ... │ ...
0x003C │ 15 (SysTick) │ SysTick_Handler address
0x0040 │ 16 (IRQ0) │ First peripheral interrupt
0x0044 │ 17 (IRQ1) │ Second peripheral interrupt
... │ ... │ (RP2040 has 26 IRQs)
Example minimal vector table in assembly:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
.section .vectors, "a"
.align 2
.word _stack_top // 0x00: Initial SP
.word Reset_Handler + 1 // 0x04: Reset (bit 0 = Thumb)
.word NMI_Handler + 1 // 0x08: NMI
.word HardFault_Handler+1 // 0x0C: HardFault
.word 0 // 0x10: Reserved
// ... more entries ...
NOTE: On RP2040, flash is at 0x10000000, so your vector table
lives there. The boot ROM copies the SP and PC from flash.

AArch64 Exception Levels:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─────────────────────────────────────────────────────────────────┐
│ EL3: Secure Monitor │
│ - Highest privilege, manages secure/non-secure worlds │
│ - TrustZone firmware lives here │
├─────────────────────────────────────────────────────────────────┤
│ EL2: Hypervisor │
│ - Virtualization support │
│ - Controls virtual machines │
├─────────────────────────────────────────────────────────────────┤
│ EL1: OS Kernel │
│ - Where Linux kernel runs │
│ - Your bare-metal code runs here! │
├─────────────────────────────────────────────────────────────────┤
│ EL0: User Applications │
│ - Lowest privilege │
│ - Normal programs run here under Linux │
└─────────────────────────────────────────────────────────────────┘
On Raspberry Pi boot:
┌──────────────────────────────────────────────────────────────┐
│ GPU firmware starts at EL3, then drops to EL2, │
│ loads your kernel8.img, and jumps to 0x80000 at EL2. │
│ Your bare-metal code typically runs at EL1 after setup. │
└──────────────────────────────────────────────────────────────┘

Interrupt Flow on Cortex-M:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Main Code Running
│
│ ← UART receives byte
│ Hardware sets interrupt flag
│ NVIC sees enabled interrupt
▼
┌──────────────────────────────────────────────────────────────────┐
│ AUTOMATIC HARDWARE ACTIONS (you don't write code for this): │
│ 1. Finish current instruction │
│ 2. Push 8 registers to stack: r0-r3, r12, LR, PC, xPSR │
│ 3. Load new PC from vector table (exception #) │
│ 4. Load 0xFFFFFFF9 into LR (EXC_RETURN) │
│ 5. Enter Handler mode (privileged) │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ YOUR ISR EXECUTES: │
│ - Must save r4-r11 if you use them (push {r4-r7}) │
│ - Read UART data register (clears interrupt flag) │
│ - Process byte (store in buffer, set flag, etc.) │
│ - Restore r4-r11 if saved │
│ - Return with: BX LR (the magic EXC_RETURN value) │
└──────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ AUTOMATIC HARDWARE ACTIONS: │
│ 1. Hardware detects EXC_RETURN in LR │
│ 2. Pop 8 registers from stack │
│ 3. Resume execution exactly where interrupted │
│ 4. Return to Thread mode │
└──────────────────────────────────────────────────────────────────┘
│
▼
Main Code Continues (unaware anything happened!)
Stack During Interrupt:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
BEFORE interrupt: AFTER entry, BEFORE ISR code:
┌──────────────┐ ┌──────────────┐
│ (old data) │ │ (old data) │
│ │ ├──────────────┤
│ │ │ xPSR │ ← +0x1C from new SP
│ │ ├──────────────┤
│ │ │ PC (return) │ ← +0x18
│ │ ├──────────────┤
│ │ │ LR │ ← +0x14
│ │ ├──────────────┤
│ │ │ r12 │ ← +0x10
│ │ ├──────────────┤
│ │ │ r3 │ ← +0x0C
│ │ ├──────────────┤
│ │ │ r2 │ ← +0x08
│ │ ├──────────────┤
│ │ │ r1 │ ← +0x04
│ │ ├──────────────┤
SP →│ │ SP→│ r0 │ ← +0x00 (new SP)
└──────────────┘ └──────────────┘
The 32 bytes (8 × 4) are pushed automatically by hardware!

How it works (step-by-step, with invariants and failure modes)
- Boot loads initial SP and PC from the vector table (Cortex-M) or firmware-defined entry (AArch64). citeturn0search6turn2search4
- An interrupt triggers hardware context save and branches to the handler.
- Handler restores context and returns using the architecture-specific mechanism.
- Failure mode: wrong vector address or corrupted stack → boot hang or fault loop.
Minimal concrete example (pseudo, not runnable)
VECTOR_TABLE[Reset] -> Reset_Handler
On interrupt: push context, branch handler, restore, return
Common misconceptions
- “Interrupt handlers are just normal functions” → They obey different entry/exit rules.
- “Boot is just jump to main” → Boot is a structured sequence with strict alignment rules.
Check-your-understanding questions
- Why must the reset handler address be Thumb-aligned on Cortex-M?
- What does EL1 represent in AArch64?
- Why must ISRs be short?
Check-your-understanding answers
- Bit 0 of the address indicates Thumb state; if it’s wrong, the CPU faults.
- EL1 is the kernel-level privilege where OS code typically runs. citeturn0search6turn2search4
- Long ISRs increase latency and can block higher-priority interrupts.
Real-world applications
- Firmware bootloaders, interrupt-driven I/O, and OS exception handling. citeturn0search0turn0search6
Where you’ll apply it
- This project: see §3.1 and §5.4 in P07-aarch64-exception-level-lab.md
- P05 Vector Table Builder
- P06 Interrupt-Driven UART
- P07 AArch64 Exception Level Lab
References
- AArch64 exception model and privilege levels. citeturn0search6turn2search4
- Cortex-M profile emphasis on low-latency interrupt response. citeturn0search0
Key insights Boot and exceptions are not features you add later; they are the foundation of control flow.
Summary Once you understand boot and exceptions, most “mysterious” bare-metal failures become obvious.
Homework/Exercises to practice the concept
- Draw the Cortex-M vector table layout and label the first 8 entries.
- Explain how an interrupt differs from a synchronous exception.
Solutions to the homework/exercises
- The first entry is the initial SP, followed by reset, NMI, HardFault, and system handlers.
- Interrupts are asynchronous hardware events; synchronous exceptions are triggered by the current instruction.
3. Project Specification
3.1 What You Will Build
A lab that reports current EL and safely transitions to EL1.
3.2 Functional Requirements
- Requirement 1: Read current exception level
- Requirement 2: Configure target EL and transition
- Requirement 3: Log outcomes in deterministic order
3.3 Non-Functional Requirements
- Clear diagnostics on failure
3.4 Example Usage / Output
$ aarch64-el-lab
EL at entry: EL2
Dropped to EL1
EL now: EL1
$ aarch64-el-lab --invalid-el
error: unsupported target EL
exit code: 2
3.5 Data Formats / Schemas / Protocols
- EL log: timestamp, EL value
3.6 Edge Cases
- Unexpected starting EL
- Traps during transition
3.7 Real World Outcome
This is the golden reference for success:
- You can explain why a platform boots at EL2 or EL3.
3.7.1 How to Run (Copy/Paste)
- Build: follow the toolchain steps defined in this guide
- Run: use the CLI examples in §3.4 with fixed inputs
- Expected directory: project root
3.7.2 Golden Path Demo (Deterministic)
Run with a fixed input set and confirm output matches §3.4 exactly.
3.7.3 If CLI: Exact Terminal Transcript
$ aarch64-el-lab
EL at entry: EL2
Dropped to EL1
EL now: EL1
$ aarch64-el-lab --invalid-el
error: unsupported target EL
exit code: 2
4. Solution Architecture
4.1 High-Level Design
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Input Layer │───▶│ Core Logic │───▶│ Output Layer │
└──────────────┘ └──────────────┘ └──────────────┘
4.2 Key Components
| Component | Responsibility | Key Decisions |
|---|---|---|
| Input Parser | Validate and normalize input | Strict error handling |
| Core Engine | Perform the main computation | Deterministic paths |
| Reporter | Produce user-facing output | Stable formatting |
4.3 Data Structures (No Full Code)
Record Entry {
name: string
fields: list
notes: text
}
4.4 Algorithm Overview
Key Algorithm: Core Flow
- Parse input and validate parameters.
- Execute the core transformation or analysis.
- Emit deterministic output or error summary.
Complexity Analysis:
- Time: O(n) in the size of input records
- Space: O(n) for stored mappings and logs
5. Implementation Guide
5.1 Development Environment Setup
# Install toolchain and verify versions
toolchain --version
5.2 Project Structure
project-root/
├── src/
│ ├── core
│ └── io
├── tests/
│ └── fixtures
├── docs/
└── README.md
5.3 The Core Question You’re Answering
“Observe and transition between AArch64 exception levels.”
5.4 Concepts You Must Understand First
Stop and research these before coding:
- Profiles & Execution States
- What is the key invariant you must preserve?
- Boot, Exceptions & Interrupts
- What is the key invariant you must preserve?
5.5 Questions to Guide Your Design
- Data Flow
- How does input become output?
- Which steps must be deterministic?
- Validation
- What is the simplest test that proves correctness?
- How will you detect regressions?
5.6 Thinking Exercise
Trace the Critical Path
Write a step-by-step trace of the most important workflow in this project.
Questions to answer:
- Where could a subtle bug hide?
- What would you log to prove correctness?
5.7 The Interview Questions They’ll Ask
- “What is the core invariant this project relies on?”
- “How would you debug a failure in this workflow?”
- “What trade-offs did you make in design?”
- “How does this map to real hardware or toolchains?”
- “How do you prove your output is correct?”
5.8 Hints in Layers
Hint 1: Start small Focus on the smallest input that still demonstrates the concept.
Hint 2: Make output deterministic Fix inputs and produce stable logs before expanding functionality.
Hint 3: Validate against a known reference Compare with a known-good output or specification.
Hint 4: Add instrumentation Log internal steps so you can verify each phase explicitly.
5.9 Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| Core concept | “ARM Assembly Language” by William Hohl | Ch. 3-5 |
| Binary formats | “Linkers and Loaders” by John R. Levine | Ch. 1-3 |
5.10 Implementation Phases
Phase 1: Foundation (2-4 hours)
Goals:
- Establish a minimal working pipeline
- Validate one end-to-end path
Tasks:
- Build the smallest viable input and output
- Verify outputs against a reference Checkpoint: Output matches expected golden path
Phase 2: Core Functionality (4-8 hours)
Goals:
- Implement main logic and validation
- Add structured error handling
Tasks:
- Implement the core transformation
- Add deterministic reporting Checkpoint: Core tests pass reliably
Phase 3: Polish & Edge Cases (2-4 hours)
Goals:
- Cover edge cases
- Improve output clarity
Tasks:
- Add negative tests
- Document limitations Checkpoint: All edge cases handled gracefully
5.11 Key Implementation Decisions
| Decision | Options | Recommendation | Rationale |
|---|---|---|---|
| Input format | Free-form vs structured | Structured | Easier validation |
| Output format | Human vs machine | Both | Supports verification and tooling |
6. Testing Strategy
6.1 Test Categories
| Category | Purpose | Examples |
|---|---|---|
| Unit Tests | Validate core logic | Field parsing, bounds checks |
| Integration Tests | Validate full flow | End-to-end CLI runs |
| Edge Case Tests | Validate boundaries | Empty input, invalid flags |
6.2 Critical Test Cases
- Golden path: Fixed input produces known output.
- Invalid input: Error path triggers correct exit code.
- Boundary case: Maximum supported value handled correctly.
6.3 Test Data
Input: fixed seed or fixed fixture
Expected: exact output text from §3.4
7. Common Pitfalls & Debugging
| Pitfall | Symptom | Solution |
|---|---|---|
| Misaligned assumptions | Unexpected output | Re-check invariants |
| Missing validation | Silent failures | Add explicit checks |
| Non-determinism | Flaky output | Fix inputs and seeds |
7.2 Debugging Strategies
- Trace everything: Log each step with stable ordering
- Compare against reference: Use known-good outputs
7.3 Performance Traps
- Avoid repeated parsing of the same input; cache results when possible
8. Extensions & Challenges
8.1 Beginner Extensions
- Add one extra output format
- Add a help screen with examples
8.2 Intermediate Extensions
- Add a verification mode that compares two outputs
- Add structured JSON output
8.3 Advanced Extensions
- Add a batch mode for large inputs
- Add cross-target comparisons (M vs A profile)
9. Real-World Connections
9.1 Industry Applications
- Firmware bring-up: use the same checks to validate early boot images
- Security audits: analyze binaries for ABI or control-flow correctness
9.2 Related Open Source Projects
- binutils: source of many ARM tooling workflows
- QEMU: emulator used for ARM testing
9.3 Interview Relevance
- Explains why ARM behavior differs across profiles
- Demonstrates toolchain literacy and debugging rigor
10. Resources
10.1 Essential Reading
- “ARM Assembly Language” by William Hohl - practical instruction usage
- “Linkers and Loaders” by John R. Levine - binary layout
10.2 Video Resources
- ARM architecture overview talks and lectures
10.3 Tools & Documentation
- GNU binutils documentation
- Arm developer documentation
10.4 Related Projects in This Series
- This project connects with: P01-toolchain-pipeline-explorer.md, P02-register-stack-visualizer.md, P03-thumb-encoder-decoder.md
11. Self-Assessment Checklist
11.1 Understanding
- I can explain the core concept without notes
- I can explain why my design choices were necessary
- I can describe one realistic failure mode
11.2 Implementation
- All functional requirements are met
- Tests pass deterministically
- Edge cases are documented
11.3 Growth
- I can describe what I would improve next time
- I can explain this project in an interview
12. Submission / Completion Criteria
Minimum Viable Completion:
- Core functionality works on reference inputs
- Deterministic golden path is documented
- At least one failure path is demonstrated
Full Completion:
- All minimum criteria plus:
- Edge cases are covered with tests
- Output format is stable and documented
Excellence (Going Above & Beyond):
- Add a comparison against a second target
- Provide a short write-up of lessons learned