This tutorial walks through practical debugging sessions using the ARM2 Emulator's debugger with real example programs. It covers both command-line debug mode (--debug) and TUI mode (--tui).
- Build the emulator:
go build -o arm-emulator - Example programs are in the
examples/directory - See Debugger Reference for complete command documentation
The times table program reads a number and prints its multiplication table from 1 to 12. We'll use the debugger to understand how it works.
# Command-line mode
./arm-emulator --debug examples/times_table.s
# Or TUI mode (recommended for beginners)
./arm-emulator --tui examples/times_table.s(debugger) list
This shows the source code around the entry point. You'll see the main label and initial instructions.
(debugger) break main
Breakpoint 1 at 0x0000 (main)
(debugger) info breakpoints
Num Type Enabled Address Location
1 BP yes 0x0000 main
(debugger) run
Breakpoint 1 hit at 0x0000 (main)
(debugger) list
7: main:
8: ; Print prompt message
9: LDR r0, =prompt_msg
10: BL print_string
11:
12: ; Read number from input
13: BL read_int
(debugger) info registers
R0: 0x00000000 (0) R8: 0x00000000 (0)
R1: 0x00000000 (0) R9: 0x00000000 (0)
R2: 0x00000000 (0) R10: 0x00000000 (0)
R3: 0x00000000 (0) R11: 0x00000000 (0)
R4: 0x00000000 (0) R12: 0x00000000 (0)
R5: 0x00000000 (0) SP: 0xFFFF0000
R6: 0x00000000 (0) LR: 0x00000000
R7: 0x00000000 (0) PC: 0x00000000
CPSR: N=0 Z=0 C=0 V=0
All registers start at zero, SP is at the top of the stack.
(debugger) step
PC: 0x0004
(debugger) print R0
R0 = 0x0060 (96) ; Address of prompt_msg string
(debugger) x/s R0
"Enter a number (1-12): "
The LDR r0, =prompt_msg loaded the string address into R0.
(debugger) next
Enter a number (1-12): _
The BL print_string call was executed, printing the prompt. Using next (not step) stepped over the function call.
(debugger) break loop
Breakpoint 2 at 0x001C (loop)
(debugger) continue
5
After typing "5" and pressing Enter, execution continues to the loop breakpoint.
(debugger) print R4
R4 = 0x00000005 (5) ; The input number
(debugger) print R5
R5 = 0x00000001 (1) ; Loop counter
(debugger) watch R5
Watchpoint 1: R5 (write)
We're watching R5, which is the loop counter (1-12).
(debugger) continue
5 x 1 = 5
Watchpoint 1 hit: R5 changed from 1 to 2
PC: 0x0048 (after ADD r5, r5, #1)
(debugger) continue
5 x 2 = 10
Watchpoint 1 hit: R5 changed from 2 to 3
The watchpoint triggers every time R5 is incremented.
Let's break when we reach the 10th iteration:
(debugger) delete 2 # Remove old loop breakpoint
(debugger) break loop if R5 == 10
Breakpoint 3 at 0x001C (loop) if R5 == 10
(debugger) continue
5 x 3 = 15
5 x 4 = 20
...
5 x 9 = 45
5 x 10 = 50
Breakpoint 3 hit at 0x001C (loop)
(debugger) print R5
R5 = 0x0000000A (10)
The conditional breakpoint only triggers when R5 equals 10.
(debugger) print R4
R4 = 0x00000005 (5) ; Input number
(debugger) print R5
R5 = 0x0000000A (10) ; Counter
(debugger) step
(debugger) step
(debugger) step
(debugger) print R6
R6 = 0x00000032 (50) ; Result: 5 * 10 = 50
(debugger) delete 1 # Remove watchpoint
(debugger) delete 3 # Remove conditional breakpoint
(debugger) continue
5 x 11 = 55
5 x 12 = 60
Program exited with code 0
The TUI mode provides a visual interface that makes debugging easier:
./arm-emulator --tui examples/times_table.s┌─────────────────────────────────────────────────────────────┐
│ Source View │
│ 7: main: │
│ 8: ; Print prompt message │
│ 9: > LDR r0, =prompt_msg <- Current line │
│ 10: BL print_string │
│ 11: │
├──────────────────────────┬──────────────────────────────────┤
│ Registers │ Memory View │
│ R0: 0x00000060 (96) │ 00008000: E3 A0 00 60 │
│ R4: 0x00000000 (0) │ 00008010: EB 00 00 10 │
│ R5: 0x00000000 (0) │ (written bytes shown in green) │
│ R6: 0x00000000 (0) │ │
│ PC: 0x00000000 │ │
│ CPSR: ---- │ │
│ (changed in green) │ │
├──────────────────────────┴──────────────────────────────────┤
│ Stack View │ Breakpoints │ Watchpoints │
│ SP: 0xFFFF0000 │ 1: loop (on) │ │
│ FFFF0000: 00 00 00 00 │ │ │
│ (changes in green) │ │ │
├─────────────────────────────────────────────────────────────┤
│ Console Output │
│ Debugger started. Type 'help' for commands. │
└─────────────────────────────────────────────────────────────┘
(debugger) _
- F9 at line 19 (loop:) to set a breakpoint
- F5 to run the program
- Enter "5" when prompted
- F10 to step over instructions
- F11 to step into function calls
- F6 to center the current PC line in Source and Disassembly views (useful if you've scrolled away)
- Tab / Shift+Tab to switch between panels
- ↑/↓ to scroll within the focused panel
- PgUp/PgDn to scroll by page, Home/End to jump to start/end
- Ctrl+L to refresh the display if needed
- Type commands at the bottom prompt
- Real-time visual feedback: See registers update in real-time with green highlighting
- Memory write tracking: Written memory bytes highlighted in green, auto-scrolls to show changes
- Stack monitoring: PUSH/POP operations highlighted in green
- Symbol-aware display: Shows function/label names instead of raw addresses
- Visual current line indicator:
>marks the instruction about to execute - Scrollable views: Navigate through source code, memory, and stack with arrow keys and PgUp/PgDn
- PC centering: Press F6 to bring the current instruction back into view
- No repeated commands: No need to type
info registersrepeatedly - Easy breakpoint management: F9 to toggle breakpoints at current line
- Multi-panel layout: Split-screen view of code, registers, memory, stack, breakpoints, and console
Let's debug a more complex program that evaluates expressions.
./arm-emulator --debug examples/calculator.s(debugger) list
1: ; Simple calculator
2: ; Reads two numbers and an operator, performs calculation
3:
4: main:
5: LDR r0, =prompt1
6: BL print_string
(debugger) break main
(debugger) break read_int
(debugger) break add_op
(debugger) break sub_op
(debugger) break mul_op
(debugger) break div_op
(debugger) info breakpoints
Num Type Enabled Address Location
1 BP yes 0x0000 main
2 BP yes 0x0020 read_int
3 BP yes 0x0040 add_op
4 BP yes 0x0050 sub_op
5 BP yes 0x0060 mul_op
6 BP yes 0x0070 div_op
(debugger) run
Breakpoint 1 hit at 0x0000 (main)
(debugger) continue
Enter first number: 10
Breakpoint 2 hit at 0x0020 (read_int)
(debugger) continue
Enter operator (+,-,*,/): +
Enter second number: 25
Breakpoint 3 hit at 0x0040 (add_op)
(debugger) print R4
R4 = 0x0000000A (10) ; First number
(debugger) print R5
R5 = 0x00000019 (25) ; Second number
(debugger) step
(debugger) step
(debugger) print R6
R6 = 0x00000023 (35) ; Result: 10 + 25 = 35
(debugger) backtrace
#0 0x0040 in add_op
#1 0x0018 in main
This shows the call stack - we're currently in add_op, called from main.
(debugger) reset
(debugger) run
Enter first number: 10
Enter operator (+,-,*,/): /
Enter second number: 0
(debugger) break div_op
(debugger) continue
Breakpoint at 0x0070 (div_op)
(debugger) print R5
R5 = 0x00000000 (0) ; Divisor is zero!
(debugger) step
Error: Division by zero!
This program sums an array of numbers.
./arm-emulator --debug examples/array_sum.s(debugger) break main
(debugger) run
(debugger) print array
array = 0x0100
(debugger) x/10d array
0x0100: 5 ; array[0]
0x0104: 10 ; array[1]
0x0108: 15 ; array[2]
0x010C: 20 ; array[3]
0x0110: 25 ; array[4]
0x0114: 30 ; array[5]
0x0118: 35 ; array[6]
0x011C: 40 ; array[7]
0x0120: 45 ; array[8]
0x0124: 50 ; array[9]
(debugger) watch [0x0108] ; Watch array[2]
Watchpoint 1: [0x0108] (write)
(debugger) continue
If any instruction writes to address 0x0108, the watchpoint will trigger.
(debugger) break sum_loop
(debugger) run
(debugger) print R0
R0 = 0x00000000 (0) ; Accumulator
(debugger) print R1
R1 = 0x00000100 (256) ; Array pointer
(debugger) print R2
R2 = 0x0000000A (10) ; Array length
(debugger) step
(debugger) step
(debugger) step
(debugger) print R0
R0 = 0x00000005 (5) ; Sum after first element
(debugger) print R1
R1 = 0x00000104 (260) ; Pointer advanced by 4 bytes
(debugger) print [R1]
[R1] = 0x0000000A (10) ; Value at current pointer
(debugger) print R0 + [R1]
35 ; Sum + next value
(debugger) print R2 - 1
9 ; Remaining iterations
Fibonacci is a recursive function, perfect for practicing call stack debugging.
./arm-emulator --debug examples/fibonacci.s(debugger) break fib
Breakpoint 1 at fib
(debugger) run
Enter N: 5
Breakpoint 1 hit at fib
(debugger) backtrace
#0 0x0020 in fib
#1 0x0010 in main
(debugger) print R0
R0 = 0x00000005 (5) ; fib(5)
(debugger) continue
Breakpoint 1 hit at fib
(debugger) backtrace
#0 0x0020 in fib
#1 0x0028 in fib
#2 0x0010 in main
(debugger) print R0
R0 = 0x00000004 (4) ; fib(4) - recursive call
(debugger) continue
Breakpoint 1 hit at fib
(debugger) backtrace
#0 0x0020 in fib
#1 0x0028 in fib
#2 0x0028 in fib
#3 0x0010 in main
(debugger) print R0
R0 = 0x00000003 (3) ; fib(3) - deeper recursion
(debugger) info stack
SP: 0xFFFEFFE0
Base: 0xFFFF0000
Used: 32 bytes
Stack contents:
0xFFFEFFE0: 0x00000028 ; Return address (fib)
0xFFFEFFE4: 0x00000003 ; Saved R0
0xFFFEFFE8: 0x00000028 ; Return address (fib)
0xFFFEFFEC: 0x00000004 ; Saved R0
0xFFFEFFF0: 0x00000028 ; Return address (fib)
0xFFFEFFF4: 0x00000005 ; Saved R0
(debugger) finish
Returned to 0x0028
(debugger) print R0
R0 = 0x00000002 (2) ; fib(3) returned 2
(debugger) backtrace
#0 0x0028 in fib ; Now back up one level
#1 0x0028 in fib
#2 0x0010 in main
Let's say you have a buggy program that's producing incorrect output.
- Identify the symptom: Wrong output value
- Set breakpoints: At key calculation points
- Examine state: Check registers and memory
- Step through: Execute line by line
- Find the cause: Compare expected vs actual values
(debugger) break calculate_average
(debugger) run
(debugger) print R0
R0 = 0x00000064 (100) ; Sum
(debugger) print R1
R1 = 0x00000005 (5) ; Count
(debugger) step
(debugger) step
(debugger) step
(debugger) print R2
R2 = 0x00000014 (20) ; Result: 100 / 5 = 20 ✓
; But the output shows 25! Let's keep investigating...
(debugger) step
(debugger) print R2
R2 = 0x00000019 (25) ; Aha! Something changed it!
(debugger) list
50: DIV R2, R0, R1 ; Calculate average
51: ADD R2, R2, #5 ; ← BUG! Accidentally adding 5
52: MOV R0, R2
Found the bug! There's an extra ADD R2, R2, #5 that shouldn't be there.
TUI mode is more intuitive for learning:
./arm-emulator --tui program.sInstead of hitting a breakpoint 100 times:
(debugger) break loop if R0 >= 100
Watch a critical variable and break when conditions are met:
(debugger) watch result
(debugger) break check_result if [result] == 0
step(F11): Goes into function callsnext(F10): Steps over function calls
Use next when you trust the function; use step when you need to debug it.
Instead of stepping through an entire function:
(debugger) finish
(debugger) x/4x array ; Hexadecimal
(debugger) x/4d array ; Decimal
(debugger) x/s string ; String
(debugger) x/4t flags ; Binary
(debugger) print (R0 + R1) * 2
(debugger) print [SP + 8]
(debugger) print R0 & 0xFF
When execution is in an unexpected place:
(debugger) backtrace
Made a mistake? Reset the VM:
(debugger) reset
(debugger) run
For complex issues, use tracing alongside debugging:
./arm-emulator --debug --trace --trace-file debug.log program.sThe emulator provides several diagnostic modes to help analyze program behavior:
# Code coverage - see which instructions were executed
./arm-emulator --coverage program.s
# Stack trace - monitor stack operations and detect issues
./arm-emulator --stack-trace program.s
# Flag trace - track CPSR flag changes
./arm-emulator --flag-trace program.s
# Register trace - analyze register usage patterns
./arm-emulator --register-trace program.s
# Combine multiple modes
./arm-emulator --coverage --stack-trace --flag-trace --register-trace program.sThese modes can help identify:
- Dead code (coverage)
- Stack overflow/underflow (stack trace)
- Incorrect conditional logic (flag trace)
- Uninitialized registers or inefficient register usage (register trace)
(debugger) break loop_start
(debugger) watch loop_counter
(debugger) continue
; Check if loop_counter is being updated correctly
(debugger) break before_calculation
(debugger) step
; Step through the calculation line by line
; Print intermediate values
(debugger) break main
(debugger) run
(debugger) continue
; Check if program exits normally or crashes
; Examine exit code with: print R0
(debugger) break function_name
(debugger) run
; If breakpoint never hits, check the caller
(debugger) break caller
; Examine the branch/call instruction
(debugger) watch [suspicious_address]
(debugger) run
; When watchpoint hits, check what wrote to memory
(debugger) backtrace
| Shortcut | Action |
|---|---|
| F1 | Show help |
| F5 | Continue execution (run/continue) |
| F9 | Toggle breakpoint at current line |
| F10 | Step over (next) - executes one instruction, stepping over calls |
| F11 | Step into (step) - executes one instruction, stepping into calls |
| Ctrl+C | Stop/interrupt program |
| Ctrl+L | Refresh display (useful if display gets corrupted) |
| Tab | Switch between panels (Source, Registers, Memory, Stack, etc.) |
| ↑/↓ | Command history / scroll in panels |
| PgUp/PgDn | Scroll active panel |
TUI Visual Indicators:
- Green highlighting: Changed registers, written memory bytes, stack operations
>symbol: Current line about to execute- Symbol names: Function/label names shown in source and other panels
- Auto-scroll: Memory window automatically scrolls to show written addresses
- Read the Debugger Reference for complete command documentation
- Try debugging the example programs in
examples/ - Experiment with watchpoints and conditional breakpoints
- Practice using the TUI mode keyboard shortcuts
- Debugger Reference - Complete command reference
- TUTORIAL.md - Learn ARM2 assembly from scratch
- Instruction Set Reference - ARM2 CPU instructions and syscalls
- Assembler Directives - .text, .data, .word, .ltorg, etc.
- Programming Reference - Condition codes, addressing modes, shifts
- Assembly Reference - Comprehensive assembly syntax guide
- Examples - 49 sample programs to practice debugging
- FAQ - Common questions and troubleshooting