How Data Is Represented in Assembly Language

How is data represented in Assembly language

Every piece of information a computer works with — numbers, text, images, even instructions themselves — ultimately boils down to patterns of bits. Assembly language is where that truth becomes unavoidable: there’s no hiding behind types like int or string the way high-level languages let you. This post walks through exactly how data is represented in Assembly, from raw bits to declared data types, signed/unsigned numbers, floating point, characters, and arrays.

The Foundation: Bits and Bytes

At the most fundamental level, all data in a computer is represented as binary digits (bits) — 0s and 1s. Bits are grouped into larger units for practical use:

UnitSizeCommon Use
Bit1 binary digitSmallest unit; flags, boolean values
Nibble4 bitsHalf a byte; one hex digit
Byte8 bitsSmallest addressable unit of memory; ASCII characters
Word16 bits (2 bytes)Historical “native” size on 16-bit CPUs
Doubleword (dword)32 bits (4 bytes)Standard int, 32-bit registers
Quadword (qword)64 bits (8 bytes)64-bit registers, long values, pointers

Assembly directives directly reflect these units when declaring data:

section .data
    myByte  db 0x41        ; define byte (8 bits)
    myWord  dw 0x1234       ; define word (16 bits)
    myDword dd 0x12345678   ; define doubleword (32 bits)
    myQword dq 0x123456789ABCDEF0  ; define quadword (64 bits)

Number Representation: Binary, Decimal, and Hexadecimal

In Assembly, numeric literals can typically be written in several bases, and the assembler converts them to their binary machine representation automatically.

BaseExample (value 42)Common Prefix/Suffix
Binary101010b or 0b101010b suffix or 0b prefix
Decimal42none
Hexadecimal2Ah or 0x2Ah suffix or 0x prefix
mov eax, 42        ; decimal
mov ebx, 0x2A       ; hexadecimal
mov ecx, 101010b    ; binary

All three instructions load the exact same value into their respective registers — the base is purely a notational convenience for the programmer; internally, everything becomes binary.

Signed vs Unsigned Representation

CPUs don’t inherently “know” whether a bit pattern represents a signed or unsigned number — the instruction used determines how the bits are interpreted. Most modern architectures use two’s complement representation for signed integers.

Two’s Complement Explained

To represent a negative number in two’s complement:

  1. Write the positive value in binary.
  2. Invert all bits (one’s complement).
  3. Add 1.

Example: representing -5 as an 8-bit two’s complement number

