Project 1: ARM Instruction Decoder & Disassembler

Back to ARM Deep Dive


Quick Reference

Attribute Details
Difficulty Intermediate
Time Estimate 1-2 weeks
Language C (primary), Rust/Python/Go (alternatives)
Prerequisites C programming fundamentals, binary/hexadecimal, basic assembly concepts
Key Topics Binary parsing, instruction encoding, bitwise operations, opcode tables
Hardware Required None (software only)
Book Reference “The Art of ARM Assembly, Volume 1” by Randall Hyde

Learning Objectives

By completing this project, you will:

  1. Understand ARM instruction encoding - Learn how 32-bit ARM instructions are structured with condition codes, opcodes, register fields, and operands
  2. Master bitwise operations - Extract specific bit ranges, mask fields, and manipulate binary data fluently
  3. Internalize the ARM instruction set - Know the encoding for data processing, load/store, branch, and multiply instructions
  4. Build a working disassembler - Create a tool that converts raw binary to human-readable assembly
  5. Read ELF binaries - Parse executable formats to find code sections

Theoretical Foundation

Core Concepts

1. RISC vs CISC Encoding Philosophy

ARM is a RISC (Reduced Instruction Set Computer) architecture, which fundamentally affects instruction encoding:

x86 (CISC):                          ARM (RISC):
+----------------------------------+ +----------------------------------+
| Variable-length instructions     | | Fixed-length instructions        |
| 1 to 15 bytes per instruction    | | Always 4 bytes (ARM mode)        |
| Complex decoding logic needed    | | Simple, uniform decoding         |
| Many instruction formats         | | Few, regular instruction formats |
+----------------------------------+ +----------------------------------+

This fixed-length encoding makes disassembly straightforward: read 4 bytes, decode, move to next 4 bytes.

2. ARM 32-bit Instruction Format

Every ARM instruction occupies exactly 32 bits with a consistent structure:

31 30 29 28 | 27 26 25 | 24 23 22 21 | 20 | 19 18 17 16 | 15 14 13 12 | 11 ... 0
+-----------+---------+-------------+----+-------------+-------------+----------+
| Cond Code |  Type   |   Opcode    | S  |     Rn      |     Rd      | Operand2 |
+-----------+---------+-------------+----+-------------+-------------+----------+
    4 bits    3 bits     varies      1b      4 bits        4 bits       12 bits

Condition Field (bits 31-28): ARM’s signature feature. Every instruction can execute conditionally:

Code Mnemonic Meaning Flags Tested
0000 EQ Equal Z=1
0001 NE Not Equal Z=0
0010 CS/HS Carry Set / Unsigned >= C=1
0011 CC/LO Carry Clear / Unsigned < C=0
0100 MI Minus (Negative) N=1
0101 PL Plus (Positive or Zero) N=0
0110 VS Overflow Set V=1
0111 VC Overflow Clear V=0
1000 HI Unsigned Higher C=1 and Z=0
1001 LS Unsigned Lower or Same C=0 or Z=1
1010 GE Signed >= N=V
1011 LT Signed < N!=V
1100 GT Signed > Z=0 and N=V
1101 LE Signed <= Z=1 or N!=V
1110 AL Always (unconditional) -
1111 NV Never (reserved) -

3. Instruction Class Identification

Bits 27-25 determine the instruction class:

Bits 27-25:
  000 = Data Processing (register) or Multiply/Extra Load/Store
  001 = Data Processing (immediate)
  010 = Load/Store (immediate offset)
  011 = Load/Store (register offset)
  100 = Load/Store Multiple
  101 = Branch
  110 = Coprocessor Load/Store or Double Register Transfer
  111 = Software Interrupt or Coprocessor

Data Processing Instruction (bits 27-25 = 00x):
+------+--------+------+---+----+----+------------+
| Cond | 00 | I | OpCode | S | Rn | Rd | Operand2 |
+------+--------+------+---+----+----+------------+
 31-28   27-26   25   24-21  20 19-16 15-12  11-0

Where OpCode (bits 24-21):
  0000 = AND    0100 = ADD    1000 = TST    1100 = ORR
  0001 = EOR    0101 = ADC    1001 = TEQ    1101 = MOV
  0010 = SUB    0110 = SBC    1010 = CMP    1110 = BIC
  0011 = RSB    0111 = RSC    1011 = CMN    1111 = MVN

4. The Barrel Shifter

ARM’s unique feature: operand2 can include a shift operation at no extra cost:

Operand2 when bit 25 = 0 (register operand):
+-------------+------+---+------+----+
| Shift Amount| Type | 0 | Shift| Rm |
+-------------+------+---+------+----+
    11-7       6-5    4    3-0

