Project 1: ARM Instruction Decoder & Disassembler
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:
- Understand ARM instruction encoding - Learn how 32-bit ARM instructions are structured with condition codes, opcodes, register fields, and operands
- Master bitwise operations - Extract specific bit ranges, mask fields, and manipulate binary data fluently
- Internalize the ARM instruction set - Know the encoding for data processing, load/store, branch, and multiply instructions
- Build a working disassembler - Create a tool that converts raw binary to human-readable assembly
- 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:
- Reads raw binary files or ELF executables
- Decodes each 32-bit ARM instruction
- Outputs human-readable assembly with detailed breakdowns
- Handles data processing, load/store, branch, and multiply instructions
- 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:
- “Explain how ARM encodes a conditional ADD instruction.”
- Walk through condition field, opcode, S bit, registers, operand2
- “What’s the barrel shifter and why is it useful?”
- Free shift operation in operand2, enables efficient array indexing
- “How does ARM encode immediate values larger than 8 bits?”
- 8-bit value with 4-bit rotation (2x), can encode aligned constants
- “What’s the difference between ARM and Thumb mode?”
- ARM: 32-bit instructions, full features; Thumb: 16-bit, subset, denser code
- “How would you identify a function call vs a plain branch?”
- BL sets LR (link bit set), B doesn’t
- “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
- Add Thumb support: Detect and decode 16-bit Thumb instructions
- Parse ELF symbols: Show function names from symbol table
- Add VFP/NEON: Decode floating-point and SIMD instructions
- Create library mode: Build as a library for use in other tools
- 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(orMOV 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 |