How Are Constants Represented in Assembly Language? A Deep Dive

How are constants represented in Assembly language

Constants seem like the simplest thing in programming — a number that doesn’t change. But the first time I tried to define a constant in Assembly, I realized there’s more nuance here than in any high-level language I’d used before. Depending on the assembler, the architecture, and where the constant lives (embedded directly in an instruction vs. stored in memory), the representation and behavior can be quite different. Let me walk through everything I’ve learned about how constants actually work under the hood.

What Counts as a “Constant” in Assembly?

In Assembly, a constant is a fixed value known at assembly time — it doesn’t change while the program runs, and its value is baked directly into the machine code or into a labeled memory location. Constants generally fall into a few categories:

  • Immediate values — literal numbers embedded directly inside an instruction’s encoding
  • Symbolic constants — named values defined with directives like EQU or %define, resolved at assembly time
  • Data-section constants — values stored in memory (often in a read-only .rodata section) and referenced by label
  • String/character constants — sequences of bytes representing text

Numeric Literal Formats

Assembly language supports several numeric bases, and each assembler has its own syntax quirks.

BaseNASM syntaxGAS (AT&T) syntaxMASM syntax
Decimal424242
Hexadecimal0x2A or 2Ah0x2A2Ah
Binary0b101010 or 101010b0b101010101010b
Octal52o or 05205252o
Character'A''A''A'
; NASM examples
mov eax, 42          ; decimal
mov ebx, 0x2A         ; hexadecimal
mov ecx, 101010b       ; binary
mov edx, 'A'            ; character constant (ASCII 65)

I personally lean on hexadecimal for anything related to memory addresses, bit masks, or flags, since it maps cleanly onto binary (each hex digit is exactly 4 bits), and decimal for anything that represents an actual quantity, like a loop counter.

Symbolic Constants: EQU and Friends

Rather than sprinkling magic numbers throughout your code, Assembly lets you name a constant once and reuse it everywhere. This is done with the EQU directive (NASM, MASM) or .equ/.set (GAS).

; NASM
BUFFER_SIZE equ 256
MAX_RETRIES equ 5

section .bss
    buffer resb BUFFER_SIZE

section .text
    mov ecx, MAX_RETRIES
; GNU Assembler (AT&T syntax)
.equ BUFFER_SIZE, 256
.equ MAX_RETRIES, 5

movl $BUFFER_SIZE, %eax

Unlike a variable, a symbol defined with EQU isn’t a memory location — it’s purely a name resolved by the assembler at build time. This means there’s zero runtime cost to using it; the assembler simply substitutes the value wherever the name appears, similar to a C preprocessor #define.

The Difference Between EQU and a Labeled Memory Constant

This trips a lot of beginners up, so it’s worth spelling out clearly:

; This is a symbolic constant — no memory is allocated
PI_APPROX equ 3

; This is a memory-resident constant — memory IS allocated,
; and you must dereference it to get the value
section .rodata
pi_value: dd 3.14159
mov eax, PI_APPROX        ; loads the literal value 3 into eax
mov eax, [pi_value]        ; loads the 4 bytes stored at label pi_value

Using PI_APPROX directly embeds the value 3 into the instruction’s machine code as an immediate operand. Using [pi_value] generates a memory access instruction that reads from the .rodata section at runtime.

Immediate Values and Instruction Encoding

At the machine-code level, an “immediate” constant is literally encoded as extra bytes appended to the instruction opcode. For example, on x86, mov eax, 42 assembles to something like:

B8 2A 00 00 00

Here, B8 is the opcode for “move immediate 32-bit value into EAX,” and the remaining four bytes (2A 00 00 00, little-endian) are the constant 42 itself. This is why immediate constants have size limits — an x86 instruction can only embed so many bytes of immediate data (commonly up to 32 bits, or 64 bits for specific move instructions like MOVABS on x86-64).

; x86-64: only MOV can load a full 64-bit immediate
mov rax, 0x123456789ABCDEF0    ; assembles with a 64-bit immediate

; Most other instructions are limited to 32-bit immediates,
; sign-extended into the 64-bit register
add rax, 0x7FFFFFFF

Constants on ARM: A Different Story

ARM’s fixed-width, 32-bit instruction encoding creates a unique constraint: you can’t fit an arbitrary 32-bit constant into a 32-bit instruction, because part of that instruction has to encode the opcode and register operands too. ARM solves this with a clever scheme.

ARM32: Rotated 8-bit Immediates

On classic ARM (AArch32), immediate operands are encoded as an 8-bit value combined with a 4-bit rotation, giving you access to a specific set of “nice” 32-bit values rather than every possible one.

MOV R0, #0xFF          ; valid — 0xFF fits directly
MOV R0, #0xFF00         ; valid — 0xFF rotated
MOV R0, #0x12345678       ; INVALID — cannot be encoded directly

For values that don’t fit this pattern, you need multiple instructions or a literal pool (a small area of memory near the code, holding the constant, loaded with LDR):

LDR R0, =0x12345678       ; pseudo-instruction: assembler builds a literal pool

AArch64: MOVZ / MOVK