5 in binary:        00000101
Invert (1's comp):  11111010
Add 1:               11111011   <- this represents -5
Bit Pattern (8-bit)Unsigned ValueSigned (Two’s Complement) Value
0000000000
01111111127127
10000000128-128
11111111255-1

This is why the same bit pattern can mean two completely different things depending on whether an instruction treats it as signed or unsigned — for example, x86-64 provides both mul/div (unsigned) and imul/idiv (signed) instruction variants.

mov eax, -5      ; assembler encodes this using two's complement
imul ebx, eax    ; signed multiplication

Character and String Representation

Text data is represented using character encoding standards, most commonly ASCII or UTF-8, where each character maps to a specific numeric byte value.

CharacterASCII (Decimal)ASCII (Hex)
‘A’650x41
‘a’970x61
‘0’480x30
‘ ‘ (space)320x20
section .data
    letter db 'A'                  ; single character, stored as byte 0x41
    greeting db "Hello", 0         ; null-terminated string (C-style)
    greeting_len equ $ - greeting  ; calculate string length

Strings in Assembly are typically just contiguous sequences of bytes in memory — there’s no built-in “string type.” The convention of null-termination (0 byte marking the end) comes from C and is widely used, but Assembly itself doesn’t enforce any particular string format; length-prefixed strings (storing the length before the data) are another common alternative.

Floating-Point Representation

Floating-point numbers use the IEEE 754 standard, representing values through three components: sign, exponent, and mantissa (fraction).

IEEE 754 Single Precision (32-bit) layout:

graph LR
    A["Sign (1 bit)"] --> B["Exponent (8 bits)"]
    B --> C["Mantissa/Fraction (23 bits)"]
ComponentBitsPurpose
Sign10 = positive, 1 = negative
Exponent8Scales the value (biased by 127)
Mantissa23Fractional precision

Double precision (64-bit) extends this to 1 sign bit, 11 exponent bits, and 52 mantissa bits, offering much greater range and precision.

x86-64 floating-point example using SSE registers:

section .data
    myFloat  dd 3.14159      ; 32-bit single precision
    myDouble dq 2.718281828  ; 64-bit double precision

section .text
    movss xmm0, [myFloat]    ; move scalar single-precision float into xmm0
    movsd xmm1, [myDouble]   ; move scalar double-precision float into xmm1

ARM (AArch64) floating-point example:

ldr s0, [x0]      ; load 32-bit float into s0 (single-precision SIMD/FP register)
ldr d0, [x1]      ; load 64-bit double into d0 (double-precision SIMD/FP register)

Both architectures rely on dedicated floating-point/SIMD register sets (XMM/YMM on x86-64, V/S/D registers on ARM) separate from the general-purpose integer registers, since floating-point arithmetic requires specialized hardware circuitry.

Data Declaration Directives Comparison

Directive (NASM)SizeGAS EquivalentPurpose
db1 byte.byteDefine byte(s)
dw2 bytes.word / .shortDefine word(s)
dd4 bytes.long / .intDefine doubleword(s) / 32-bit float
dq8 bytes.quadDefine quadword(s) / 64-bit double
resbReserve N bytes (uninitialized).skip / .spaceReserve uninitialized storage
section .bss
    buffer resb 64     ; reserve 64 uninitialized bytes

Arrays and Structured Data

Arrays in Assembly are simply contiguous blocks of memory, with elements accessed via indexed addressing (as discussed in addressing modes).

section .data
    numbers dd 10, 20, 30, 40, 50   ; array of 5 doublewords (ints)

section .text
    mov ebx, numbers        ; ebx = base address of array
    mov eax, [ebx + 4*2]    ; access numbers[2] = 30 (0-indexed, 4 bytes per element)

Structures (like a C struct) are represented similarly — as a contiguous block of memory with fields at known fixed offsets:

; Represents: struct Point { int x; int y; };
section .data
    point1 dd 10, 20   ; x = 10 (offset 0), y = 20 (offset 4)

section .text
    mov eax, [point1]       ; load x (point1.x)
    mov ebx, [point1 + 4]   ; load y (point1.y)

Endianness: Byte Order in Memory

An important and often confusing aspect of data representation is endianness — the order in which bytes of a multi-byte value are stored in memory.

ArchitectureDefault Endianness
x86 / x86-64Little-endian (least significant byte first)
ARMBi-endian (usually configured little-endian; supports both)

Example: storing the 32-bit value 0x12345678 in little-endian memory:

Address:  0x1000  0x1001  0x1002  0x1003
Byte:      78      56      34      12

The least significant byte (0x78) is stored at the lowest memory address. This matters enormously when debugging with a memory dump, reading network protocol data (which is often big-endian, called “network byte order”), or working across different architectures.

Worked Example: Decoding an IEEE 754 Float by Hand

Understanding the theory behind IEEE 754 is one thing; actually decoding a real value bit by bit makes it concrete. Let’s decode the 32-bit representation 0x40490FDB, which happens to be the standard single-precision approximation of π (pi).

Step 1: Convert to binary

0x40490FDB = 01000000 01001001 00001111 11011011

Step 2: Split into sign, exponent, and mantissa

Sign (1 bit):      0
Exponent (8 bits): 10000000
Mantissa (23 bits): 10010010000111111011011

Step 3: Interpret each field

  • Sign = 0 → positive number.
  • Exponent = 10000000 in binary = 128 in decimal. IEEE 754 single precision uses a bias of 127, so the actual exponent = 128 − 127 = 1.
  • Mantissa = 10010010000111111011011, interpreted as an implicit leading 1 followed by a fraction: 1.10010010000111111011011 in binary.

Step 4: Compute the final value

value = (-1)^sign × 1.mantissa × 2^exponent
value = 1 × 1.57079637... × 2^1
value ≈ 3.14159265...

This matches π to the precision available in 32 bits. This worked example illustrates several things at once: why floating-point numbers have limited precision (only 23 mantissa bits to represent the fractional part), why the “implicit leading 1” trick saves a bit of storage (since every normalized binary number in scientific notation has a leading 1, it doesn’t need to be stored explicitly), and why converting between binary float representations and decimal values isn’t a trivial one-to-one mapping — which is precisely why certain decimal values (like 0.1) can never be represented exactly in binary floating point, leading to the classic “floating-point rounding error” behavior seen in nearly every programming language.

At the Assembly level, you’d rarely decode a float by hand like this in practice — but understanding that this exact bit-level process is what’s happening whenever movss/movsd (x86-64) or ldr s0/ldr d0 (ARM) load a floating-point value demystifies a lot of otherwise confusing floating-point behavior seen in higher-level languages.

Data Representation Summary Table

Data TypeTypical SizeRepresentation Method
Integer (unsigned)1/2/4/8 bytesPlain binary
Integer (signed)1/2/4/8 bytesTwo’s complement
Character1 byteASCII (or multi-byte for UTF-8)
StringVariableSequence of bytes, often null-terminated
Single-precision float4 bytesIEEE 754 single precision
Double-precision float8 bytesIEEE 754 double precision
BooleanTypically 1 byte or a flag bit0 = false, non-zero = true
Pointer/Address4 bytes (32-bit) / 8 bytes (64-bit)Plain binary memory address

Boolean Values and Bit-Level Packing

High-level languages typically provide a dedicated bool or boolean type, but at the Assembly level, there’s no such distinct data type — boolean logic is represented using the same integer storage as everything else, with the convention that zero means false and any non-zero value means true.

section .data
    isActive db 1      ; "true", stored as byte value 1
    isDone   db 0       ; "false", stored as byte value 0

section .text
    cmp byte [isActive], 0
    je  is_false        ; jump if isActive == 0 (false)
    ; ... handle "true" case ...
is_false:
    ; ... handle "false" case ...

Because a full byte is often wasteful for representing a single true/false value, real-world code frequently packs multiple boolean flags into individual bits of a single byte or word — a technique often called a bitfield or flag byte. This is extremely common in systems programming, hardware register interfaces, and file format headers, where every byte of space matters.

Example: packing 8 independent boolean flags into a single byte

; Bit layout: [7][6][5][4][3][2][1][0]
;              |  |  |  |  |  |  |  +-- flag_readable
;              |  |  |  |  |  |  +----- flag_writable
;              |  |  |  |  |  +-------- flag_executable
;              |  |  |  |  +----------- flag_hidden
;              |  |  |  +-------------- (unused bits...)

section .data
    fileFlags db 0

section .text
    ; Set the "writable" bit (bit 1) without disturbing other bits
    or byte [fileFlags], 00000010b

    ; Clear the "hidden" bit (bit 3) without disturbing other bits
    and byte [fileFlags], 11110111b

    ; Test whether the "executable" bit (bit 2) is set
    test byte [fileFlags], 00000100b
    jnz is_executable

This pattern — using or to set specific bits, and (with an inverted mask) to clear specific bits, and test to check specific bits without modifying anything — is the standard toolkit for bitfield manipulation in Assembly, and it directly underlies how high-level language bitflag enums, hardware register configuration (like setting control bits in a memory-mapped I/O device), and compact data structures in file formats and network protocols all work under the hood. Understanding this also explains why operations like flags |= SOME_FLAG or flags &= ~SOME_FLAG are so common in C and similar languages — they’re direct, readable expressions of exactly the or/and-with-inverted-mask pattern shown above.

Practical Use Cases

  • Interfacing with C code: understanding exact data sizes and layouts is essential when writing Assembly that calls or is called by C functions, since struct layouts and calling conventions depend on precise byte sizes.
  • Network programming: converting between little-endian (host) and big-endian (network) byte order is a routine, critical task (htons/ntohs in C ultimately do simple byte-swapping).
  • File format parsing: reverse engineers and low-level programmers must understand exact binary layouts (integers, floats, strings) to correctly parse file formats like ELF, PE, or custom binary formats.
  • Cryptography implementations: precise control over bit-level and byte-level data representation is essential for implementing hashing and encryption algorithms correctly.

Common Mistakes

  1. Confusing signed and unsigned interpretation — using mul when you meant imul (or vice versa) silently produces incorrect results rather than an error.
  2. Endianness confusion — assuming a specific byte order when reading raw memory or network data without accounting for the actual architecture’s endianness.
  3. Incorrect data size directives — using dd (4 bytes) when you meant dq (8 bytes) causes data misalignment and corrupted subsequent values.
  4. Forgetting null termination — when working with C-style strings, forgetting the trailing 0 byte can cause buffer over-reads.
  5. Floating-point precision assumptions — assuming exact decimal representation is possible in IEEE 754 binary floating point, when many decimal fractions (like 0.1) can’t be represented exactly.

Best Practices

  • Always match your data declaration size (db/dw/dd/dq) to the actual size needed by your algorithm or data structure.
  • Be explicit and consistent about signed vs. unsigned operations — choose the correct instruction variant deliberately.
  • When working with binary file formats or network protocols, always verify and handle endianness explicitly.
  • Use a hex editor or debugger’s memory view to verify your data is laid out exactly as expected.

FAQs

Q: Does Assembly language have data types like C or Java? Not in the same sense. Assembly only understands raw bit patterns of specific sizes (byte, word, dword, qword); the meaning (signed integer, float, character) is determined entirely by which instructions you use to operate on that data.

Q: What is two’s complement and why is it used? Two’s complement is the standard way of representing signed integers in binary. It’s used because it allows addition and subtraction to work identically for both signed and unsigned numbers using the same hardware circuitry, simplifying CPU design.

Q: Why does endianness matter? Endianness determines the byte order of multi-byte values in memory. It matters when directly inspecting raw memory, exchanging data between systems (especially over networks), or parsing binary file formats.

Q: How are floating-point numbers different from integers at the hardware level? Floating-point numbers use the IEEE 754 format (sign, exponent, mantissa) and are processed by dedicated floating-point/SIMD hardware units, while integers use straightforward binary (or two’s complement) representation processed by the ALU.

Summary and Key Takeaways

  • All data in Assembly language is ultimately represented as binary bit patterns; the interpretation (signed, unsigned, float, character) depends entirely on the instructions used.
  • Signed integers use two’s complement representation; floating-point numbers use the IEEE 754 standard.
  • Text is represented via character encodings like ASCII, typically stored as sequences of bytes with conventions like null-termination.
  • Endianness (byte ordering) is a critical, architecture-dependent detail that affects memory layout and cross-system data exchange.
  • Understanding data representation at this level is foundational for systems programming, reverse engineering, cryptography, and low-level debugging.

References

  • Intel® 64 and IA-32 Architectures Software Developer’s Manual, Vol. 1, Chapter 4 (Data Types) — https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html
  • IEEE 754-2019 Standard for Floating-Point Arithmetic — https://ieeexplore.ieee.org/document/8766229
  • ARM Architecture Reference Manual (Data Types and Endianness) — https://developer.arm.com/documentation
  • GNU Assembler (GAS) Documentation — https://sourceware.org/binutils/docs/as/
Total
0
Shares

Leave a Reply

Previous Post
What is an instruction set in Assembly language

What Is an Instruction Set in Assembly Language?

Next Post
Define mnemonic in the context of Assembly language

What Is a Mnemonic in Assembly Language? A Complete Explanation

Related Posts