Project 2: ARM Assembly Calculator
Quick Reference
| Attribute | Details |
|---|---|
| Difficulty | Beginner |
| Time Estimate | Weekend (2-3 days) |
| Language | ARM Assembly (required - no alternatives) |
| Prerequisites | Basic programming concepts (variables, loops, conditionals), command line familiarity |
| Key Topics | ARM registers, system calls, arithmetic instructions, branching, ASCII conversion |
| Hardware Required | None (use QEMU emulator) or Raspberry Pi |
| Book Reference | “ARM Assembly By Example” (armasm.com) |
Learning Objectives
By completing this project, you will:
- Understand ARM registers - Know R0-R15, their purposes, and calling conventions
- Master basic ARM instructions - Use MOV, ADD, SUB, MUL, CMP, and branches fluently
- Make Linux system calls - Understand how programs interact with the OS at the lowest level
- Convert between ASCII and integers - Implement number parsing and formatting in assembly
- Control program flow - Use conditional execution and branches effectively
Theoretical Foundation
Core Concepts
1. ARM Register Set
ARM32 has 16 general-purpose registers, each 32 bits wide:
ARM Register File:
+--------+----------+----------------------------------------+
| Reg | Alias | Purpose |
+--------+----------+----------------------------------------+
| R0 | | Argument 1 / Return value |
| R1 | | Argument 2 / Return value (64-bit) |
| R2 | | Argument 3 |
| R3 | | Argument 4 |
| R4-R11 | | Callee-saved (preserved across calls) |
| R12 | IP | Intra-procedure scratch |
| R13 | SP | Stack Pointer |
| R14 | LR | Link Register (return address) |
| R15 | PC | Program Counter (current instruction) |
+--------+----------+----------------------------------------+
Special Register:
+--------+--------------------------------------------------+
| CPSR | Current Program Status Register |
| | N=Negative, Z=Zero, C=Carry, V=Overflow |
+--------+--------------------------------------------------+
Calling Convention Summary:
- R0-R3: Arguments passed to functions, return values
- R4-R11: Must be preserved if you use them (push/pop)
- R12: Scratch register
- R13/SP: Stack pointer (grows downward)
- R14/LR: Where to return to after a function call
- R15/PC: Current instruction (read: PC+8 due to pipeline)
2. ARM Instruction Syntax
ARM assembly uses a consistent format:
[label:] <instruction>{cond}{s} Rd, Rn, <Operand2>
Where:
label: Optional label for branches
cond: Optional condition code (EQ, NE, GT, etc.)
s: Optional - update condition flags
Rd: Destination register
Rn: First source register (for 3-operand instructions)
Operand2: Register, immediate, or shifted register
Examples:
MOV R0, #42 @ R0 = 42
ADD R1, R2, R3 @ R1 = R2 + R3
SUBS R0, R0, #1 @ R0 = R0 - 1, update flags
BEQ loop_end @ Branch if Zero flag set
MOVGT R0, #1 @ R0 = 1, only if Greater Than
3. Linux System Calls on ARM
ARM Linux uses the SVC #0 instruction (formerly SWI) for system calls:
System Call Convention:
+--------+------------------------------------------+
| R7 | System call number |
| R0 | Argument 1 / Return value |
| R1 | Argument 2 |
| R2 | Argument 3 |
| R3 | Argument 4 |
+--------+------------------------------------------+
Common System Calls:
+--------+--------+-----------------------------------+
| Number | Name | Arguments |
+--------+--------+-----------------------------------+
| 1 | exit | R0 = exit code |
| 3 | read | R0=fd, R1=buffer, R2=count |
| 4 | write | R0=fd, R1=buffer, R2=count |
+--------+--------+-----------------------------------+
File Descriptors:
0 = stdin (keyboard input)
1 = stdout (screen output)
2 = stderr (error output)
Example - Print “Hello”:
MOV R7, #4 @ syscall: write
MOV R0, #1 @ fd: stdout
LDR R1, =msg @ buffer address
MOV R2, #5 @ length
SVC #0 @ make syscall
msg: .ascii "Hello"
4. Data Sections
ARM assembly programs have distinct sections:
.data Section (Initialized data):
+--------------------------------------------------+
| Variables with initial values |
| Stored in the executable, loaded to RAM |
| Examples: strings, lookup tables |
+--------------------------------------------------+
.bss Section (Uninitialized data):
+--------------------------------------------------+
| Variables without initial values |
| Space reserved, zeroed at load time |
| Examples: input buffers, work arrays |
+--------------------------------------------------+
.text Section (Code):
+--------------------------------------------------+
| Executable instructions |
| Usually read-only in memory |
| Contains your program logic |
+--------------------------------------------------+
Example layout:
.data
prompt: .asciz "Enter number: " @ null-terminated string
result: .asciz "Result: "
.bss
buffer: .space 16 @ 16 bytes for input
.text
.global _start
_start:
@ your code here
5. ASCII Number Conversion
Converting between ASCII characters and numeric values:
ASCII Digit to Number:
Character '5' has ASCII value 53 (0x35)
Number 5 is just... 5
Conversion: number = ascii_char - '0'
number = ascii_char - 48
Multi-digit Parsing (e.g., "123"):
Start with accumulator = 0
For each digit:
accumulator = accumulator * 10 + digit
'1': 0 * 10 + 1 = 1
'2': 1 * 10 + 2 = 12
'3': 12 * 10 + 3 = 123
Number to ASCII (reverse process):
Divide by 10 repeatedly
Remainder is each digit (right to left)
Add '0' (48) to get ASCII
Why This Matters
Building a calculator in assembly teaches you:
- How function calls really work: You see the calling convention in action
- System call mechanics: Direct kernel interaction, no library wrappers
- Memory and registers: No variables, just registers and addresses
- Conditional execution: ARM’s unique feature for branchless code
- The cost of abstraction: Appreciate what compilers do for you
Historical Context
ARM assembly represents one of the cleanest RISC designs. The original ARM1 (1985) had:
- Simple load/store architecture
- Conditional execution on every instruction
- Barrel shifter for free shifts
- Only 30,000 transistors (vs 275,000 for Intel 386)
These design choices made ARM power-efficient, which is why it dominates mobile devices today. Writing assembly helps you understand these tradeoffs.
Project Specification
What You’ll Build
A four-function calculator entirely in ARM assembly that:
- Prompts for two numbers
- Prompts for an operator (+, -, *, /)
- Performs the calculation
- Displays the result
- Handles errors (division by zero)
Requirements
Core Requirements:
- Print prompts to stdout
- Read input from stdin
- Parse multi-digit numbers from ASCII
- Implement addition
- Implement subtraction
- Implement multiplication
- Implement division with remainder
- Convert result back to ASCII for display
- Exit cleanly with status code 0
Extended Requirements:
- Handle negative numbers
- Detect overflow conditions
- Support larger numbers (64-bit)
- Add a loop for multiple calculations
- Clear, commented code
Example Output
$ ./armcalc
Enter first number: 42
Enter operator (+, -, *, /): *
Enter second number: 13
Result: 546
$ ./armcalc
Enter first number: 100
Enter operator (+, -, *, /): /
Enter second number: 7
Result: 14 remainder 2
$ ./armcalc
Enter first number: 255
Enter operator (+, -, *, /): +
Enter second number: 256
Result: 511
$ ./armcalc
Enter first number: 50
Enter operator (+, -, *, /): /
Enter second number: 0
Error: Division by zero
Solution Architecture
High-Level Design
Program Flow
|
v
+---------------+
| _start |
| (entry point) |
+---------------+
|
v
+---------------+
| print_prompt1 |
+---------------+
|
v
+---------------+
| read_number |-----> stdin
| (first num) |
+---------------+
|
v
+---------------+
| print_prompt2 |
+---------------+
|
v
+---------------+
| read_operator |-----> stdin
+---------------+
|
v
+---------------+
| print_prompt3 |
+---------------+
|
v
+---------------+
| read_number |-----> stdin
| (second num) |
+---------------+
|
v
+---------------+
| dispatch_op |
+-------+-------+
|
+-------+-------+-------+-------+
| | | | |
v v v v v
do_add do_sub do_mul do_div error
| | | | |
+-------+-------+-------+-------+
|
v
+---------------+
| print_result |-----> stdout
+---------------+
|
v
+---------------+
| exit |
+---------------+
Register Usage Plan
Register Allocation:
+------+--------------------------------+
| R0 | System call arg / temp |
| R1 | System call arg / temp |
| R2 | System call arg / temp |
| R3 | temp |
| R4 | First operand (preserved) |
| R5 | Second operand (preserved) |
| R6 | Operator character (preserved) |
| R7 | System call number |
| R8 | Result (preserved) |
| R9 | Remainder for division |
+------+--------------------------------+
Note: We use R4-R9 for our values because they're
callee-saved - we don't need to worry about system
calls clobbering them.
Data Section Layout
.data
prompt1: .asciz "Enter first number: "
.equ prompt1_len, . - prompt1 - 1
prompt2: .asciz "Enter operator (+, -, *, /): "
.equ prompt2_len, . - prompt2 - 1
prompt3: .asciz "Enter second number: "
.equ prompt3_len, . - prompt3 - 1
result_msg: .asciz "Result: "
.equ result_len, . - result_msg - 1
remainder_msg: .asciz " remainder "
.equ remainder_len, . - remainder_msg - 1
newline: .ascii "\n"
error_msg: .asciz "Error: Division by zero\n"
.equ error_len, . - error_msg - 1
.bss
input_buf: .space 16 @ Buffer for number input
output_buf: .space 16 @ Buffer for number output
Implementation Guide
Environment Setup
Option 1: QEMU Emulator (Recommended for beginners)
# Ubuntu/Debian
sudo apt install qemu-user gcc-arm-linux-gnueabihf
# Assemble
arm-linux-gnueabihf-as -o calc.o calc.s
# Link
arm-linux-gnueabihf-ld -o armcalc calc.o
# Run with QEMU
qemu-arm ./armcalc
Option 2: Raspberry Pi (Native)
# On Raspberry Pi (already ARM)
as -o calc.o calc.s
ld -o armcalc calc.o
./armcalc
Option 3: Cross-compilation toolchain
# Install toolchain
sudo apt install binutils-arm-none-eabi
# For bare-metal or with newlib
arm-none-eabi-as -o calc.o calc.s
arm-none-eabi-ld -o armcalc calc.o
Project Structure
arm-calculator/
+-- calc.s # Main assembly source
+-- Makefile # Build automation
+-- README.md # Documentation
+-- test.sh # Test script
Makefile:
AS = arm-linux-gnueabihf-as
LD = arm-linux-gnueabihf-ld
QEMU = qemu-arm
TARGET = armcalc
$(TARGET): calc.o
$(LD) -o $@ $<
calc.o: calc.s
$(AS) -o $@ $<
run: $(TARGET)
$(QEMU) ./$(TARGET)
clean:
rm -f *.o $(TARGET)
test: $(TARGET)
./test.sh
Implementation Hints in Layers
Hint 1: Starting Point - Hello World
Before building the calculator, make sure you can print a string:
.global _start
.text
_start:
@ Write "Hello, ARM!" to stdout
MOV R7, #4 @ syscall: write
MOV R0, #1 @ fd: stdout
LDR R1, =hello @ buffer address
MOV R2, #11 @ length
SVC #0 @ make syscall
@ Exit with code 0
MOV R7, #1 @ syscall: exit
MOV R0, #0 @ exit code
SVC #0
.data
hello: .ascii "Hello, ARM!"
If this works, you understand system calls. Now add input.
Hint 2: Reading Input
Read a line from stdin:
read_input:
@ Read into buffer
MOV R7, #3 @ syscall: read
MOV R0, #0 @ fd: stdin
LDR R1, =input_buf @ buffer
MOV R2, #16 @ max bytes
SVC #0 @ R0 now contains bytes read
@ R0 = number of bytes read (including newline)
MOV PC, LR @ return
Hint 3: ASCII to Integer Conversion
The key algorithm for parsing numbers:
@ Convert ASCII string at R1 to integer, result in R0
@ Stops at first non-digit
ascii_to_int:
PUSH {R4, R5, LR}
MOV R0, #0 @ accumulator = 0
MOV R4, #10 @ multiplier
parse_loop:
LDRB R5, [R1], #1 @ load byte, increment pointer
SUB R5, R5, #'0' @ convert ASCII to digit
CMP R5, #9 @ is it > 9?
BHI parse_done @ if so, not a digit, done
MUL R0, R0, R4 @ accumulator *= 10
ADD R0, R0, R5 @ accumulator += digit
B parse_loop
parse_done:
POP {R4, R5, PC} @ return (PC = LR)
Key insight: SUB R5, R5, #'0' converts ‘5’ (ASCII 53) to 5.
If the result is > 9, it wasn’t a digit (like newline = 10 - 48 = negative).
Hint 4: Arithmetic Operations
Implement each operation:
@ Addition: R4 + R5 -> R8
do_add:
ADD R8, R4, R5
B show_result
@ Subtraction: R4 - R5 -> R8
do_sub:
SUB R8, R4, R5
B show_result
@ Multiplication: R4 * R5 -> R8
do_mul:
MUL R8, R4, R5
B show_result
@ Division: R4 / R5 -> R8, R4 % R5 -> R9
@ Note: ARMv7 has UDIV, older ARM needs software division
do_div:
@ Check for division by zero
CMP R5, #0
BEQ div_by_zero
@ On ARMv7+:
UDIV R8, R4, R5 @ quotient
MUL R9, R8, R5 @ quotient * divisor
SUB R9, R4, R9 @ remainder = dividend - (quotient * divisor)
B show_result
@ For older ARM without UDIV, use repeated subtraction:
div_software:
MOV R8, #0 @ quotient = 0
div_loop:
CMP R4, R5 @ dividend >= divisor?
BLT div_done
SUB R4, R4, R5 @ dividend -= divisor
ADD R8, R8, #1 @ quotient++
B div_loop
div_done:
MOV R9, R4 @ remainder = what's left
B show_result
Hint 5: Integer to ASCII Conversion
Converting the result back to printable text:
@ Convert integer in R0 to ASCII string at R1
@ Returns length in R2
int_to_ascii:
PUSH {R4-R6, LR}
MOV R4, R0 @ save number
ADD R1, R1, #15 @ start at end of buffer
MOV R2, #0 @ length counter
MOV R5, #10 @ divisor
@ Handle zero specially
CMP R4, #0
BNE convert_loop
MOV R6, #'0'
STRB R6, [R1], #-1
MOV R2, #1
B convert_done
convert_loop:
CMP R4, #0
BEQ convert_done
@ R4 / 10, remainder in R6
@ On ARMv7:
UDIV R6, R4, R5 @ R6 = R4 / 10
MUL R3, R6, R5 @ R3 = quotient * 10
SUB R3, R4, R3 @ R3 = remainder
MOV R4, R6 @ R4 = quotient for next iteration
ADD R3, R3, #'0' @ convert to ASCII
STRB R3, [R1], #-1 @ store and decrement pointer
ADD R2, R2, #1 @ length++
B convert_loop
convert_done:
ADD R1, R1, #1 @ point to first digit
POP {R4-R6, PC}
Interview Questions You Should Be Able to Answer
After completing this project, you should confidently answer:
- “What registers are used for function arguments in ARM?”
- R0-R3 for first four arguments, stack for additional
- Return value in R0 (or R0-R1 for 64-bit)
- “How do you make a system call in ARM Linux?”
- Put syscall number in R7
- Arguments in R0-R6
- Execute SVC #0
- Return value in R0
- “What’s the difference between MOV and LDR?”
- MOV loads immediate or register value
- LDR loads from memory address
- MOV limited to values encodable in 12 bits
- “Explain ARM conditional execution”
- Most instructions can have condition suffix (ADDEQ, MOVNE)
- Executes based on CPSR flags (N, Z, C, V)
- Avoids branches, better pipeline efficiency
- “How would you implement a loop in ARM assembly?”
- Set up counter in register
- Loop body
- Decrement counter
- CMP and BNE (or SUBS and BNE)
- “What’s the Link Register (LR) for?”
- Stores return address when BL (branch-link) is executed
- MOV PC, LR returns from function
- Must be saved if you call another function
Books That Will Help
| Topic | Book | Chapter |
|---|---|---|
| ARM Registers & Instructions | “ARM Assembly By Example” (armasm.com) | All |
| Linux System Calls | “Introduction to Computer Organization: ARM Edition” - Plantz | Chapter 7 |
| ARM Assembly Fundamentals | Azeria Labs “Writing ARM Assembly” | Parts 1-3 |
| ARM Multiply Instructions | “The Art of ARM Assembly, Vol 1” - Hyde | Chapter 7 |
| Calling Conventions | ARM Procedure Call Standard (AAPCS) | Official Spec |
Testing Strategy
Test Script
Create test.sh:
#!/bin/bash
echo "=== ARM Calculator Tests ==="
# Test addition
echo -e "5\n+\n3" | qemu-arm ./armcalc | grep -q "8"
if [ $? -eq 0 ]; then echo "ADD: PASS"; else echo "ADD: FAIL"; fi
# Test subtraction
echo -e "10\n-\n3" | qemu-arm ./armcalc | grep -q "7"
if [ $? -eq 0 ]; then echo "SUB: PASS"; else echo "SUB: FAIL"; fi
# Test multiplication
echo -e "6\n*\n7" | qemu-arm ./armcalc | grep -q "42"
if [ $? -eq 0 ]; then echo "MUL: PASS"; else echo "MUL: FAIL"; fi
# Test division
echo -e "20\n/\n4" | qemu-arm ./armcalc | grep -q "5"
if [ $? -eq 0 ]; then echo "DIV: PASS"; else echo "DIV: FAIL"; fi
# Test division with remainder
echo -e "22\n/\n7" | qemu-arm ./armcalc | grep -q "remainder 1"
if [ $? -eq 0 ]; then echo "DIV+REM: PASS"; else echo "DIV+REM: FAIL"; fi
# Test division by zero
echo -e "10\n/\n0" | qemu-arm ./armcalc | grep -q "Error"
if [ $? -eq 0 ]; then echo "DIV0: PASS"; else echo "DIV0: FAIL"; fi
# Test multi-digit
echo -e "123\n+\n456" | qemu-arm ./armcalc | grep -q "579"
if [ $? -eq 0 ]; then echo "MULTI-DIGIT: PASS"; else echo "MULTI-DIGIT: FAIL"; fi
echo "=== Tests Complete ==="
Manual Testing
# Interactive testing
qemu-arm ./armcalc
# Enter: 42, *, 13
# Expect: Result: 546
# Edge cases to test manually
# - 0 + 0 = 0
# - 0 * anything = 0
# - Large numbers: 999 * 999 = 998001
# - Subtraction going negative (if supported)
Debugging with QEMU
# Run with GDB server
qemu-arm -g 1234 ./armcalc &
# In another terminal
gdb-multiarch ./armcalc
(gdb) target remote :1234
(gdb) break _start
(gdb) continue
(gdb) info registers
(gdb) stepi
Common Pitfalls & Debugging
| Problem | Symptom | Root Cause | Fix |
|---|---|---|---|
| Segmentation fault | Crash on start | Wrong stack setup or invalid memory access | Check LDR addresses, ensure labels exist |
| Nothing prints | Silent execution | Wrong syscall number or fd | R7=4 for write, R0=1 for stdout |
| Input not read | Hangs forever | Wrong syscall or buffer | R7=3 for read, R0=0 for stdin |
| Wrong number parsed | 42 becomes 4 | Stopping at wrong character | Check loop termination condition |
| Multiplication wrong | Garbage result | Register clobbered | Use callee-saved registers (R4-R11) |
| Division crash | Illegal instruction | No UDIV on older ARM | Use software division loop |
| Infinite loop | Never exits | Wrong branch condition | Check CMP before B, use correct condition |
Debugging Techniques
@ Debug helper: Print R0 as hex (destructive)
debug_r0:
PUSH {R0-R3, R7, LR}
@ Convert R0 to hex string and print
@ ... implementation ...
POP {R0-R3, R7, PC}
@ Debug helper: Print a marker character
debug_marker:
PUSH {R0-R2, R7, LR}
MOV R7, #4
MOV R0, #1
ADR R1, marker
MOV R2, #2
SVC #0
POP {R0-R2, R7, PC}
marker: .ascii "X\n"
Use strace to see system calls:
qemu-arm -strace ./armcalc
Extensions & Challenges
After Basic Completion
- Support negative numbers: Parse leading ‘-‘, use signed arithmetic
- Add more operations: Modulo (%), power (^), bitwise (AND, OR, XOR)
- Multiple calculations: Loop until user types ‘q’
- Expression parsing: Handle “5 + 3 * 2” with precedence
- Better error handling: Invalid input, overflow detection
Challenge Mode
- Implement arbitrary precision arithmetic (big integers)
- Add floating-point support (VFP instructions)
- Create a reverse Polish notation (RPN) calculator
- Add memory functions (M+, M-, MR, MC)
Optimization Challenge
Make your code smaller:
# Check code size
arm-linux-gnueabihf-size armcalc
# Try to get under 500 bytes of .text
Self-Assessment Checklist
Before moving to Project 3, verify you can:
- Explain what each ARM register (R0-R15) is typically used for
- Write a simple ARM program that prints a string
- Read input from the keyboard in ARM assembly
- Convert ASCII digits to a number without looking at notes
- Implement a loop that counts down from 10 to 0
- Use conditional execution (like MOVEQ)
- Make a function call with BL and return with MOV PC, LR
- Use PUSH and POP to save/restore registers
Final Test
Without looking at your code, write from scratch:
- A subroutine that takes a number in R0 and prints it as decimal
- Use proper register saving (PUSH/POP)
- Handle multi-digit numbers (0-999)
If you can do this, you’re ready for bare-metal programming!
Learning Milestones
Track your progress through these milestones:
| Milestone | You Can… | Understanding Level |
|---|---|---|
| 1 | Print “Hello, ARM!” | You understand system calls and basic structure |
| 2 | Read a number from input | You understand data sections and I/O |
| 3 | Perform calculations on registers | You understand arithmetic instructions |
| 4 | Handle all four operations | You understand branching and program flow |
Sample Code Skeleton
Here’s a skeleton to get you started:
.global _start
.text
_start:
@ === Print first prompt ===
MOV R7, #4 @ write syscall
MOV R0, #1 @ stdout
LDR R1, =prompt1
LDR R2, =prompt1_len
SVC #0
@ === Read first number ===
MOV R7, #3 @ read syscall
MOV R0, #0 @ stdin
LDR R1, =input_buf
MOV R2, #16
SVC #0
@ === Convert to integer ===
LDR R1, =input_buf
BL ascii_to_int
MOV R4, R0 @ save first number in R4
@ === TODO: Read operator ===
@ === TODO: Read second number ===
@ === TODO: Dispatch based on operator ===
@ === TODO: Print result ===
@ === Exit ===
MOV R7, #1 @ exit syscall
MOV R0, #0 @ exit code 0
SVC #0
@ ============================================
@ Subroutine: ASCII to integer
@ Input: R1 = pointer to ASCII string
@ Output: R0 = integer value
@ ============================================
ascii_to_int:
@ Your implementation here
MOV R0, #0
MOV PC, LR
@ ============================================
@ Data Section
@ ============================================
.data
prompt1: .asciz "Enter first number: "
prompt1_len: .word . - prompt1 - 1
.bss
input_buf: .space 16
| <- Previous Project: ARM Instruction Decoder | Next Project: Bare-Metal LED Blinker -> |