Loading content...
Every read from or write to a storage device requires answering a fundamental question: where? The operating system must specify exactly which location on the disk contains the desired data or should receive new data. This location specification is disk addressing.
Over the decades, disk addressing has evolved from explicit physical coordinates to fully abstracted logical numbering. This evolution reflects the growing complexity of storage devices and the need for portable, scalable, and device-independent storage interfaces.
This page provides an exhaustive examination of the two primary addressing schemes—CHS (Cylinder-Head-Sector) and LBA (Logical Block Addressing)—their structure, limitations, translation mechanisms, and ongoing relevance in modern systems.
By the end of this page, you will understand: the structure and limitations of CHS addressing; the design and capacity advantages of LBA; address translation between CHS and LBA; historical capacity barriers and their solutions; the role of addressing in boot processes and partition tables; and practical tools for examining disk addressing.
Cylinder-Head-Sector (CHS) addressing was the original method for specifying disk locations, directly mapping to the physical geometry of the drive. Understanding CHS is essential for interpreting legacy systems, boot sectors, and partition table structures.
A CHS address is a three-tuple (C, H, S):
| Component | Description | Numbering | Typical Range |
|---|---|---|---|
| C (Cylinder) | Radial position of the track | 0-based | 0 to 65,535 (theoretical) |
| H (Head) | Which platter surface (read/write head) | 0-based | 0 to 254 |
| S (Sector) | Which sector on the track | 1-based | 1 to 63 (BIOS) or 255 (ATA) |
Key Quirk: Sector numbers start at 1, not 0. This historical anomaly affects all CHS calculations.
In BIOS interrupt 13h (INT 13h), CHS addresses are encoded in registers as follows:
CH register: Lower 8 bits of cylinder number
CL register: Bits 7-6: Upper 2 bits of cylinder (10-bit total)
Bits 5-0: Sector number (6 bits, values 1-63)
DH register: Head number (8 bits, values 0-255)
This encoding limits:
The maximum addressable capacity with CHS:
$$\text{Max Sectors} = C \times H \times S = 1024 \times 256 \times 63 = 16,515,072 \text{ sectors}$$
$$\text{Max Capacity} = 16,515,072 \times 512 \text{ bytes} = 8,455,716,864 \text{ bytes} \approx 7.88 \text{ GiB} \approx 8.46 \text{ GB}$$
This is the famous 8.4 GB barrier (or 7.88 GiB) that plagued PC systems in the late 1990s.
Practical BIOS Limits:
Actual limits were often more restrictive due to BIOS implementations:
| Barrier | Cause | Limit |
|---|---|---|
| 504 MB | Original BIOS: 1024 cyl × 16 heads × 63 sectors | ~504 MB |
| 2.1 GB | BIOS cylinder bit interpretation issue | ~2.1 GB |
| 7.88 GiB | Full INT 13h CHS capability | ~7.88 GiB |
| 8.4 GB | Various BIOS workarounds | ~8.4 GB |
| Year | Barrier | Cause | Solution |
|---|---|---|---|
| 1983 | 10 MB | XT BIOS design | Hardware BIOS upgrades |
| 1989 | 504 MB | BIOS: 16 heads max; CHS: 1024 cyl × 16 heads × 63 sectors | BIOS translation modes |
| 1994 | 2.1 GB | Cylinder signed integer overflow | BIOS bug fixes |
| 1996 | 7.88 GiB | Full BIOS INT 13h CHS limit | INT 13h Extensions (LBA) |
| 1998 | 8.4 GB | Combined BIOS/ATA limits | 48-bit LBA adoption |
| 2001 | 137 GB | 28-bit LBA limit (ATA-5) | 48-bit LBA (ATA-6) |
| Future | 8 ZB | 48-bit LBA theoretical limit @ 512B | Not yet reached |
The earliest significant barrier was 504 MB. Original PC BIOS assumed a maximum of 16 heads (not 256) because early ST-506 drives never exceeded 16 heads. Combined with 1024 cylinders and 63 sectors: 1024 × 16 × 63 × 512 = 528,482,304 bytes ≈ 504 MB. This required BIOS translation or LBA adoption.
As drives exceeded BIOS CHS limits, intermediate solutions emerged to stretch the addressing space. These translation modes allowed larger drives while maintaining CHS compatibility.
Translation works by presenting a logical geometry to the operating system that differs from the physical geometry of the drive:
OS sees: Logical CHS (translated)
↓
BIOS translates
↓
Drive sees: Physical CHS or LBA
| Mode | Description | Mechanism | Maximum Capacity |
|---|---|---|---|
| Normal (None) | No translation; CHS passed directly | OS CHS = Drive CHS | 504 MB (16 heads × 1024 cyl × 63 sec) |
| Large (ECHS) | Extended CHS; increases logical heads | Multiply heads, divide cylinders | ~8.4 GB |
| LBA | Translate CHS to/from LBA at BIOS | OS uses CHS; BIOS converts to LBA | ~8.4 GB (BIOS CHS limit) |
The Large/ECHS mode works by manipulating the reported geometry:
Example:
Translation:
Result:
Conversion Algorithm:
$$\text{Logical Cylinder} = \text{Physical Cylinder} \div 2^n$$ $$\text{Logical Head} = (\text{Physical Head} \times 2^n) + (\text{Physical Cylinder} \mod 2^n)$$
Where $n$ is chosen such that logical cylinders ≤ 1024.
In LBA translation mode, the BIOS:
This added a layer of abstraction but remained limited by the BIOS's 8.4 GB addressable space.
Modern BIOS (since mid-1990s) auto-detects drive geometry via ATA IDENTIFY DEVICE command and automatically selects appropriate translation. The BIOS setup typically shows 'Auto', 'Large', 'LBA', or 'None' for each drive. 'Auto' (letting BIOS decide) is almost always correct.
Logical Block Addressing (LBA) revolutionized disk addressing by abstracting away physical geometry entirely. Instead of specifying cylinder, head, and sector, LBA uses a simple linear numbering scheme.
Concept:
Example:
LBA 0 → First sector on the disk (typically MBR/GPT)
LBA 1 → Second sector
...
LBA 2,000,000,000 → Somewhere deep in a 1 TB drive
| Standard | LBA Bits | Max LBA | Max Capacity (512B sectors) | Max Capacity (4K sectors) |
|---|---|---|---|---|
| ATA-1 to ATA-5 | 28-bit | 268,435,455 | 137 GB | 1.1 TB |
| ATA-6+ | 48-bit | 281,474,976,710,655 | 144 PB | 1.15 EB |
| NVMe | 64-bit | 18,446,744,073,709,551,615 | 9.4 ZB | 75 ZB |
To address drives larger than 8.4 GB while maintaining BIOS compatibility, INT 13h Extensions (Enhanced Disk Drive, EDD) were developed:
Extended Read (INT 13h, AH=42h):
struct Disk_Address_Packet {
uint8_t packet_size; /* Size of this packet (16 or greater) */
uint8_t reserved; /* Zero */
uint16_t sector_count; /* Number of sectors to transfer */
uint16_t buffer_offset; /* Buffer offset (real mode) */
uint16_t buffer_segment; /* Buffer segment (real mode) */
uint64_t start_lba; /* Starting LBA (64-bit) */
};
This allows direct LBA addressing in BIOS code, bypassing CHS entirely.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
/* * Comprehensive LBA ↔ CHS Conversion * * These utilities demonstrate the fundamental relationship between * physical geometry addressing (CHS) and logical block addressing (LBA). */ #include <stdint.h>#include <stdbool.h>#include <stdio.h> /* Geometry descriptor */typedef struct { uint32_t cylinders; /* Number of cylinders */ uint32_t heads; /* Heads per cylinder */ uint32_t sectors_per_track; /* Sectors per track (63 for BIOS) */} Geometry; /* CHS address */typedef struct { uint32_t cylinder; /* 0-based */ uint32_t head; /* 0-based */ uint32_t sector; /* 1-based (important!) */} CHSAddress; /* * Convert LBA to CHS * * Formula derivation: * LBA = (C × Heads + H) × SectorsPerTrack + (S - 1) * * Solving for S, H, C: * S = (LBA mod SectorsPerTrack) + 1 * temp = LBA / SectorsPerTrack * H = temp mod Heads * C = temp / Heads */CHSAddress lba_to_chs(uint64_t lba, const Geometry* geom) { CHSAddress chs; /* Sector is 1-based */ chs.sector = (lba % geom->sectors_per_track) + 1; /* Calculate head and cylinder */ uint64_t temp = lba / geom->sectors_per_track; chs.head = temp % geom->heads; chs.cylinder = temp / geom->heads; return chs;} /* * Convert CHS to LBA * * Formula: LBA = (C × Heads + H) × SectorsPerTrack + (S - 1) */uint64_t chs_to_lba(CHSAddress chs, const Geometry* geom) { return ((uint64_t)chs.cylinder * geom->heads + chs.head) * geom->sectors_per_track + (chs.sector - 1); /* Sector is 1-based */} /* * Validate CHS address against geometry */bool is_valid_chs(CHSAddress chs, const Geometry* geom) { return (chs.cylinder < geom->cylinders) && (chs.head < geom->heads) && (chs.sector >= 1 && chs.sector <= geom->sectors_per_track);} /* * Calculate maximum addressable LBA for a geometry */uint64_t max_lba(const Geometry* geom) { return (uint64_t)geom->cylinders * geom->heads * geom->sectors_per_track - 1;} /* * Calculate capacity in bytes */uint64_t capacity_bytes(const Geometry* geom, uint16_t bytes_per_sector) { return (max_lba(geom) + 1) * bytes_per_sector;} /* * Demonstrate the 8.4 GB barrier */void demonstrate_limits() { /* Maximum BIOS CHS geometry */ Geometry bios_max = { .cylinders = 1024, .heads = 256, .sectors_per_track = 63 }; uint64_t max_sectors = (uint64_t)bios_max.cylinders * bios_max.heads * bios_max.sectors_per_track; uint64_t max_bytes = max_sectors * 512; printf("BIOS CHS Maximum:"); printf(" Cylinders: %u", bios_max.cylinders); printf(" Heads: %u", bios_max.heads); printf(" Sectors/Track: %u", bios_max.sectors_per_track); printf(" Max Sectors: %llu", max_sectors); printf(" Max Bytes: %llu", max_bytes); printf(" Max GiB: %.2f", max_bytes / (1024.0 * 1024.0 * 1024.0)); printf(" Max GB: %.2f", max_bytes / 1e9); /* Output: * Max Sectors: 16515072 * Max Bytes: 8455716864 * Max GiB: 7.88 * Max GB: 8.46 */} /* * Common geometry examples */void geometry_examples() { /* 504 MB barrier geometry */ Geometry small = {1024, 16, 63}; printf("504 MB barrier: %llu bytes", capacity_bytes(&small, 512)); /* Typical translated geometry for ~40 GB drive */ Geometry translated = {16383, 16, 63}; printf("Translated: %llu bytes", capacity_bytes(&translated, 512)); /* These numbers often don't reflect physical reality */ /* They're just BIOS translation artifacts */}28-bit LBA (ATA-5 and earlier) supports 2²⁸ = 268,435,456 sectors, or 137.4 GB with 512-byte sectors. This barrier was overcome by 48-bit LBA in ATA-6 (2003), supporting 144 PB—far beyond current drive capacities.
Modern drives present LBA to the host but must internally translate to physical locations. This translation is more complex than a simple formula due to zone bit recording, defect management, and performance optimization.
With Zone Bit Recording (ZBR), tracks have varying sectors-per-track. The drive maintains a zone table:
| Zone | Cylinder Range | Sectors/Track | Starting LBA |
|---|---|---|---|
| 0 | 0-9,999 | 800 | 0 |
| 1 | 10,000-19,999 | 768 | 128,000,000 |
| 2 | 20,000-29,999 | 736 | 250,880,000 |
| ... | ... | ... | ... |
Given an LBA, the firmware:
No magnetic surface is perfect. Defects are managed via:
P-List (Primary Defect List):
G-List (Growth Defect List):
Defect Remapping:
When a defect is discovered:
Spare Area Allocation:
Drives reserve capacity for spares:
Modern drives use multi-level translation:
LBA → Zone Lookup → In-Zone Offset → Physical CHS → Defect Remap → Final Physical CHS
Performance Considerations:
If the drive's translation tables (stored in firmware area) become corrupted, the mapping from LBA to physical location is lost. Data may still exist on platters, but locating it requires reverse-engineering the zone layout—a task for specialized data recovery services.
The Master Boot Record (MBR) partition table, designed in 1983, uses CHS addressing—creating fundamental limitations that still affect systems today.
Each MBR partition entry (16 bytes) contains:
| Offset | Size | Field | Description |
|---|---|---|---|
| 0x00 | 1 | Status | 0x80 = bootable, 0x00 = not bootable |
| 0x01 | 3 | CHS Start | Starting CHS address |
| 0x04 | 1 | Type | Partition type code (e.g., 0x07 = NTFS) |
| 0x05 | 3 | CHS End | Ending CHS address |
| 0x08 | 4 | LBA Start | Starting LBA (32-bit) |
| 0x0C | 4 | Sector Count | Number of sectors (32-bit) |
The 3-byte CHS fields encode:
Byte 0: Head number (8 bits, 0-255)
Byte 1: Sector in bits 5-0 (6 bits, 1-63)
Upper 2 bits of cylinder in bits 7-6
Byte 2: Lower 8 bits of cylinder
This limits CHS to 1024 cylinders × 256 heads × 63 sectors = 8.4 GB.
MBR contains both CHS and LBA fields for each partition. Modern systems use LBA exclusively:
32-bit LBA Limit:
$$\text{Max Sectors} = 2^{32} - 1 = 4,294,967,295$$ $$\text{Max Capacity} = 4,294,967,295 \times 512 = 2,199,023,255,040 \text{ bytes} \approx 2 \text{ TiB}$$
This is the 2 TiB barrier for MBR partitioning.
For drives larger than 8.4 GB, CHS fields use placeholder values:
Systems ignore CHS and use LBA fields instead.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
/* * MBR Partition Table Parser * * Demonstrates extraction of CHS and LBA addressing from * MBR partition entries. */ #include <stdint.h>#include <stdio.h> /* MBR Partition Entry (16 bytes) */typedef struct __attribute__((packed)) { uint8_t status; /* 0x80 = bootable */ uint8_t chs_start[3]; /* Starting CHS address */ uint8_t type; /* Partition type */ uint8_t chs_end[3]; /* Ending CHS address */ uint32_t lba_start; /* Starting LBA (little-endian) */ uint32_t sector_count; /* Number of sectors (little-endian) */} MBRPartitionEntry; /* CHS Address structure */typedef struct { uint16_t cylinder; uint8_t head; uint8_t sector;} CHSAddr; /* * Extract CHS from MBR's packed 3-byte format * * Byte 0: Head * Byte 1: Sector (bits 5-0), Cylinder high (bits 7-6) * Byte 2: Cylinder low */CHSAddr extract_chs(const uint8_t* packed) { CHSAddr chs; chs.head = packed[0]; chs.sector = packed[1] & 0x3F; /* Bits 5-0 */ chs.cylinder = packed[2] | ((packed[1] & 0xC0) << 2); /* 10-bit */ return chs;} /* * Check if CHS indicates "beyond limit" */int is_chs_beyond_limit(CHSAddr chs) { /* Common "overflow" indicators */ return (chs.cylinder == 1023 && chs.head >= 254) || (chs.cylinder >= 1023);} /* * Parse and display partition entry */void parse_partition(const MBRPartitionEntry* entry, int index) { printf("Partition %d:", index); printf(" Status: %s (0x%02X)", entry->status == 0x80 ? "Bootable" : "Non-bootable", entry->status); printf(" Type: 0x%02X", entry->type); CHSAddr start = extract_chs(entry->chs_start); CHSAddr end = extract_chs(entry->chs_end); printf(" CHS Start: C=%u, H=%u, S=%u%s", start.cylinder, start.head, start.sector, is_chs_beyond_limit(start) ? " [beyond limit]" : ""); printf(" CHS End: C=%u, H=%u, S=%u%s", end.cylinder, end.head, end.sector, is_chs_beyond_limit(end) ? " [beyond limit]" : ""); printf(" LBA Start: %u", entry->lba_start); printf(" Sector Count: %u", entry->sector_count); uint64_t size_bytes = (uint64_t)entry->sector_count * 512; printf(" Size: %.2f GiB (%.2f GB)", size_bytes / (1024.0 * 1024.0 * 1024.0), size_bytes / 1e9); printf("");} /* * Read and parse MBR from disk image */void parse_mbr(const uint8_t* mbr_sector) { /* Check MBR signature */ if (mbr_sector[510] != 0x55 || mbr_sector[511] != 0xAA) { printf("Invalid MBR signature!"); return; } printf("Valid MBR signature (0x55AA) "); /* Partition table starts at offset 446 */ const MBRPartitionEntry* entries = (const MBRPartitionEntry*)(mbr_sector + 446); for (int i = 0; i < 4; i++) { if (entries[i].type != 0) { /* Non-empty partition */ parse_partition(&entries[i], i + 1); } } /* Calculate 2 TiB limit */ printf("MBR LBA Limit: 2^32 sectors = %.2f TiB", (4294967295ULL * 512) / (1024.0 * 1024.0 * 1024.0 * 1024.0));}Despite GPT's advantages, MBR remains common for: legacy BIOS boot (non-UEFI systems), removable media interoperability, embedded systems, and systems with <2 TiB drives where simplicity is valued. GPT is required for drives >2 TiB and UEFI boot.
The GUID Partition Table (GPT), part of the UEFI specification, eliminates CHS entirely and uses 64-bit LBA addressing for vast capacity support.
LBA 0: Protective MBR (for legacy compatibility)
LBA 1: Primary GPT Header
LBA 2-33: Primary Partition Entries (128 entries × 128 bytes)
LBA 34-...: Usable Data Space
LBA -33...: Secondary Partition Entries (backup)
LBA -1: Secondary GPT Header (backup)
Each GPT partition entry is 128 bytes (expandable):
| Offset | Size | Field |
|---|---|---|
| 0x00 | 16 | Partition Type GUID |
| 0x10 | 16 | Unique Partition GUID |
| 0x20 | 8 | Starting LBA (64-bit) |
| 0x28 | 8 | Ending LBA (64-bit) |
| 0x30 | 8 | Attributes |
| 0x38 | 72 | Partition Name (UTF-16LE) |
With 64-bit LBA addressing:
$$\text{Max Sectors} = 2^{64} - 1 \approx 1.84 \times 10^{19}$$ $$\text{Max Capacity (512B)} = 2^{64} \times 512 = 9.44 \times 10^{21} \text{ bytes} = 9.44 \text{ ZB}$$ $$\text{Max Capacity (4KB)} = 2^{64} \times 4096 = 7.55 \times 10^{22} \text{ bytes} = 75.5 \text{ ZB}$$
This capacity exceeds any foreseeable storage technology by many orders of magnitude.
GPT includes a Protective MBR at LBA 0:
| Aspect | MBR | GPT |
|---|---|---|
| CHS Support | Yes (legacy) | No (LBA only) |
| LBA Bit Width | 32-bit | 64-bit |
| Max Disk Size | 2 TiB | 9.44 ZB (512B) / 75.5 ZB (4K) |
| Max Partitions | 4 primary (or 3 + extended) | 128 (expandable) |
| Redundancy | None | Backup header at disk end |
| Integrity Check | None | CRC32 checksums |
| Boot Support | BIOS (legacy) | UEFI (native) |
| Partition ID | 1-byte type code | 16-byte GUID |
Best practice for new installations: use GPT for all disks, regardless of size. Even for <2 TiB drives, GPT provides CRC integrity checks, backup partition tables, and modern UEFI boot support. MBR should only be used for legacy compatibility requirements.
Understanding how to examine and work with disk addresses is essential for troubleshooting, forensics, and low-level storage management. Here are practical tools and techniques.
1234567891011121314151617181920212223242526272829303132333435363738394041
#!/bin/bash# Examining disk addressing on Linux # 1. View block device size in sectorssudo blockdev --getsz /dev/sda# Output: Total 512-byte sectors# Example: 1953525168 (for 1TB drive) # 2. Get sector size informationsudo blockdev --getss /dev/sda # Logical sector size (usually 512)sudo blockdev --getpbsz /dev/sda # Physical sector size (512 or 4096) # 3. View drive geometry (mostly fictional on modern drives)sudo fdisk -l /dev/sda# Look for: "Disklabel type: gpt" or "dos" (MBR)# Note: "255 heads, 63 sectors/track" is standard fake geometry # 4. Detailed partition info with LBAsudo parted /dev/sda unit s print# Shows partitions with sector-based start/end # 5. Read raw MBR (first 512 bytes)sudo dd if=/dev/sda bs=512 count=1 | xxd | head -20 # 6. Read GPT header (LBA 1)sudo dd if=/dev/sda bs=512 skip=1 count=1 | xxd # 7. Check if disk uses GPTsudo gdisk -l /dev/sda | grep "GPT:" # 8. SMART data (includes sector counts)sudo smartctl -i /dev/sda# Shows: "User Capacity" and "Sector Sizes" # 9. View kernel's view of sectorscat /sys/block/sda/size# Total sectors (512-byte) # 10. hdparm for LBA infosudo hdparm -I /dev/sda | grep -A5 "LBA"# Shows: "LBA48 user addressable sectors"12345678910111213141516171819202122232425262728293031323334353637383940414243444546
# Windows Disk Addressing Examination # 1. Get disk information including sector sizesGet-Disk | Format-List * # 2. Specific sector information$disk = Get-Disk -Number 0$disk.LogicalSectorSize # Usually 512$disk.PhysicalSectorSize # 512 or 4096$disk.Size # Total bytes # 3. Calculate total sectors$totalSectors = $disk.Size / $disk.LogicalSectorSize"Total LBA Sectors: $totalSectors" # 4. Partition information with offsetsGet-Partition -DiskNumber 0 | Format-List PartitionNumber, Offset, Size # 5. Convert offset to LBA$partition = Get-Partition -DiskNumber 0 -PartitionNumber 1$startLBA = $partition.Offset / 512"Partition 1 starts at LBA: $startLBA" # 6. Diskpart for detailed info# Run: diskpart# Commands:# list disk# select disk 0# detail disk# list partition# select partition 1# detail partition # 7. Check partition style (MBR vs GPT)Get-Disk | Select-Object Number, PartitionStyle # 8. WMI query for disk geometry (legacy)Get-WmiObject Win32_DiskDrive | Select-Object Caption, TotalSectors, BytesPerSector # 9. Read MBR via PowerShell (requires admin)$stream = [System.IO.File]::OpenRead("\\.\PhysicalDrive0")$buffer = New-Object byte[] 512$stream.Read($buffer, 0, 512) | Out-Null$stream.Close()# Check signature"{0:X2} {1:X2}" -f $buffer[510], $buffer[511] # Should be "55 AA"When checking sector sizes, remember: LogicalSectorSize is what the OS uses for addressing; PhysicalSectorSize is the actual hardware sector. If they differ (e.g., 512 logical / 4096 physical), you have a 512e drive, and alignment matters for performance.
We have traced the evolution of disk addressing from physical coordinates to logical abstraction. Let's consolidate the key concepts:
What's Next:
With addressing schemes understood, we complete our disk structure exploration with Disk Geometry—examining how physical and logical geometry relate, the translation mechanisms that bridge them, and how operating systems discover and work with drive geometry.
You now understand the evolution and mechanics of disk addressing—from physical CHS coordinates to abstracted LBA numbering. This knowledge is essential for understanding boot processes, partition tables, capacity barriers, and low-level storage management.