What Is Computation? Bits, Bytes, Binary & Hex
Master the language hardware speaks: why computers are binary, how to fluently read and write in binary and hex, and why a kibibyte is not a kilobyte.
Overview
Every program you will ever write — whether it's a web server, a GPU shader, or a Linux kernel patch — ultimately executes as a river of ones and zeros flowing through silicon. Before you can reason about memory layouts, system calls, or CPU pipelines, you need to be fluent in the language that hardware actually speaks: binary. This module builds that fluency from absolute first principles.
We start with a single question that most textbooks skip: why binary? The answer turns out to be deeply physical — it is not arbitrary. Then we climb through the number systems (binary, octal, hexadecimal, decimal) not as party tricks but as tools you will use daily when reading kernel source, debugging memory dumps, and interpreting hardware registers. Every concept is grounded in a concrete example you can run right now.
By the end of this module you will have the instincts of a systems programmer when it comes to numbers. You will see 0xDEADBEEF and immediately parse it. You will know why a 32-bit address space is 4 GiB, not 4 GB, and you will be able to explain the difference to someone else. These are not trivia items — they are load-bearing intuitions that every concept in this roadmap depends on.
One last note before we dive in: mathematics here is your friend, not your enemy. Place-value arithmetic is the same idea whether the base is 10, 2, or 16. If you have ever counted change, you already understand the core principle. We are just going to make it precise and extend it.
The Linux kernel is, at its core, a program that manages hardware on behalf of other programs. Hardware communicates entirely through registers, memory-mapped I/O addresses, and bit-fields packed into integers. When you read a kernel driver, you will see lines like val |= (1 << BIT_ENABLE) or addr & ~PAGE_MASK. If binary and hexadecimal are not second nature, these lines look like line noise. Once they are, they read like plain English.
More concretely: every kernel data structure is designed with alignment and size in mind. The size of a pointer (4 bytes on 32-bit, 8 bytes on 64-bit) determines the entire memory model of the kernel. Every system call argument, every flags field, every hardware register is a number in binary. Your first kernel patch will almost certainly touch something that requires you to manipulate individual bits. This module is not a detour — it is the foundation that everything else is built on, and getting it right here saves you hours of confusion later.
Binary to Hexadecimal: The Place-Value Structure of One Byte
Converting one byte (8 bits) to hexadecimal by splitting into two 4-bit nibbles and looking up each nibble's hex digit. No arithmetic needed — just a table lookup per nibble.
The concepts
01Why Binary? The Physics of a Bit
The choice to use binary is not a historical accident or a committee decision. It is forced by physics.
A transistor — the fundamental building block of a CPU — is a switch. In its simplest use, it is either conducting current (ON) or not conducting current (OFF). You could try to build a computer that distinguishes ten voltage levels (0 V, 0.5 V, 1.0 V, … 4.5 V) to represent decimal digits directly. Engineers tried this in the 1940s. The problem is noise: real circuits have thermal noise, manufacturing variation, voltage fluctuations. Two voltage levels that are far apart (say 0 V and 3.3 V) are extremely easy to distinguish reliably even in the presence of noise. Ten levels crammed into the same range are nearly impossible to keep separate.
This is the key insight: binary is the most noise-tolerant encoding possible. A single binary digit — a bit (binary digit) — can only be 0 or 1. Any voltage above a threshold is a 1; anything below is a 0. You can build billions of these switches on a chip the size of a fingernail and they still work reliably.
The word 'bit' was coined by statistician John Tukey in 1947. Claude Shannon, in his landmark 1948 paper 'A Mathematical Theory of Communication', proved that the bit is the fundamental unit of information — you cannot convey more than 1 bit of information with a two-state device, and you need at least 1 bit to distinguish two equally likely outcomes.
So: hardware is binary because physics demands it, and everything else — all the software, all the data structures, all the kernel code — is built on top of that physical reality.
02The Bit: 0 and 1, True and False, Off and On
A single bit carries the smallest possible piece of information: a choice between two states. Different contexts give these states different names, but they are always the same underlying thing:
| Context | State 0 | State 1 |
|---|---|---|
| Electronics | LOW voltage | HIGH voltage |
| Logic | False | True |
| Arithmetic | Zero | One |
| Storage | Erased | Written |
One bit alone is not very useful — it can only distinguish two things. But the power of bits comes from combining them. Two bits give you four possible states (00, 01, 10, 11). Three bits give eight. In general, N bits give you 2^N possible states. This exponential relationship is one of the most important facts in computing:
- 8 bits → 256 states (0 to 255)
- 16 bits → 65,536 states
- 32 bits → 4,294,967,296 states (~4 billion)
- 64 bits → 18,446,744,073,709,551,616 states (~18 quintillion)
This is why upgrading from 32-bit to 64-bit CPUs was such a big deal: the addressable memory space went from 4 GB to a theoretical 16 exabytes. Not because the CPU got faster — because the address register got wider, doubling the number of bits and squaring the number of representable addresses.
A key habit to build: when you see a power of 2, think 'that is N bits'. 256 = 2^8 (8 bits). 4096 = 2^12 (12 bits, a common page size). 65536 = 2^16 (16 bits, a port number range). These relationships will appear constantly in kernel code.
/* Demonstrate the 2^N relationship in C */
#include <stdio.h>
#include <stdint.h>
int main(void) {
/* How many values fit in N bits? */
for (int n = 1; n <= 64; n *= 2) {
/* 1ULL is an unsigned 64-bit 1, so the shift never overflows */
unsigned long long states = 1ULL << n;
printf("%2d bits = %llu states\n", n, states);
}
return 0;
}
/* Output:
1 bits = 2 states
2 bits = 4 states
4 bits = 16 states
8 bits = 256 states
16 bits = 65536 states
32 bits = 4294967296 states
64 bits = 18446744073709551616 states
*/
03Binary: Place Value in Base 2
You already know place value from decimal. In decimal (base 10), the number 3,742 means:
3 × 10³ + 7 × 10² + 4 × 10¹ + 2 × 10⁰ = 3000 + 700 + 40 + 2 = 3742
Each position is worth 10 times the position to its right. Binary (base 2) is identical in structure, but each position is worth 2 times the position to its right:
The 8-bit binary number 10110101 means:
| Bit position | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| Place value | 128 | 64 | 32 | 16 | 8 | 4 | 2 | 1 |
| Digit | 1 | 0 | 1 | 1 | 0 | 1 | 0 | 1 |
| Contribution | 128 | 0 | 32 | 16 | 0 | 4 | 0 | 1 |
128 + 32 + 16 + 4 + 1 = 181
Note that bit positions are counted from the right, starting at 0. The rightmost bit is called the Least Significant Bit (LSB) — it contributes the smallest value (1). The leftmost bit of a fixed-width number is the Most Significant Bit (MSB) — it contributes the largest value. In an 8-bit number, the MSB is worth 128.
To convert decimal to binary, use repeated division by 2: divide by 2, record the remainder (0 or 1), repeat with the quotient, then read remainders from bottom to top.
Example: 181 → 2
- 181 ÷ 2 = 90 remainder 1 (LSB)
- 90 ÷ 2 = 45 remainder 0
- 45 ÷ 2 = 22 remainder 1
- 22 ÷ 2 = 11 remainder 0
- 11 ÷ 2 = 5 remainder 1
- 5 ÷ 2 = 2 remainder 1
- 2 ÷ 2 = 1 remainder 0
- 1 ÷ 2 = 0 remainder 1 (MSB)
Read from bottom to top: 10110101 ✓
Practice this until it is automatic. In kernel development, you will often need to know if a specific bit in a register is set or not, which requires understanding exactly which bit position corresponds to which power of 2.
/* Convert decimal to binary and print it, bit by bit */
#include <stdio.h>
#include <stdint.h>
void print_binary(uint8_t n) {
/* Print MSB first: loop from bit 7 down to bit 0 */
for (int i = 7; i >= 0; i--) {
/* Test bit i: shift a 1 left by i positions, AND with n */
printf("%d", (n >> i) & 1);
}
printf(" = %u decimal\n", n);
}
int main(void) {
print_binary(0); /* 00000000 = 0 */
print_binary(1); /* 00000001 = 1 */
print_binary(128); /* 10000000 = 128 */
print_binary(181); /* 10110101 = 181 */
print_binary(255); /* 11111111 = 255 */
return 0;
}
04The Byte: 8 Bits, the Fundamental Unit
While the bit is the smallest unit of information, it is almost never addressed individually in hardware. The practical fundamental unit of computer memory is the byte: a group of 8 bits.
Why 8? Early computers used varying word sizes (6-bit, 7-bit). IBM's System/360 (1964) standardized on 8-bit bytes because 8 bits can encode 256 values — enough for uppercase and lowercase letters, digits, punctuation, and control codes. This was just enough for the ASCII character set (which only needed 7 bits, with one bit to spare). The 8-bit byte won the industry standards war and has been the atomic unit of addressable memory ever since.
Every address in your computer's memory addresses one byte. The C sizeof operator returns sizes in bytes. When the Linux kernel defines u8, u16, u32, u64, those are unsigned integers of 1, 2, 4, and 8 bytes respectively.
An 8-bit byte can represent:
- Unsigned integers: 0 to 255 (2^8 − 1)
- Signed integers (two's complement): −128 to +127
- One ASCII character: 'A' = 65, 'a' = 97, '0' = 48
- Two hexadecimal digits: exactly (we will see why shortly)
The fact that one byte = exactly two hex digits is not a coincidence — it is one of the key reasons hexadecimal was chosen as the standard notation for binary data. Every kernel memory dump you ever read will be in hex, with bytes as the unit.
#include <stdio.h>
#include <stdint.h>
#include <limits.h> /* CHAR_BIT */
int main(void) {
printf("Bits per byte on this machine: %d\n", CHAR_BIT);
/* CHAR_BIT is guaranteed to be at least 8 by the C standard.
On nearly every modern machine it is exactly 8. */
uint8_t byte = 0b10110101; /* C binary literal (C23/GCC extension) */
printf("Value: %u\n", byte); /* 181 */
printf("As char (if printable): %c\n", (char)byte); /* non-printable here */
/* ASCII: 'A' is 65 = 0x41 = 0b01000001 */
uint8_t letter = 'A';
printf("'A' in decimal: %u, hex: 0x%02X, binary: ", letter, letter);
for (int i = 7; i >= 0; i--) printf("%d", (letter >> i) & 1);
printf("\n");
/* Output: 'A' in decimal: 65, hex: 0x41, binary: 01000001 */
return 0;
}
05Hexadecimal: Binary's Human-Readable Shorthand
Binary is what hardware understands, but writing out 32 or 64 bits by hand is error-prone and tedious. Decimal hides the structure (there is no obvious visual relationship between decimal 4096 and the binary 1000000000000). Hexadecimal (base 16) is the solution: it groups binary digits into chunks of 4, making the bit structure visible while keeping the notation compact.
Hexadecimal uses 16 symbols: 0–9 and A–F (or a–f), where A=10, B=11, C=12, D=13, E=14, F=15. In C and most systems contexts, hex literals are prefixed with 0x (e.g. 0xFF, 0xDEAD).
The magic of hex: 4 bits = exactly 1 hex digit, 8 bits = exactly 2 hex digits. This means you can convert between binary and hex without any arithmetic — just look up each group of 4 bits:
| Binary | Hex | Decimal |
|---|---|---|
| 0000 | 0 | 0 |
| 0001 | 1 | 1 |
| 0010 | 2 | 2 |
| 0011 | 3 | 3 |
| 0100 | 4 | 4 |
| 0101 | 5 | 5 |
| 0110 | 6 | 6 |
| 0111 | 7 | 7 |
| 1000 | 8 | 8 |
| 1001 | 9 | 9 |
| 1010 | A | 10 |
| 1011 | B | 11 |
| 1100 | C | 12 |
| 1101 | D | 13 |
| 1110 | E | 14 |
| 1111 | F | 15 |
Example: convert 10110101 (binary, 8 bits) to hex:
- Split into groups of 4 from the right:
1011|0101 1011= B,0101= 5- Result: 0xB5
Verify: 0xB5 = 11×16 + 5 = 176 + 5 = 181. ✓ (matches what we computed in the binary section)
A 32-bit address like 0xC0001234 is instantly readable as 8 hex digits (4 bytes). The same number in binary is 32 characters long. This is why every kernel dump, debugger, and disassembler uses hex.
Memorize the 16 entries in the table above. It takes about 20 minutes and will save you hours over a lifetime of kernel work.
/* Print a 32-bit value in all three bases, showing the hex/binary grouping */
#include <stdio.h>
#include <stdint.h>
void print_all_bases(uint32_t val) {
printf("Decimal : %u\n", val);
printf("Hex : 0x%08X\n", val); /* 8 hex digits, zero-padded */
printf("Binary : ");
for (int i = 31; i >= 0; i--) {
printf("%d", (val >> i) & 1);
if (i % 4 == 0 && i != 0) printf("_"); /* group by 4 */
}
printf("\n\n");
}
int main(void) {
print_all_bases(0xDEADBEEF);
print_all_bases(0x00001000); /* 4096: a common page size */
print_all_bases(255);
return 0;
}
/* 0xDEADBEEF =
Binary: 1101_1110_1010_1101_1011_1110_1110_1111
Note how each group of 4 bits maps directly to one hex digit: D E A D B E E F */
06Converting Between Bases: Systematic Methods
You need four conversion directions. Here they are, with the fastest method for each.
Binary → Decimal: Sum the place values of all 1-bits.
00101010 = 32 + 8 + 2 = 42
Decimal → Binary: Repeated division by 2, read remainders bottom-up (shown in the binary section).
Hex → Decimal: Each hex digit d at position p (counting from right, zero-based) contributes d × 16^p.
0x2A = 2×16 + 10×1 = 32 + 10 = 42
Decimal → Hex: Repeated division by 16.
42 ÷ 16 = 2 remainder A (=10). 2 ÷ 16 = 0 remainder 2. Read bottom-up: 0x2A.
Binary → Hex (the most useful, zero-arithmetic): Group 4 bits, look up each group.
00101010 → 0010 1010 → 2 A → 0x2A
Hex → Binary: Expand each hex digit to 4 bits.
0x2A → 0010 1010 → 00101010
The binary↔hex conversions are the ones you will use constantly. The others are needed occasionally but you can always use printf or Python's hex() / bin() / int(x, base) for the messy ones. What matters is that you can do them in your head quickly for small numbers and understand exactly what printf("%x", n) is doing when you read kernel code.
A practical shortcut: memorize the powers of 2 up to 2^16: 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536. Seeing any of these in code is an immediate signal that bit-level structure is involved.
/* Python is excellent for base conversion during kernel debugging.
These are the standard library functions you will use: */
# Decimal 42 in all bases
n = 42
print(bin(n)) # '0b101010'
print(oct(n)) # '0o52'
print(hex(n)) # '0x2a'
# Parse from any base
print(int('101010', 2)) # 42 (binary string -> int)
print(int('2a', 16)) # 42 (hex string -> int)
print(int('0x2a', 16)) # 42 (with prefix)
print(int('0x2a', 0)) # 42 (auto-detect prefix)
# Kernel-style: pretty-print a 32-bit register value
reg = 0xDEADBEEF
print(f'0x{reg:08X}') # 0xDEADBEEF
print(f'{reg:032b}') # 32-char binary, no prefix
# Group by nibbles:
bs = f'{reg:032b}'
print(' '.join(bs[i:i+4] for i in range(0, 32, 4)))
# 1101 1110 1010 1101 1011 1110 1110 1111
07Octal: The Third Base You Will Encounter
Base 8 (octal) is less common than hex but appears in one critical place in Linux: Unix file permissions.
Octal digits run 0–7. Each octal digit represents exactly 3 bits. The permission bits of a Unix file are 9 bits organized as three groups of 3 (owner, group, others), each group encoding read (r=4), write (w=2), execute (x=1). This 3-bit structure maps perfectly to one octal digit per group.
chmod 755 means:
- Owner: 7 = 4+2+1 = rwx
- Group: 5 = 4+0+1 = r-x
- Others: 5 = 4+0+1 = r-x
In C, octal literals are written with a leading zero: 0755 (not 755). This is a famous source of bugs — int x = 010; is 8, not 10.
In the kernel source, you will see patterns like:
static const struct file_operations foo_fops = { ... };
/* ... */
mkdir(path, 0755);
Always read that leading zero as 'this is octal'. Outside of permissions, you will rarely need octal, but you absolutely must recognize it.
#include <stdio.h>
int main(void) {
/* TRAP: leading zero means octal in C! */
int a = 10; /* decimal ten */
int b = 010; /* octal 010 = decimal 8 (!) */
int c = 0x10; /* hex 0x10 = decimal 16 */
printf("10 = %d\n", a); /* 10 */
printf("010 = %d\n", b); /* 8 */
printf("0x10= %d\n", c); /* 16 */
/* Unix permission 0755: rwxr-xr-x */
int perm = 0755;
printf("0755 octal = %d decimal\n", perm); /* 493 */
printf("Owner: r=%d w=%d x=%d\n",
(perm >> 6) & 4 ? 1 : 0,
(perm >> 6) & 2 ? 1 : 0,
(perm >> 6) & 1);
return 0;
}
08Kilobytes vs Kibibytes: The Prefix Wars
Here is a confusion that causes real bugs in kernel development and storage systems. The prefixes 'kilo', 'mega', 'giga' mean powers of 1000 in the SI (metric) system — always have, always will. But in computing, memory and storage were historically measured in powers of 2, and people borrowed the SI prefixes:
| What people called it | What they meant | Actual SI meaning |
|---|---|---|
| 1 KB (kilobyte) | 1024 bytes | 1000 bytes |
| 1 MB (megabyte) | 1,048,576 bytes | 1,000,000 bytes |
| 1 GB (gigabyte) | 1,073,741,824 bytes | 1,000,000,000 bytes |
This ambiguity caused genuine consumer confusion (hard drives advertised in decimal GB looked smaller than OS-reported binary GB) and led the IEC to introduce binary prefixes in 1998:
| Binary prefix | Symbol | Value | Power of 2 |
|---|---|---|---|
| kibibyte | KiB | 1,024 bytes | 2^10 |
| mebibyte | MiB | 1,048,576 bytes | 2^20 |
| gibibyte | GiB | 1,073,741,824 bytes | 2^30 |
| tebibyte | TiB | 1,099,511,627,776 bytes | 2^40 |
The Linux kernel documentation and source code use the IEC prefixes correctly. You will see SZ_1K, SZ_4K, SZ_1M macros in <linux/sizes.h> — these are powers of 2. The kernel's Kconfig and documentation use MiB/GiB. When a kernel developer says 'the stack is 8 KiB', they mean exactly 8 × 1024 = 8192 bytes.
In practice: when you see KB/MB/GB in Linux kernel context, assume binary (powers of 2) unless the context is a storage device or network bandwidth, where decimal is standard. Better: just use KiB/MiB/GiB unambiguously.
#include <stdio.h>
/* linux/sizes.h style definitions */
#define SZ_1K (1UL << 10) /* 1,024 bytes = 1 KiB */
#define SZ_4K (1UL << 12) /* 4,096 bytes = 4 KiB (default page size) */
#define SZ_1M (1UL << 20) /* 1,048,576 bytes = 1 MiB */
#define SZ_1G (1UL << 30) /* 1,073,741,824 bytes = 1 GiB */
int main(void) {
printf("1 KiB = %lu bytes\n", SZ_1K);
printf("4 KiB = %lu bytes\n", SZ_4K); /* x86 page size */
printf("1 MiB = %lu bytes\n", SZ_1M);
printf("1 GiB = %lu bytes\n", SZ_1G);
/* 32-bit address space */
unsigned long space = 1UL << 32;
printf("2^32 bytes = %lu bytes = %.2f GiB = %.2f GB\n",
space,
(double)space / SZ_1G, /* 4.00 GiB */
(double)space / 1000000000.0); /* 4.29 GB */
return 0;
}
09Bitwise Operations: Manipulating Individual Bits
In the kernel, you will constantly need to set, clear, toggle, or test individual bits in integers. The C bitwise operators are the primary tool for this.
AND (&): Bit is 1 only if both input bits are 1. Used to test or clear bits. OR (|): Bit is 1 if either input bit is 1. Used to set bits. XOR (^): Bit is 1 if the input bits differ. Used to toggle bits. NOT (~): Flips all bits. Used to build masks. Left shift (<<): Multiply by 2 per position. Used to position a bit mask. Right shift (>>): Divide by 2 per position (for unsigned). Used to extract bits.
The canonical idioms, which you will see thousands of times in kernel source:
/* Set bit N */ val |= (1U << N);
/* Clear bit N */ val &= ~(1U << N);
/* Toggle bit N */ val ^= (1U << N);
/* Test bit N */ if (val & (1U << N)) { /* bit N is set */ }
/* Extract bits [high:low] */
uint8_t field = (val >> low) & ((1U << (high - low + 1)) - 1);
This last pattern (extracting a bit field) is everywhere in hardware drivers. A hardware register might pack 'device class' in bits [7:4] and 'revision' in bits [3:0]. You extract them with shifts and masks.
Think of & with a mask as a spotlight: it illuminates (keeps) only the bits where the mask has a 1, and blacks out (zeros) everything else. That mental model makes these patterns obvious.
#include <stdio.h>
#include <stdint.h>
int main(void) {
uint8_t reg = 0b00101100; /* Imagine a hardware register */
/* Set bit 0 (enable some feature) */
reg |= (1U << 0);
printf("After set bit 0: 0x%02X (%d)\n", reg, reg); /* 0x2D = 45 */
/* Clear bit 3 */
reg &= ~(1U << 3);
printf("After clear bit 3: 0x%02X (%d)\n", reg, reg); /* 0x25 = 37 */
/* Test bit 5 */
if (reg & (1U << 5))
printf("Bit 5 is SET\n");
else
printf("Bit 5 is CLEAR\n");
/* Extract a 4-bit field from bits [5:2] */
uint8_t field = (reg >> 2) & 0x0F; /* 0x0F = 0b00001111: 4-bit mask */
printf("Bits [5:2] = %u\n", field);
return 0;
}
10How Memory is Addressed: Bytes, Words, and Alignment
Memory is an enormous array of bytes, each with a unique numeric address. On a 64-bit system, addresses are 64-bit integers (though current CPUs only use 48 or 57 of those bits for virtual addresses). Address 0x1000 refers to byte number 4096. Address 0x1001 refers to the next byte.
When a CPU reads a 4-byte integer (a uint32_t), it reads 4 consecutive bytes from memory. The address of the integer is the address of its first (lowest-addressed) byte. On most architectures, CPUs can only efficiently read multi-byte values that are aligned — meaning the address is a multiple of the data size.
- A 2-byte short at address 0x1002 is aligned (0x1002 is divisible by 2). ✓
- A 4-byte int at address 0x1004 is aligned (0x1004 is divisible by 4). ✓
- A 4-byte int at address 0x1001 is misaligned — some CPUs will generate a fault, others will do two slower reads. ✗
In C, you can check alignment with the modulo operator or the bitwise AND trick: a value x is aligned to N bytes (where N is a power of 2) if and only if (x & (N-1)) == 0. This works because N-1 in binary is all 1s in the lower log₂(N) bits — ANDing with it extracts exactly the bits that must be zero for alignment.
Example: is address 0x1004 aligned to 4 bytes? 0x1004 & (4-1) = 0x1004 & 0x3 = ...0100 & ...0011 = 0x0000. Yes.
The Linux kernel is full of alignment checks. PAGE_ALIGNED(addr) expands to !(addr & ~PAGE_MASK) — a bitwise test. Understanding these requires fluency in binary.
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
/* Is addr aligned to align bytes? (align must be a power of 2) */
static inline bool is_aligned(uintptr_t addr, size_t align) {
return (addr & (align - 1)) == 0;
}
int main(void) {
/* Test a range of addresses for 4-byte alignment */
for (uintptr_t addr = 0x1000; addr <= 0x100F; addr++) {
printf("0x%04lX: 4-byte aligned? %s\n",
addr,
is_aligned(addr, 4) ? "yes" : "no");
}
/* Output: 0x1000 yes, 0x1001 no, 0x1002 no, 0x1003 no, 0x1004 yes, ... */
return 0;
}
11Hexadecimal in Practice: Reading Kernel Output
Let's ground everything by reading a real piece of kernel output. When you run dmesg after boot, you will see lines like:
[ 0.000000] BIOS-e820: [mem 0x0000000000000000-0x000000000009efff] usable
[ 0.000000] BIOS-e820: [mem 0x0000000000100000-0x00000000bffd2fff] usable
[ 0.000000] kernel: .text : 0xffffffff81000000 - 0xffffffff82600de5
Every number starting with 0x is an address in hexadecimal. Let's decode:
0x0000000000100000= 1 MiB (0x100000 = 1×16^5 = 1,048,576 = 2^20). The first megabyte is reserved for legacy BIOS.0xffffffff81000000is in the upper 2 GiB of the 64-bit address space — this is where the kernel maps its own code in virtual memory.
Now look at a kernel Oops (crash report):
RIP: 0010:0xffffffffc09a1234
Code: 48 89 e5 48 83 ec 10 48 8b 05 ...
The Code: line is a hex dump — the raw bytes of machine code, two hex digits per byte. 48 89 e5 is the mov %rsp, %rbp instruction in x86-64 machine code. Each pair of hex digits is one byte.
This is why hex mastery is non-negotiable: kernel debugging means reading raw memory, addresses, and bytecode, all in hex. The exercises at the end of this module will have you doing exactly this.
/* Produce a hex dump of a data structure, like a mini kernel memory dump */
#include <stdio.h>
#include <stdint.h>
#include <string.h>
void hexdump(const void *data, size_t len) {
const uint8_t *p = (const uint8_t *)data;
for (size_t i = 0; i < len; i++) {
if (i % 16 == 0) printf("%04zx: ", i); /* address */
printf("%02X ", p[i]); /* byte as hex */
if (i % 16 == 15 || i == len - 1) printf("\n");
}
}
int main(void) {
/* Dump the bytes of a string -- notice how ASCII maps to hex */
const char *msg = "Hello, kernel!";
hexdump(msg, strlen(msg));
/* Output:
0000: 48 65 6C 6C 6F 2C 20 6B 65 72 6E 65 6C 21
H=0x48, e=0x65, l=0x6C, o=0x6F ... */
/* Dump a 32-bit integer (shows byte order) */
uint32_t val = 0xDEADBEEF;
printf("\n0xDEADBEEF in memory (little-endian x86):\n");
hexdump(&val, sizeof(val));
/* Output: EF BE AD DE (lowest byte first on x86) */
return 0;
}
Key insights
- ✦Binary is forced by physics: two voltage levels are the only noise-tolerant encoding possible at transistor scale. Everything else follows from this.
- ✦N bits gives you exactly 2^N states — this exponential relationship explains why adding one bit doubles capacity and why 64-bit addressing is transformative.
- ✦One byte = 8 bits = exactly 2 hex digits. This is why hexadecimal is the universal language of binary data, not decimal.
- ✦The binary↔hex conversion requires zero arithmetic: just group 4 bits and look up the hex digit. Memorizing the 16 entries lets you read hex dumps instantly.
- ✦A leading zero in C means octal, not decimal. 0755 is 493, not 755. This is a live pitfall in system programming.
- ✦A KiB is 1024 bytes; a KB should be 1000 bytes. The Linux kernel uses KiB/MiB/GiB (powers of 2) consistently. Conflating these causes real sizing bugs.
- ✦Alignment is a bitwise property: address x is N-byte aligned (N a power of 2) iff (x & (N-1)) == 0. This pattern appears everywhere in kernel code.
- ✦The four bitwise idioms (set with |, clear with & ~, toggle with ^, test with &) are the vocabulary of hardware programming — internalize them now.
Common pitfalls
- ⚠Writing `int x = 010` and expecting 10: the leading zero makes it octal (= 8). Always double-check numeric literals that start with 0.
- ⚠Confusing KB with KiB: a '4 KB page' in kernel docs means 4096 bytes (4 KiB). Mixing decimal and binary prefixes when computing buffer sizes leads to off-by-a-factor-of-1.024 bugs.
- ⚠Off-by-one in bit positions: bit 0 is the LSB (value 1), bit 7 is the MSB of a byte (value 128). Confusing 'bit number' with 'bit value' is a constant source of errors.
- ⚠Left-shifting into the sign bit: `1 << 31` on a 32-bit `int` is undefined behavior in C. Always use `1U << 31` or `1UL << 31` when working with the high bits of unsigned types.
- ⚠Assuming big-endian when x86 is little-endian: if you store 0xDEADBEEF at address 0x1000, byte at 0x1000 is 0xEF (the least significant byte), not 0xDE. Hex dumps on x86 reverse your visual expectation.
- ⚠Forgetting that `~` has lower precedence than `<<` in C: write `~(1U << N)` with explicit parentheses, not `~1U << N` (which shifts *after* flipping).
Practice
- 01easy
Write a C program that takes an integer from the command line and prints it in binary, octal, decimal, and hexadecimal, clearly labeling each. Format the binary output with underscores every 4 bits (e.g. `1011_0101`). Test with 0, 1, 255, 256, 65535, and 2147483647.
- 02easy
Implement a function `int popcount(uint32_t x)` that counts the number of 1-bits in a 32-bit unsigned integer WITHOUT using any library function or the `__builtin_popcount` intrinsic. Test it against `__builtin_popcount` for 100 random values. Then look up Brian Kernighan's bit-counting trick and rewrite using it.
- 03medium
Write a C function `hexdump(const void *data, size_t len)` that prints output in classic kernel hexdump style: address on the left (8 hex digits), 16 bytes per row as space-separated hex pairs in the middle, and the ASCII representation on the right (print '.' for non-printable bytes). Run it on a struct containing several fields of different types.
- 04medium
Decode a fake hardware register by hand AND in C. Given a 32-bit value `0xA5C31204`, extract the following fields: bits [31:28] (device class), bits [27:20] (vendor ID), bits [15:8] (flags), bits [3:0] (revision). Print each field in decimal and hex. Then write a function that packs these fields back into a single uint32_t given the four values, and verify that pack(unpack(x)) == x.
- 05hard
Run `xxd /bin/ls | head -20` on a Linux or macOS system. Identify: (1) the ELF magic bytes at offset 0 (0x7F 'E' 'L' 'F'), (2) the byte at offset 4 that says '2' for 64-bit, and (3) the byte at offset 5 that says '1' for little-endian. Then write a small C program that opens its own argv[0] binary, reads the first 16 bytes, and prints them as a hex dump with field annotations.
Resources
Paste any C snippet and instantly see the compiled assembly — invaluable for understanding how bit operations translate to actual machine instructions.
Hands-on videos building an 8-bit computer from logic gates up — makes binary and hardware concrete in a way no textbook can.
The definitive reference for bit manipulation tricks used in production compilers and the Linux kernel. Chapter 1–2 are directly relevant to this module.
The rigorous mathematical foundation for everything in this module, from a legend. Dense but rewarding.
Wikipedia's article on binary prefixes (KiB/MiB/GiB) is thorough and explains the history of the KB/KiB confusion with primary sources.
The canonical kernel header defining SZ_1K, SZ_4K, SZ_1M etc. — see how the kernel itself defines powers of 2.
The founding paper of information theory. Section 1 defines the bit. Reading the original is a rite of passage.
Beej's guides are models of clear technical writing. This chapter covers all the C bitwise operators with excellent examples.
Checkpoints — you can now…
- ✓You can convert any 8-bit binary number to hexadecimal in your head, without a calculator, in under 10 seconds.
- ✓You can explain, in one sentence, why computers use binary instead of decimal.
- ✓You can correctly use the set, clear, toggle, and test bit idioms in C (`|=`, `&= ~`, `^=`, `&`) on a specific bit position.
- ✓You can write a C function that extracts an arbitrary bit field [high:low] from a uint32_t using shifts and masks.
- ✓You know that `0755` in a C source file means octal (decimal 493), not decimal 755.
- ✓You can explain the difference between 1 KB and 1 KiB, and state which one the Linux kernel uses when it says '4 KB page'.
- ✓You can read a 16-byte hex dump line and identify each byte's value, the starting address, and the ASCII representation.
- ✓You can verify whether a memory address is N-byte aligned using a single bitwise AND operation.