The Process of Data Movement in Assembly Language: Registers, Memory, and Addressing Modes Explained

Describe the process of data movement in Assembly language

Nearly everything a program does eventually boils down to moving data from one place to another — from memory into a register, from one register to another, or from a register back out to memory. In this post, I’ll walk through exactly how data movement works in assembly language, covering instructions, addressing modes, and practical examples across x86, x86-64, and ARM.

Why Data Movement Is Foundational

Before a CPU can add, compare, or branch on a value, that value has to actually be somewhere the CPU’s execution units can reach — almost always a register. Data movement instructions are responsible for getting values:

Without reliable, well-understood data movement, none of the other operations (arithmetic, comparisons, branching) would have anything to work with.

The Core Instruction: MOV

The most fundamental data movement instruction across nearly every architecture is some form of MOV.

x86-64 Examples

mov eax, 10           ; immediate-to-register: EAX = 10
mov ebx, eax           ; register-to-register: EBX = EAX
mov [rdi], eax          ; register-to-memory: store EAX at address in RDI
mov eax, [rdi]          ; memory-to-register: load value at address in RDI into EAX

ARM64 Examples

mov x0, #10            ; immediate-to-register: X0 = 10
mov x1, x0               ; register-to-register: X1 = X0
str w0, [x1]              ; register-to-memory: store W0 at address in X1
ldr w0, [x1]               ; memory-to-register: load value at address in X1 into W0

Notice an important architectural difference here: x86 uses a single MOV mnemonic for both register and memory operations, while ARM uses separate, explicit LDR (load) and STR (store) instructions, reflecting its load/store architecture philosophy — arithmetic instructions in ARM never touch memory directly, only registers.

Internal Working Process: How a MOV Actually Executes

flowchart TD
    A[Fetch MOV instruction] --> B[Decode: determine source and destination]
    B --> C{Source type?}
    C -->|Immediate| D[Value embedded directly in instruction encoding]
    C -->|Register| E[Read value from source register]
    C -->|Memory| F[Calculate effective address, read from memory/cache]
    D --> G[Write value to destination]
    E --> G
    F --> G
    G --> H{Destination type?}
    H -->|Register| I[Value stored in destination register]
    H -->|Memory| J[Value written to memory/cache at calculated address]

Addressing Modes: The Different Ways to Specify Data Location

Addressing modes define how an instruction determines where its data actually lives. This is one of the richest parts of understanding data movement.