Shift Types (bits 6-5):
  00 = LSL (Logical Shift Left)
  01 = LSR (Logical Shift Right)
  10 = ASR (Arithmetic Shift Right)
  11 = ROR (Rotate Right)

Example: ADD R0, R1, R2, LSL #2
  Means: R0 = R1 + (R2 << 2) = R1 + R2*4

5. Immediate Value Encoding

ARM uses a clever 12-bit encoding for immediates:

Operand2 when bit 25 = 1 (immediate):
+--------+----------+
| Rotate | Immediate|
+--------+----------+
  11-8      7-0

Value = Immediate rotated right by (2 * Rotate)

Example: 0x102 in operand2
  Rotate = 1, Immediate = 0x02
  Value = 0x02 ROR (2*1) = 0x02 ROR 2 = 0x80000000

This allows encoding values like 0xFF, 0xFF00, 0xFF0000, 0xFF000000
using only 12 bits!

Why This Matters

Building an instruction decoder is the foundation for:

  • Reverse engineering: Understanding binary firmware, malware analysis
  • Debugging tools: Building debuggers like GDB/LLDB
  • Security research: Vulnerability analysis, exploit development
  • Compiler development: Code generation verification
  • Emulators: The decode stage is critical for any emulator

Historical Context

ARM instruction encoding was designed in 1985 at Acorn Computers. The designers (Sophie Wilson and Steve Furber) made brilliant tradeoffs:

  • Fixed-length instructions: Simplified pipeline design, predictable fetch
  • Condition codes on every instruction: Reduced branch penalties
  • Barrel shifter for free: Made common operations like array indexing efficient
  • Register-based design: 16 general-purpose registers vs x86’s 8

These decisions from nearly 40 years ago still influence billions of devices today.


Project Specification

What You’ll Build

A command-line ARM instruction decoder/disassembler that:

  1. Reads raw binary files or ELF executables
  2. Decodes each 32-bit ARM instruction
  3. Outputs human-readable assembly with detailed breakdowns
  4. Handles data processing, load/store, branch, and multiply instructions
  5. Shows the instruction encoding details

Requirements

Core Requirements:

  • Parse raw binary files (just instructions)
  • Decode condition codes correctly
  • Handle data processing instructions (MOV, ADD, SUB, CMP, etc.)
  • Handle load/store instructions (LDR, STR)
  • Handle branch instructions (B, BL)
  • Display register names (R0-R15, SP, LR, PC)
  • Show instruction address and raw hex

Extended Requirements:

  • Parse ELF files to find .text section
  • Decode barrel shifter operands
  • Handle immediate operands with rotation
  • Decode multiply instructions
  • Support Thumb mode detection
  • Show detailed bit-field breakdown

Example Output

$ ./arm-decode firmware.bin

ARM Instruction Decoder v1.0
File: firmware.bin
Size: 256 bytes (64 instructions)