On 64-bit ARM, large constants are built up in 16-bit chunks using MOVZ (move with zero) and MOVK (move with keep):

MOVZ X0, #0x5678
MOVK X0, #0x1234, LSL #16
; X0 now holds 0x12345678

This is a great example of how architecture design directly shapes how something as “simple” as a constant gets represented — RISC architectures like ARM trade instruction simplicity for more complex constant-loading sequences, while CISC architectures like x86 allow bulkier immediate encodings directly.

String and Character Constants

Strings are just constants too — sequences of byte-sized constants, usually null-terminated or length-prefixed.

; NASM
section .data
    msg db "Hello, World!", 0    ; null-terminated string
    msg_len equ $ - msg           ; computed constant: length of msg

The $ symbol here refers to the current address, so $ - msg computes the string’s length at assembly time — a neat trick that avoids hardcoding a length that could get out of sync with the actual string.

Floating-Point Constants

Floating-point constants require special directives, since their in-memory representation follows the IEEE-754 standard rather than plain integer encoding.

section .rodata
    pi       dd 3.14159265     ; single precision (32-bit)
    e_const  dq 2.718281828459  ; double precision (64-bit)

There is no direct way to embed a floating-point immediate into most x86 arithmetic instructions — floating-point constants almost always live in memory and are loaded via SSE/AVX instructions like MOVSS or MOVSD.

movss xmm0, [pi]

Internal Representation: How the Assembler Resolves Constants

flowchart TD
    A[Source code with constant] --> B{Type of constant?}
    B -->|EQU / symbolic| C[Assembler substitutes value at compile time]
    B -->|Immediate operand| D[Encoded directly into instruction bytes]
    B -->|Data section constant| E[Allocated in .data or .rodata, referenced by address]
    C --> F[No runtime memory access]
    D --> F
    E --> G[Runtime memory read via label/address]

Practical Use Cases

  • Buffer sizes and loop bounds defined with EQU for readability and easy tuning
  • Bitmask flags for hardware registers, defined in hex for clarity
  • Lookup tables of precomputed constants (e.g., sine tables, CRC polynomials) stored in .rodata
  • Magic numbers in file format parsers (e.g., checking if the first 4 bytes of a file equal 0x7F454C46, the ELF magic number)

Comparing Constant Representations

MethodStorage costRuntime costFlexibility
Immediate operand0 (part of instruction)None — value is “free”Limited by instruction encoding size
EQU/.equ symbolic constant0NoneText substitution only, no type checking
.data/.rodata memory constantUses memoryOne memory load per accessCan be arbitrarily large, addressable
ARM literal poolUses memory near codeOne load instructionNeeded for constants too large to encode directly

Debugging and Common Mistakes

  1. Assuming EQU creates a variable — it doesn’t; you can’t take its address or modify it at runtime.
  2. Immediate value overflow — trying to encode a constant too large for the instruction’s immediate field results in an assembler error (or silent truncation, depending on the assembler).
  3. Forgetting .rodata vs .data — placing constants that should never change in a writable section is a common source of subtle bugs and a security smell (a writable .rodata-equivalent can be exploited).
  4. Endianness confusion — when manually inspecting a constant’s byte encoding in a debugger, remember x86 is little-endian, so 0x12345678 appears in memory as 78 56 34 12.

Best Practices

  • Prefer named EQU constants over magic numbers for anything used more than once.
  • Use hexadecimal for masks/flags, decimal for counts/quantities.
  • Place read-only constants in .rodata (or the assembler’s equivalent) so the OS can enforce write-protection.
  • Compute derived constants (like string lengths) using assembler expressions ($ - label) instead of hardcoding them.

FAQs

Q: Can I change a constant defined with EQU during program execution? No. EQU constants are resolved entirely at assembly time; there’s no runtime entity to modify.

Q: What’s the largest immediate value x86-64 supports? 64 bits, but only for specific instructions like MOVABS/MOV into a 64-bit register. Most arithmetic and logic instructions are limited to 32-bit immediates (sign-extended).

Q: Why can’t ARM load any 32-bit constant in one instruction? Because ARM instructions are fixed at 32 bits wide, and a chunk of those bits must encode the opcode and registers, leaving too little room for an arbitrary 32-bit immediate. ARM instead uses rotated 8-bit immediates, multiple MOVZ/MOVK instructions, or literal pools.

Q: Is a string constant stored differently from a numeric constant? Not fundamentally — both are just bytes in memory. The difference is purely how your program interprets and processes those bytes.

Summary and Key Takeaways

Constants in Assembly aren’t a single, uniform concept — they split into immediate values baked directly into instructions, symbolic names resolved entirely at assembly time, and memory-resident values accessed through labels. Which one you use has real consequences for performance, code size, and readability. Understanding this distinction — and how architectures like x86 and ARM encode immediates differently — is foundational to writing efficient, correct low-level code.

References

Total
0
Shares

Leave a Reply

Previous Post
What is the significance of the accumulator register

The Significance of the Accumulator Register in Assembly Language

Next Post
Explain the concept of subroutine in Assembly language

Subroutines in Assembly Language: A Complete Guide From Beginner to Advanced

Related Posts