Addressing Modex86-64 ExampleARM64 ExampleDescription
Immediatemov eax, 5mov x0, #5Constant value embedded in the instruction itself
Register directmov eax, ebxmov x0, x1Value comes directly from another register
Register indirectmov eax, [ebx]ldr x0, [x1]Address to read/write comes from a register
Base + displacementmov eax, [ebx+8]ldr x0, [x1, #8]Register plus a constant offset
Base + index + scalemov eax, [ebx+ecx*4]ldr x0, [x1, x2, lsl #2]Common pattern for array indexing
RIP-relative (x86-64 only)lea rax, [rip+label]N/AAddress relative to current instruction pointer, used for position-independent code
Pre/post-indexed (ARM specific)N/Aldr x0, [x1, #8]! (pre-indexed) / ldr x0, [x1], #8 (post-indexed)Automatically updates the base register as part of the memory access

ARM’s pre- and post-indexed addressing modes are particularly powerful for iterating through arrays, since the base register update and the memory access happen together in a single instruction:

; Post-indexed: load value at [x1], then increment x1 by 8 afterward
ldr x0, [x1], #8

Moving Data Between Registers of Different Sizes

Data movement often needs to handle size mismatches — moving an 8-bit or 16-bit value into a larger register while controlling what happens to the extra bits.

; x86-64: sign-extend a 32-bit value into a 64-bit register
movsxd rax, ebx        ; sign-extend EBX into RAX

; x86-64: zero-extend a 32-bit value into a 64-bit register
; (Note: on x86-64, writing to a 32-bit register automatically zero-extends the upper 32 bits)
mov eax, ebx             ; implicitly zero-extends into RAX's upper half

; zero-extend an 8-bit value
movzx eax, bl             ; zero-extend BL (8-bit) into EAX (32-bit)

ARM64 has equivalent behavior:

sxtw x0, w1              ; sign-extend W1 (32-bit) into X0 (64-bit)
uxtb w0, w1                ; zero-extend the lowest byte of W1 into W0

Getting sign-extension versus zero-extension wrong is a very common source of subtle bugs, especially when working with negative numbers stored in smaller registers.

Loading Effective Addresses vs. Loading Values

A frequently confused pair of instructions:

mov rax, [my_variable]     ; loads the VALUE stored at my_variable's address
lea rax, [my_variable]      ; loads the ADDRESS of my_variable itself (no memory read)

LEA (Load Effective Address) doesn’t actually access memory at all — it just performs the address calculation and stores the result. This makes it useful not only for getting pointers, but also as a fast arithmetic trick:

lea eax, [ebx + ecx*2 + 4]    ; computes ebx + ecx*2 + 4 without touching memory

Compilers frequently use LEA for simple multiplication and addition combinations because it can execute faster than equivalent MUL/ADD instruction sequences.

Moving Blocks of Data: String Instructions (x86)

x86 provides dedicated instructions for moving blocks of memory efficiently:

mov rsi, source_address
mov rdi, dest_address
mov rcx, block_length
rep movsb                    ; repeat MOVSB (move byte) RCX times, advancing RSI/RDI automatically

REP MOVSB is essentially a hardware-accelerated memcpy at the instruction level, historically important for bulk data movement, though modern compilers and CPUs often prefer vectorized (SIMD) copy loops for large blocks due to better throughput.

Vectorized Data Movement (SIMD)

Modern CPUs support moving multiple data elements simultaneously using SIMD registers:

; x86-64: move 128 bits (four 32-bit floats) at once
movaps xmm0, [array_a]      ; requires 16-byte aligned memory
movups xmm0, [array_a]       ; works with unaligned memory, slightly slower historically

ARM’s equivalent uses NEON registers:

ld1 {v0.4s}, [x0]              ; load four 32-bit values into a NEON register

These instructions matter enormously for performance in multimedia processing, scientific computing, and machine learning workloads, since moving and processing multiple values per instruction dramatically reduces total instruction count.

Practical Use Cases

Performance Considerations

Comparison: Register-Memory Model vs. Load-Store Model

Aspectx86 (Register-Memory Model)ARM (Load-Store Model)
Arithmetic directly on memoryYes (ADD [mem], eax)No — must load into register first
Instruction count for memory-involved mathOften fewer instructionsOften more instructions, but more uniform/predictable
Complexity of instruction decodingHigher (variable-length, many addressing modes)Lower (fixed-length, simpler decode logic)

Best Practices

Common Mistakes and Troubleshooting

FAQs

What’s the difference between MOV and LEA in x86? MOV with a memory operand reads or writes the actual value stored at that address. LEA only calculates the address itself and stores that calculated address — it never touches the memory being referenced.

Why does ARM use separate LDR/STR instructions instead of a universal MOV like x86? Because ARM follows a load/store architecture philosophy, where arithmetic and logic instructions only ever operate on registers, never directly on memory. Data must be explicitly loaded into a register before any computation, and explicitly stored back afterward.

What happens if I move a negative 8-bit value into a 32-bit register without sign-extending it? Using a plain zero-extending move (MOVZX) on a negative signed byte will produce an incorrect large positive value in the destination register, since the sign bit isn’t propagated. You need MOVSX (sign-extend) for correct signed behavior.

Is REP MOVSB still used in modern code? It’s less common in hand-written performance-critical code today, since modern SIMD-based copy routines are often faster for large blocks, but some highly optimized memcpy implementations still selectively use variants of it for specific size ranges.

Summary and Key Takeaways

References

Exit mobile version