Data Representation: Binary, Two's Complement, Floating Point and Endianness

Hardware stores everything as bits (0/1).

Concept

Bits, Bytes and Number Bases Hardware stores everything as bits (0/1). Groups of bits encode numbers, characters, instructions and addresses; the meaning comes from how they are used.

Example (decimal 171) Hexadecimal (16) 0xAB (one hex digit = 4 bits)

An *n*-bit unsigned integer represents 0 … 2ⁿ − 1.

Signed Integers: Two's Complement Every modern CPU represents signed integers in two's complement:

The most significant bit has weight −2ⁿ⁻¹; an *n*-bit value ranges from −2ⁿ⁻¹ to 2ⁿ⁻¹ − 1 (int8: −128 … 127).

Negation: invert all bits and add 1 (-x == ~x + 1).

The same adder circuit adds signed and unsigned numbers — only the interpretation of the result (and of the flags, Module 4) differs.

Overflow: unsigned arithmetic wraps modulo 2ⁿ (defined in C). Signed overflow is undefined behaviour in C — the hardware wraps, but the compiler may assume it never happens.

Sign extension copies the sign bit when widening a signed value (int8 -2 → int32 -2 = 0xFFFFFFFE); zero extension fills with zeros for unsigned values.

Floating Point (IEEE 754) A float (32 bits) stores sign (1 bit) | exponent (8 bits, bias 127) | fraction (23 bits); value = (−1)^sign × 1.fraction × 2^(exponent − 127). A double uses 1 | 11 | 52 bits (bias 1023). Consequences:

Most decimal fractions (0.1) have no exact binary representation → rounding errors.

Special values: ±0, ±∞, NaN, subnormals.

Never compare floats with == after arithmetic; use a tolerance.

Multi-byte values can be stored with the least significant byte first (little-endian: x86, most ARM configurations, RISC-V) or most significant byte first (big-endian: network byte order, many older CPUs, some protocols). It matters whenever bytes cross a boundary: files, networks, CAN frames, memory-mapped registers (htonl/ntohl).

Data Type Sizes C only guarantees minimum sizes; the actual sizes depend on the ABI. Linux x86-64 uses LP64: int 32 bits, long and pointers 64 bits (64-bit Windows uses LLP64: long is 32 bits). Use <stdint.h> types (uint32_t) when the exact width matters.

Why It Matters

Integer overflow, sign-extension bugs, endianness errors in protocols and floating-point comparison bugs are among the most common low-level defects — and several (integer overflow into buffer sizes) are security vulnerabilities.

Syntax

uint8_t u = 250; u += 10;          // wraps to 4
int32_t w = (int8_t)0xFE;          // sign-extends to -2
uint32_t net = htonl(host);        // host -> network (big-endian) byte order
fabs(a - b) < 1e-9                 // compare floating point with a tolerance

Example

2.1 Bits of integers, overflow, sign extension, endianness and IEEE 754

#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <limits.h>

static void bits(const char *label, uint32_t v, int width)
{
    printf("%-22s", label);
    for (int i = width - 1; i >= 0; i--) {
        putchar((v >> i) & 1 ? '1' : '0');
        if (i % 4 == 0 && i) putchar('_');
    }
    putchar('\n');
}

