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:
| Unit | Size | Common Use |
|---|---|---|
| Bit | 1 binary digit | Smallest unit; flags, boolean values |
| Nibble | 4 bits | Half a byte; one hex digit |
| Byte | 8 bits | Smallest addressable unit of memory; ASCII characters |
| Word | 16 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.
| Base | Example (value 42) | Common Prefix/Suffix |
|---|---|---|
| Binary | 101010b or 0b101010 | b suffix or 0b prefix |
| Decimal | 42 | none |
| Hexadecimal | 2Ah or 0x2A | h 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:
- Write the positive value in binary.
- Invert all bits (one’s complement).
- 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 Value | Signed (Two’s Complement) Value |
|---|---|---|
00000000 | 0 | 0 |
01111111 | 127 | 127 |
10000000 | 128 | -128 |
11111111 | 255 | -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.
| Character | ASCII (Decimal) | ASCII (Hex) |
|---|---|---|
| ‘A’ | 65 | 0x41 |
| ‘a’ | 97 | 0x61 |
| ‘0’ | 48 | 0x30 |
| ‘ ‘ (space) | 32 | 0x20 |
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)"]
| Component | Bits | Purpose |
|---|---|---|
| Sign | 1 | 0 = positive, 1 = negative |
| Exponent | 8 | Scales the value (biased by 127) |
| Mantissa | 23 | Fractional 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) | Size | GAS Equivalent | Purpose |
|---|---|---|---|
db | 1 byte | .byte | Define byte(s) |
dw | 2 bytes | .word / .short | Define word(s) |
dd | 4 bytes | .long / .int | Define doubleword(s) / 32-bit float |
dq | 8 bytes | .quad | Define quadword(s) / 64-bit double |
resb | Reserve N bytes (uninitialized) | .skip / .space | Reserve 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.
| Architecture | Default Endianness |
|---|---|
| x86 / x86-64 | Little-endian (least significant byte first) |
| ARM | Bi-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 =
10000000in 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 Type | Typical Size | Representation Method |
|---|---|---|
| Integer (unsigned) | 1/2/4/8 bytes | Plain binary |
| Integer (signed) | 1/2/4/8 bytes | Two’s complement |
| Character | 1 byte | ASCII (or multi-byte for UTF-8) |
| String | Variable | Sequence of bytes, often null-terminated |
| Single-precision float | 4 bytes | IEEE 754 single precision |
| Double-precision float | 8 bytes | IEEE 754 double precision |
| Boolean | Typically 1 byte or a flag bit | 0 = false, non-zero = true |
| Pointer/Address | 4 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/ntohsin 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
- Confusing signed and unsigned interpretation — using
mulwhen you meantimul(or vice versa) silently produces incorrect results rather than an error. - Endianness confusion — assuming a specific byte order when reading raw memory or network data without accounting for the actual architecture’s endianness.
- Incorrect data size directives — using
dd(4 bytes) when you meantdq(8 bytes) causes data misalignment and corrupted subsequent values. - Forgetting null termination — when working with C-style strings, forgetting the trailing
0byte can cause buffer over-reads. - 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/