Address    Hex        Instruction    Comment
---------- ---------- -------------- ----------------------------------
0x00000000 E3A00001   MOV  R0, #1    ; R0 = 1
0x00000004 E3A01002   MOV  R1, #2    ; R1 = 2
0x00000008 E0802001   ADD  R2, R0, R1 ; R2 = R0 + R1
0x0000000C E1A0F00E   MOV  PC, LR    ; Return (PC = LR)
0x00000010 0A000003   BEQ  0x00000024 ; Branch if Z=1
0x00000014 E59F0010   LDR  R0, [PC, #16] ; Load from PC+16+8
0x00000018 E1520000   CMP  R2, R0    ; Compare R2 with R0
0x0000001C C2833005   ADDGT R3, R3, #5 ; If GT, R3 += 5

========================================
Detailed decode for 0xE0802001:
========================================
Binary: 1110 0000 1000 0000 0010 0000 0000 0001

[31-28] Condition: 1110 = AL (Always execute)
[27-26] Type:      00 = Data Processing
[25]    I-bit:     0 = Register operand
[24-21] Opcode:    0100 = ADD
[20]    S-bit:     0 = Don't update flags
[19-16] Rn:        0000 = R0 (first operand)
[15-12] Rd:        0010 = R2 (destination)
[11-0]  Operand2:  000000000001
        [11-7]  Shift: 00000 = 0
        [6-5]   Type:  00 = LSL
        [4]     Reg:   0 = Immediate shift
        [3-0]   Rm:    0001 = R1

Result: ADD R2, R0, R1
        R2 = R0 + (R1 << 0) = R0 + R1

Solution Architecture

High-Level Design

                    Input File
                         |
                         v
               +------------------+
               |   File Reader    |
               |  (Raw or ELF)    |
               +------------------+
                         |
                         v
               +------------------+
               | Instruction      |
               | Fetcher          |
               | (4 bytes at a    |
               |  time)           |
               +------------------+
                         |
                         v
               +------------------+
               | Condition Code   |
               | Decoder          |
               | (bits 31-28)     |
               +------------------+
                         |
                         v
               +------------------+
               | Instruction Type |
               | Classifier       |
               | (bits 27-25)     |
               +------------------+
                         |
          +--------------+---------------+
          |              |               |
          v              v               v
    +-----------+  +-----------+  +-----------+
    | Data Proc |  | Load/Store|  | Branch    |
    | Decoder   |  | Decoder   |  | Decoder   |
    +-----------+  +-----------+  +-----------+
          |              |               |
          +--------------+---------------+
                         |
                         v
               +------------------+
               | Instruction      |
               | Formatter        |
               | (assembly text)  |
               +------------------+
                         |
                         v
                    Output

Component Breakdown

// Core data structures

typedef struct {
    uint32_t raw;           // Raw 32-bit instruction
    uint32_t address;       // Memory address

    uint8_t condition;      // Condition code (4 bits)
    uint8_t type;           // Instruction type

    // Decoded fields vary by instruction type
    union {
        struct {
            uint8_t opcode;     // ALU operation
            uint8_t s_bit;      // Update flags?
            uint8_t rn;         // First operand register
            uint8_t rd;         // Destination register
            uint16_t operand2;  // Second operand (12 bits)
        } data_proc;

        struct {
            uint8_t p_bit;      // Pre/post indexing
            uint8_t u_bit;      // Add/subtract offset
            uint8_t b_bit;      // Byte/word
            uint8_t w_bit;      // Write-back
            uint8_t l_bit;      // Load/store
            uint8_t rn;         // Base register
            uint8_t rd;         // Src/dest register
            uint16_t offset;    // Offset (12 bits)
        } load_store;

        struct {
            uint8_t l_bit;      // Link bit (BL vs B)
            int32_t offset;     // Signed 24-bit offset
        } branch;
    };
} instruction_t;

// Function prototypes
instruction_t decode(uint32_t raw, uint32_t address);
const char* get_condition_string(uint8_t cond);
const char* get_opcode_string(uint8_t opcode);
void format_operand2(uint16_t op2, bool is_immediate, char* buffer);
void print_instruction(instruction_t* insn);

Decoding Flow

Step 1: Extract Condition Code
+--------------------------------+
| instruction >> 28              | -> 4-bit condition
+--------------------------------+

Step 2: Identify Instruction Class
+--------------------------------+
| (instruction >> 25) & 0x7      | -> 3-bit type
+--------------------------------+

Step 3: Class-Specific Decode
+----------------------------------+
| if (type == 0 || type == 1)      |
|     decode_data_processing()     |
| else if (type == 2 || type == 3) |
|     decode_load_store()          |
| else if (type == 5)              |
|     decode_branch()              |
| else                             |
|     decode_other()               |
+----------------------------------+

Step 4: Format Output
+--------------------------------+
| Build mnemonic + operands      |
| Apply condition suffix         |
| Calculate target addresses     |
+--------------------------------+

Implementation Guide

Environment Setup

Required Tools:

# Linux/macOS
sudo apt install build-essential    # or brew install gcc
sudo apt install binutils-arm-none-eabi  # for testing

# Verify
arm-none-eabi-objdump --version

# Create project structure
mkdir arm-decoder && cd arm-decoder
mkdir src tests
touch src/main.c src/decoder.c src/decoder.h
touch Makefile

Makefile:

CC = gcc
CFLAGS = -Wall -Wextra -std=c99 -g
TARGET = arm-decode

SRCS = src/main.c src/decoder.c
OBJS = $(SRCS:.c=.o)

$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c -o $@ $<

clean:
	rm -f $(OBJS) $(TARGET)

test: $(TARGET)
	./$(TARGET) tests/test.bin

Project Structure

arm-decoder/
+-- src/
|   +-- main.c          # Entry point, file handling, CLI
|   +-- decoder.c       # Core decoding logic
|   +-- decoder.h       # Data structures and prototypes
|   +-- format.c        # Output formatting
|   +-- format.h        # Formatter prototypes
+-- tests/
|   +-- test.bin        # Test binary (you'll create this)
|   +-- test.S          # Test assembly source
+-- Makefile
+-- README.md

Implementation Hints in Layers

Hint 1: Starting Point (Conceptual Direction)

Begin with the simplest instruction class: data processing instructions. Focus on MOV first:

MOV R0, #1 is encoded as: 0xE3A00001

Break this down:
- E = 1110 (condition: AL, always)
- 3 = 0011 (bit 25 = 1, so immediate operand)
- A = 1010 (opcode = 1101 = MOV, S = 0)
- 0 = 0000 (Rn, ignored for MOV)
- 0 = 0000 (Rd = R0)
- 001 = #1 (immediate value 1, rotation 0)

Your first goal: print “MOV R0, #1” when given 0xE3A00001.

Hint 2: Core Bit Extraction Pattern

Master this pattern for extracting bit fields:

// Extract bits [high:low] from value
#define BITS(val, high, low) (((val) >> (low)) & ((1 << ((high) - (low) + 1)) - 1))

// Examples:
uint32_t insn = 0xE3A00001;
uint8_t cond = BITS(insn, 31, 28);     // 0xE = 14 = AL
uint8_t type = BITS(insn, 27, 25);     // 0x1 = Immediate data proc
uint8_t opcode = BITS(insn, 24, 21);   // 0xD = 13 = MOV
uint8_t s_bit = BITS(insn, 20, 20);    // 0 = don't update flags
uint8_t rd = BITS(insn, 15, 12);       // 0 = R0
uint16_t imm = BITS(insn, 7, 0);       // 1
uint8_t rotate = BITS(insn, 11, 8);    // 0

Hint 3: Instruction Classification

Use a switch statement based on bits 27-25, but watch for special cases:

uint32_t classify_instruction(uint32_t insn) {
    uint8_t type = BITS(insn, 27, 25);

    switch (type) {
        case 0:
            // Could be data processing OR multiply
            // Check bits 7 and 4: if both set, it's multiply
            if ((insn & 0x0FC00090) == 0x00000090) {
                return INSN_MULTIPLY;
            }
            return INSN_DATA_PROC_REG;

        case 1:
            return INSN_DATA_PROC_IMM;

        case 2:
            return INSN_LOAD_STORE_IMM;

        case 3:
            return INSN_LOAD_STORE_REG;

        case 4:
            return INSN_LOAD_STORE_MULTI;

        case 5:
            return INSN_BRANCH;

        case 6:
        case 7:
            // Coprocessor or software interrupt
            if (BITS(insn, 24, 24) && type == 7) {
                return INSN_SWI;
            }
            return INSN_COPROCESSOR;
    }
    return INSN_UNKNOWN;
}

Hint 4: Testing and Verification

Create test binaries using the ARM assembler:

# Create test.S
cat > tests/test.S << 'EOF'
.global _start
_start:
    mov r0, #1
    mov r1, #2
    add r2, r0, r1
    sub r3, r2, r1
    cmp r2, r3
    beq equal
    bne notequal
equal:
    mov r0, #0
notequal:
    mov r0, #1
EOF

# Assemble and link
arm-none-eabi-as -o tests/test.o tests/test.S
arm-none-eabi-ld -Ttext=0 -o tests/test.elf tests/test.o
arm-none-eabi-objcopy -O binary tests/test.elf tests/test.bin

# Verify with official disassembler
arm-none-eabi-objdump -d tests/test.elf

Compare your output with objdump - they should match!


Interview Questions You Should Be Able to Answer

After completing this project, you should confidently answer:

  1. “Explain how ARM encodes a conditional ADD instruction.”
    • Walk through condition field, opcode, S bit, registers, operand2
  2. “What’s the barrel shifter and why is it useful?”
    • Free shift operation in operand2, enables efficient array indexing
  3. “How does ARM encode immediate values larger than 8 bits?”
    • 8-bit value with 4-bit rotation (2x), can encode aligned constants
  4. “What’s the difference between ARM and Thumb mode?”
    • ARM: 32-bit instructions, full features; Thumb: 16-bit, subset, denser code
  5. “How would you identify a function call vs a plain branch?”
    • BL sets LR (link bit set), B doesn’t
  6. “What does ‘load/store architecture’ mean for a disassembler?”
    • Only LDR/STR access memory; all ALU ops work on registers only

Books That Will Help

Topic Book Chapter
ARM Instruction Encoding ARM Architecture Reference Manual Section A5
Condition Codes “The Art of ARM Assembly, Vol 1” - Hyde Chapter 4
Binary Parsing in C “Computer Systems: A Programmer’s Perspective” - Bryant Chapter 2
ELF Format “Practical Binary Analysis” - Andriesse Chapter 2
Bitwise Operations “Hacker’s Delight” - Warren Chapter 2

Testing Strategy

Unit Tests

// test_decoder.c
void test_mov_immediate() {
    instruction_t insn = decode(0xE3A00001, 0);
    assert(insn.condition == 14);  // AL
    assert(insn.data_proc.opcode == 13);  // MOV
    assert(insn.data_proc.rd == 0);  // R0
    // Check immediate = 1
    printf("test_mov_immediate: PASS\n");
}

void test_add_registers() {
    instruction_t insn = decode(0xE0802001, 0);
    assert(insn.data_proc.opcode == 4);  // ADD
    assert(insn.data_proc.rd == 2);  // R2
    assert(insn.data_proc.rn == 0);  // R0
    // Check rm = 1
    printf("test_add_registers: PASS\n");
}

void test_conditional_branch() {
    instruction_t insn = decode(0x0A000003, 0);
    assert(insn.condition == 0);  // EQ
    assert(insn.branch.l_bit == 0);  // B, not BL
    printf("test_conditional_branch: PASS\n");
}

Integration Tests

# Test against known good output
./arm-decode tests/test.bin > output.txt
arm-none-eabi-objdump -d tests/test.elf | grep -E "^\s+[0-9a-f]+:" > expected.txt
diff output.txt expected.txt && echo "PASS"

Edge Cases to Test

  • All 15 condition codes
  • All data processing opcodes (16)
  • Immediate values with rotation
  • Shifted register operands (LSL, LSR, ASR, ROR)
  • PC-relative loads (LDR R0, [PC, #offset])
  • Load/store with write-back
  • Branch with negative offset
  • MOV PC, LR (return instruction)

Common Pitfalls & Debugging

Problem Symptom Root Cause Fix
Wrong endianness Garbage output ARM uses little-endian Read bytes in correct order
Off-by-one in bit extraction Wrong values Bit numbering confusion Remember: bit 0 is LSB
Missing special cases Wrong instruction type Multiply looks like data proc Check bits 7,4 for multiply
Immediate rotation error Wrong immediate value Forgot to multiply rotation by 2 rotate = rotate_field * 2
Branch offset wrong Wrong target address Forgot PC+8 and sign extension PC is 8 ahead, offset is signed
Register names wrong “R13” instead of “SP” Not mapping special registers Map R13=SP, R14=LR, R15=PC

Debugging Techniques

// Add verbose mode for debugging
void debug_instruction(uint32_t insn) {
    printf("Raw: 0x%08X\n", insn);
    printf("Binary: ");
    for (int i = 31; i >= 0; i--) {
        printf("%d", (insn >> i) & 1);
        if (i % 4 == 0) printf(" ");
    }
    printf("\n");
    printf("Condition: 0x%X\n", BITS(insn, 31, 28));
    printf("Type: 0x%X\n", BITS(insn, 27, 25));
    // ... more fields
}

Extensions & Challenges

After Basic Completion

  1. Add Thumb support: Detect and decode 16-bit Thumb instructions
  2. Parse ELF symbols: Show function names from symbol table
  3. Add VFP/NEON: Decode floating-point and SIMD instructions
  4. Create library mode: Build as a library for use in other tools
  5. Add ARM64 support: Extend to AArch64 instruction set

Challenge Mode

  • Implement instruction-level caching for repeated decodes
  • Add a simple symbolic execution mode
  • Build a control flow graph from decoded instructions
  • Create a diff tool comparing two binaries

Self-Assessment Checklist

Before moving to Project 2, verify you can:

  • Explain the ARM instruction encoding format without notes
  • Extract any bit field from a 32-bit value using bitwise ops
  • List all 16 condition codes and their flag requirements
  • Describe how the barrel shifter encodes shifted operands
  • Explain immediate rotation encoding
  • Identify instruction classes from bits 27-25
  • Distinguish MOV from LDR by looking at encoding
  • Calculate branch targets from encoded offsets

Final Test

Decode this instruction by hand (no code):

0xE1A0C00D

You should be able to determine:

  • Condition code
  • Instruction type
  • Operation
  • Source and destination registers
  • Result: MOV R12, R13 (or MOV IP, SP)

Learning Milestones

Track your progress through these milestones:

Milestone You Can… Understanding Level
1 Decode condition codes correctly You understand conditional execution
2 Parse data processing instructions You understand ALU instruction format
3 Handle barrel shifter and immediates You understand ARM’s flexible operand encoding
4 Decode load/store and branches You understand the complete instruction set structure

Next Project: ARM Assembly Calculator ->