Computers are machines designed to perform calculations.
These calculations are ultimately performed by the CPU, and a typical operation requires two things: operands and an operator.
Operands are usually stored in registers, which are small, fast storage locations inside the CPU.

Assembly language gives us direct access to CPU registers. In this post, we’ll explore how to store values in these registers using the fundamental mov instruction.

You can find the source code used in this tutorial on GitHub!

Using mov to Store a Value in a Register

Storing Immediate Values

store-imm-to-reg.s

1
2
3
4
5
  .text
  .global _start
_start:
  mov r0, #2
  b .

mov r0, #2: stores the number 3 in register R0.

The mov instruction follows a simple syntax:

mov destination_register, value

When a number is prefixed with # (like #2), it is called an immediate value. You can also use hexadecimal notation: e.g., #0x2.

For compiling, running on QEMU, and setting up GDB, refer to this post.

Compile

$ arm-none-eabi-gcc \
  -nostdlib \
  -Ttext=0x10000 \
  store-imm-to-reg.s \
  -o store-imm-to-reg.elf

Run on QEMU & Debug with GDB

Start QEMU

$ qemu-system-arm \
  -machine versatilepb \
  -nographic \
  -S -s \
  -kernel store-imm-to-reg.elf

Here’s a quick explanation of each option:

Option Description
-machine versatilepb Use the VersatilePB ARM development board
-nographic Disable GUI; run in console
-S Halt the CPU until GDB connects
-s Start GDB server on port 1234
-kernel Load the specified ELF file

QEMU may seem unresponsive at first — this is expected. It’s waiting for GDB to connect.

Connect with GDB

$ gdb-multiarch store-imm-to-reg.elf
Launching GDB with the ELF binary
Launching GDB with the compiled ELF binary

Inside GDB, connect to QEMU’s GDB server:

(gdb) target retmote localhost:1234
Connecting GDB to QEMU's GDB server on port 1234
Connecting GDB to QEMU’s GDB server at port 1234

Once connected, we can inspect whether our program is properly loaded into memory. To check the contents at a specific memory address, GDB uses the following command format:

x/{N}{format} {address}
  • {N}: number of units to display
  • {format}: output type (e.g., i for instructions, x for hexadecimal, s for string, u for unsigned decimal)
  • {address}: memory address to inspect

For example, to disassemble 10 instructions starting at address 0x10000, use:

(gdb) x/10i 0x10000

This helps you verify that the machine code was loaded properly and matches what you wrote in your assembly source file.

Disassembling 10 instructions starting at address 0x10000
Disassembling 10 instructions from address 0x10000

To check the current register values:

(gdb) info registers   # or shorthand: i r 
(gdb) i r pc r0        # check just PC and R0
Printing the values of PC and R0 registers in GDB
Checking the values of PC and R0 in GDB

At this point, PC (program counter) should be pointing at 0x10000, which is our mov r0, #3 instruction.

Execute Step-by-Step

To execute a single instruction:

(gdb) stepi   # or shorthand: si

Then check the register values again:

(gdb) i r pc r0
Stepping through one instruction and inspecting updated PC and R0
PC moves forward, and R0 is now updated after executing `mov`

You’ll see:

  • PC has moved from 0x10000 to 0x10004
  • R0 now contains 2

Run stepi one more time to execute b ., which creates an infinite loop by jumping to the current PC.

PC value remains the same after executing a self-branching instruction
PC value stays the same after `b .` (infinite loop)

Copying Between Registers

Let’s now copy a value from one register to another.

1
2
3
4
5
6
  .text
  .global _start
_start:
  mov r0, #2
  mov r1, r0
  b .

Here, mov r1, r0 copies the value of R0 into R1.

You can repeat the GDB steps from above to verify the register contents after each instruction.

What’s the Difference Between mov and mvn?

The mvn instruction is similar to mov, but it stores the bitwise NOT (1’s complement) of the value.

mvn mov
mvn r0, #0 mov r0, #-1
mvn r0, #-1 mov r0, #0
mvn r0, #0xF mov r0, #-16

Pro tip: Use Python REPL to test bitwise negation:

>>> ~0xf
-16

Coming Up Next: Shift Operands

Both mov and mvn belong to the Data Processing Instructions category in the ARM architecture.

These instructions can use a shifted operand as input, which allows you to shift bits while copying or computing values.

This concept applies to many arithmetic operations (like add, sub, etc.), and we’ll cover it in the next post.

Excerpt from ARM manual showing shifted operand encoding
ARM manual: encoding format for shifted operand

Summary

In this post, we explored how to:

  • Use mov to assign immediate values to registers
  • Copy values between registers
  • Understand the difference between mov and mvn
  • Use QEMU and GDB to debug and inspect registers

We confirmed our code works in a simulated ARM environment using QEMU and GDB — a key part of your low-level dev workflow.

Don’t worry if GDB feels unfamiliar right now. We’ll continue using this setup in future posts, so it will become second nature with practice.

Up next: we’ll dive into shift operands and how they enhance the expressiveness of ARM instructions.