Project 2: ARM Assembly Calculator

Back to ARM Deep Dive


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:

  1. Understand ARM registers - Know R0-R15, their purposes, and calling conventions
  2. Master basic ARM instructions - Use MOV, ADD, SUB, MUL, CMP, and branches fluently
  3. Make Linux system calls - Understand how programs interact with the OS at the lowest level
  4. Convert between ASCII and integers - Implement number parsing and formatting in assembly
  5. 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:

  1. Prompts for two numbers
  2. Prompts for an operator (+, -, *, /)
  3. Performs the calculation
  4. Displays the result
  5. 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:

  1. “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)
  2. “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
  3. “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
  4. “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
  5. “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)
  6. “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

  1. Support negative numbers: Parse leading ‘-‘, use signed arithmetic
  2. Add more operations: Modulo (%), power (^), bitwise (AND, OR, XOR)
  3. Multiple calculations: Loop until user types ‘q’
  4. Expression parsing: Handle “5 + 3 * 2” with precedence
  5. 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:

  1. A subroutine that takes a number in R0 and prints it as decimal
  2. Use proper register saving (PUSH/POP)
  3. 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 ->