int main(void)
{
    /* unsigned binary and hexadecimal */
    bits("13 (unsigned 8-bit)", 13, 8);
    printf("%-22s0x%02X = %u\n", "0xAB", 0xAB, 0xAB);

    /* two's complement: -x = ~x + 1 */
    int8_t pos = 5, neg = -5;
    bits("+5 (int8_t)", (uint8_t)pos, 8);
    bits("~5", (uint8_t)~pos, 8);
    bits("-5 = ~5 + 1", (uint8_t)neg, 8);
    printf("int8_t range: %d .. %d\n", INT8_MIN, INT8_MAX);

    /* overflow: unsigned wraps (defined), signed conversion wraps on this machine */
    uint8_t u = 250; u += 10;
    printf("uint8_t 250 + 10   = %u (wraps modulo 256)\n", u);
    int8_t s = (int8_t)(127 + 1);
    printf("(int8_t)(127 + 1)  = %d\n", s);

    /* sign extension vs zero extension */
    int8_t b = -2;
    printf("int8_t -2 -> int32: %d (0x%08X), as uint8_t -> uint32: %u\n", (int32_t)b, (uint32_t)(int32_t)b, (uint32_t)(uint8_t)b);

    /* endianness: 
…

Expected Result

~5 + 1 produced exactly the bit pattern of −5.

The 32-bit CAN identifier 0x18FEF100 is stored with its lowest byte (00) first: this x86-64 machine is little-endian.

−6.25 = −1.5625 × 2²: sign 1, biased exponent 129 (= 2 + 127), fraction 0x480000 (the .5625 part).

sizeof(long) = 8 confirms the LP64 model.

13 (unsigned 8-bit)   0000_1101
0xAB                  0xAB = 171
+5 (int8_t)           0000_0101
~5                    1111_1010
-5 = ~5 + 1           1111_1011
int8_t range: -128 .. 127
uint8_t 250 + 10   = 4 (wraps modulo 256)
(int8_t)(127 + 1)  = -128
int8_t -2 -> int32: -2 (0xFFFFFFFE), as uint8_t -> uint32: 254
0x18FEF100 in memory: 00 F1 FE 18 -> little-endian
-6.25f bits: sign 1, exponent 129 (unbiased 2), fraction 0x480000
0.1 + 0.2 = 0.30000000000000004 (not exactly 0.3)
sizeof: char 1, short 2, int 4, long 8, void* 8, float 4, double 8

Common Mistakes

Relying on signed overflow wrapping (undefined behaviour; optimizers exploit it).

Mixing signed and unsigned in comparisons: -1 < 1u is false because −1 converts to UINT_MAX.

Using char for bytes: its signedness is implementation-defined; use uint8_t.

Copying a struct with multi-byte fields straight onto the network or into a file without fixing byte order.

if (x == 0.3) after floating-point arithmetic.

Best Practices

Use fixed-width types (uint16_t, int32_t) for data that crosses boundaries.

Serialize explicitly, byte by byte, in a documented byte order.

Check for overflow before it happens (if (a > INT_MAX - b)) or use __builtin_add_overflow.

Use integers (e.g. millivolts, cents) instead of floats when exactness matters.

Interview Relevance

Very high: "what is two's complement and why is it used?", "how do you detect endianness?", "why is 0.1 + 0.2 != 0.3?", "what happens on integer overflow?", "sign extension vs zero extension?".

Practice Questions

Write the 8-bit two's complement patterns of −1, −128 and 127.

Write a function that swaps the byte order of a uint32_t using shifts and masks.

Decode the IEEE 754 bits of 0.1f by hand and compare with a program.

What does (unsigned)-1 > 0 evaluate to and why?

Quick Quiz

Q1. (Multiple choice) The range of int8_t is: A) 0..255 B) −127..127 C) −128..127 D) −255..255

Answer: C (verified: -128 .. 127).

Q2. (Predict the output) uint8_t u = 250; u += 10; printf("%u", u);

Answer: 4 — unsigned arithmetic wraps modulo 256 (verified).

Q3. (True/False) On x86-64 Linux, the bytes of 0x18FEF100 are stored in memory as 18 FE F1 00.

Answer: False. x86 is little-endian: 00 F1 FE 18 (verified).

Q4. (Find the bug) for (unsigned i = n - 1; i >= 0; i--) process(a[i]);

Answer: i >= 0 is always true for an unsigned variable; when i reaches 0, i-- wraps to UINT_MAX and the loop never ends (and indexes out of bounds).

Coding Challenge

Write uint32_t bswap32(uint32_t x) using only shifts, masks and OR, and verify it on 0x18FEF100.

#include <stdio.h>
#include <stdint.h>

static uint32_t bswap32(uint32_t x)
{
    return (x >> 24) | ((x >> 8) & 0x0000FF00u) | ((x << 8) & 0x00FF0000u) | (x << 24);
}

int main(void)
{
    uint32_t v = 0x18FEF100;
    printf("0x%08X -> 0x%08X -> 0x%08X\n", v, bswap32(v), bswap32(bswap32(v)));
    printf("matches GCC builtin: %s\n", bswap32(v) == __builtin_bswap32(v) ? "yes" : "no");
    return 0;
}