Debugging Assembly Code
Why Debugging Matters
Assembly code can be challenging to debug because you're working at such a low level. Small mistakes can lead to unexpected behavior. Learning systematic debugging techniques will save you hours of frustration.
Common Assembly Bugs
1. Off-by-One Errors
Loop counters and memory addresses are prone to off-by-one errors:
// Bug: Loops 11 times instead of 10
MOV R0, #0
MOV R1, #10
ADD R0, R0, #1 // Line 3
CMP R0, R1
BLE 3 // Wrong: should be BLT
HALT
2. Wrong Register Usage
Using the wrong register or overwriting needed values:
// Bug: R0 gets overwritten
MOV R0, #5
MOV R1, #10
ADD R2, R0, R1 // R2 = 15
MOV R0, #20 // Oops! Lost original R0
MUL R3, R0, R2 // Wrong calculation
3. Incorrect Branch Targets
Branching to the wrong line number:
MOV R0, #0
CMP R0, #0
BEQ 5 // Bug: Line 5 might not exist!
MOV R1, #10
HALT
4. Flag Misunderstanding
Using the wrong conditional branch:
MOV R0, #5
CMP R0, #10
BGT 5 // Bug: 5 is NOT > 10
MOV R1, #100 // This executes
HALT
Debugging Techniques
1. Use Step Mode
The most powerful debugging tool is the Step button:
- Execute one instruction at a time
- Watch registers and flags update after each step
- Verify each instruction does what you expect
- Check the Program Counter to see which line executes next
2. Add Temporary Output Instructions
Store intermediate values in memory to track program state:
MOV R0, #5
STR R0, 900 // Save R0 for inspection
MUL R1, R0, R0
STR R1, 901 // Save R1 for inspection
ADD R2, R1, #10
STR R2, 902 // Save R2 for inspection
HALT
3. Divide and Conquer
If your program isn't working, comment out parts to isolate the problem:
// Test first part only
MOV R0, #10
ADD R0, R0, #5
HALT // Add temporary HALT
// MOV R1, #20 // Comment out rest
// ADD R2, R0, R1
4. Check Your Math
Manually calculate what values should be in registers:
// Expected: R2 = 30
MOV R0, #10 // R0 should be 10
MOV R1, #20 // R1 should be 20
ADD R2, R0, R1 // R2 should be 30
// Use Step mode to verify each value
Using the Compiler's Debug Features
Register Panel
- Changed registers are highlighted
- Click any register to edit its value
- Test "what if" scenarios by modifying values mid-execution
Flags Panel
- Watch which flags are set after CMP and arithmetic operations
- Click flags to manually test conditional branches
- Verify your understanding of how flags work
Memory View
- Check that STR instructions write to correct addresses
- Verify LDR instructions read expected values
- Change start address to view different memory regions
- Click memory locations to edit values for testing
Program Counter
- Shows which line will execute next
- Helps verify branches go to correct locations
- Useful for tracking loop iterations
Debugging Strategies for Common Problems
Infinite Loops
If your program doesn't halt:
- Check loop counters are incrementing/decrementing
- Verify loop exit condition is correct
- Ensure branch target is inside the loop
// Fixed infinite loop
MOV R0, #0
MOV R1, #5
ADD R0, R0, #1 // Line 3: Make sure this runs!
CMP R0, R1
BLT 3 // Goes back to line 3
HALT
Wrong Results
If calculations are incorrect:
- Verify input values are loaded correctly
- Check instruction order (dependency issues)
- Ensure you're not overwriting needed values
- Step through and compare actual vs expected values
Crashes or Unexpected Behavior
- Check for invalid memory addresses
- Verify branch targets exist
- Ensure all registers are initialized before use
Systematic Debugging Process
- Reproduce: Make sure you can consistently see the bug
- Isolate: Narrow down which part of code has the problem
- Understand: Figure out what the code is actually doing
- Hypothesize: Form a theory about what's wrong
- Test: Use Step mode to verify your hypothesis
- Fix: Make the minimal change to fix the bug
- Verify: Confirm the bug is gone and nothing else broke
Prevention Tips
- Comment generously: Explain what each section does
- Use consistent formatting: Makes errors more visible
- Test incrementally: Don't write everything before testing
- Start simple: Test with simple input values first
- Document register usage: Note what each register contains
Example: Debugging a Factorial Function
// Buggy factorial calculator
MOV R0, #5 // Input: calculate 5!
MOV R1, #1 // Result
MOV R2, #1 // Counter
// Loop at line 4
MUL R1, R1, R2 // result *= counter
ADD R2, R2, #1 // counter++
CMP R2, R0 // Compare counter with input
BLE 4 // Bug: should be BLT!
// Result in R1
HALT
The bug: BLE loops one extra time. When R2 equals R0, it should stop, but BLE (branch if less than or equal) continues. Fix: Change to BLT.
Next Steps
With these debugging techniques, you'll be able to find and fix bugs more efficiently. Continue learning about the compiler's features to leverage all available debugging tools.