Chapter 2: On-Disk Layout Fundamentals
Chapter 2: On-Disk Layout Fundamentals
To understand a filesystem, one must inspect its physical byte layout on raw storage. Like an architectural blueprint, the on-disk geometry of ext4 partitions raw disk sectors into structured, predictable regions that allow the kernel to locate, read, allocate, and repair data without scanning millions of unrelated sectors.
This chapter details the fundamental on-disk structures of ext4: the 1024-byte bootloader padding, the primary and backup superblocks (ext4_super_block), 64-bit Block Group Descriptors (ext4_group_desc), allocation bitmaps, inode tables, and flexible block group optimizations (flex_bg).
Bootloader Blocks and Reserved Space
The first \(1024\text{ bytes}\) (Byte 0 to Byte 1023) of any ext4 partition are strictly reserved by the filesystem specification:
┌────────────────────────────────────────────────────────────────────────┐
│ PARTITION INITIAL OFFSET │
│ │
│ Byte 0 ────────────── Byte 1023 Byte 1024 ────────── Byte 2047 │
│ ┌───────────────────────────────┐ ┌───────────────────────────────┐ │
│ │ Reserved Bootloader Block │ │ Primary Superblock (1024 B) │ │
│ │ (x86 MBR stage 1.5 / GRUB pad)│ │ Magic: 0xEF53 │ │
│ └───────────────────────────────┘ └───────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
- Bootloader Block (\(0 - 1023\text{ bytes}\)): Used by legacy x86 bootloaders (such as LILO or GRUB Stage 1.5) to embed boot code and partition lookup tables. The ext4 kernel driver never writes to or modifies this initial 1 KiB region.
- Block Size Alignment: On systems formatted with a \(1024\text{ byte}\) block size (\(1\text{ KiB}\)), the boot block occupies Block 0, and the primary superblock begins at Block 1. On standard \(4096\text{ byte}\) block systems (\(4\text{ KiB}\)), Block 0 spans Bytes 0 through 4095; the first 1024 bytes are padding, and the superblock begins at Byte offset 1024 within Block 0.
The Superblock Structure
The Superblock is the master control record of the filesystem. It records total block and inode counts, block size, volume label, UUID, mount counts, feature flags, and pointers to journal devices.
The superblock is exactly 1024 bytes in size, defined in C by the ext4_super_block structure in fs/ext4/ext4.h:
struct ext4_super_block {
/* 0x00 */ __le32 s_inodes_count; /* Total inode count */
/* 0x04 */ __le32 s_blocks_count_lo; /* Total block count (lower 32 bits) */
/* 0x08 */ __le32 s_r_blocks_count_lo; /* Reserved blocks for superuser */
/* 0x0C */ __le32 s_free_blocks_count_lo; /* Free blocks (lower 32 bits) */
/* 0x10 */ __le32 s_free_inodes_count; /* Free inode count */
/* 0x14 */ __le32 s_first_data_block; /* First data block (0 or 1) */
/* 0x18 */ __le32 s_log_block_size; /* Block size = 1024 << s_log_block_size */
/* 0x1C */ __le32 s_log_cluster_size; /* Cluster size */
/* 0x20 */ __le32 s_blocks_per_group; /* Number of blocks in each group (e.g. 32768) */
/* 0x24 */ __le32 s_clusters_per_group;
/* 0x28 */ __le32 s_inodes_per_group; /* Number of inodes per group (e.g. 8192) */
/* 0x2C */ __le32 s_mtime; /* Mount time */
/* 0x30 */ __le32 s_wtime; /* Write time */
/* 0x34 */ __le16 s_mnt_count; /* Mounts since last e2fsck */
/* 0x36 */ __le16 s_max_mnt_count; /* Max mounts before forced check */
/* 0x38 */ __le16 s_magic; /* Magic signature: 0xEF53 */
/* 0x3A */ __le16 s_state; /* Clean (0x0001) or Error (0x0002) */
/* 0x3C */ __le16 s_errors; /* Behavior on error */
/* 0x3E */ __le16 s_minor_rev_level;
/* 0x40 */ __le32 s_lastcheck; /* Timestamp of last e2fsck check */
/* 0x44 */ __le32 s_checkinterval; /* Max time between checks */
/* 0x48 */ __le32 s_creator_os; /* OS that formatted FS (0 = Linux) */
/* 0x4C */ __le32 s_rev_level; /* Revision level (0 = original, 1 = dynamic) */
/* 0x50 */ __le16 s_def_resuid; /* Default UID for reserved blocks (root = 0) */
/* 0x52 */ __le16 s_def_resgid; /* Default GID for reserved blocks */
/* 0x54 */ __le32 s_first_ino; /* First non-reserved inode (usually 11) */
/* 0x58 */ __le16 s_inode_size; /* Size of on-disk inode (256 bytes) */
/* 0x5A */ __le16 s_block_group_nr; /* Block group index of this superblock */
/* 0x5C */ __le32 s_feature_compat; /* Compatible feature flags */
/* 0x60 */ __le32 s_feature_incompat; /* Incompatible feature flags */
/* 0x64 */ __le32 s_feature_ro_compat; /* Read-only compatible flags */
/* 0x68 */ __u8 s_uuid[16]; /* 128-bit Filesystem UUID */
/* 0x78 */ char s_volume_name[16]; /* Volume label */
/* 0x88 */ char s_last_mounted[64]; /* Directory where last mounted */
/* 0xC8 */ __le32 s_algorithm_usage_bitmap;
/* ... 64-bit expansions, checksums, and crypto flags ... */
/* 0x3FC*/ __le32 s_checksum; /* CRC32c checksum of superblock */
};The Magic Signature (0xEF53)
At byte offset 0x38 inside the superblock resides the two-byte integer s_magic. For all filesystems in the ext family (ext2, ext3, ext4), this value must equal 0xEF53 (in little-endian on disk: bytes 53 EF). If this signature is missing or corrupted, the Linux kernel refuses to mount the volume.
# Inspect the raw magic number of an ext4 disk using dd and xxd
sudo dd if=/dev/nvme0n1p2 bs=1 skip=1080 count=2 2>/dev/null | xxd
# 00000000: 53efBlock Groups and Group Descriptors
To prevent severe fragmentation and minimize disk head seek times on physical spindles, ext4 divides the entire volume into equal-sized chunks called Block Groups.
A block group typically contains 32,768 blocks. With a \(4\text{ KiB}\) block size, each block group spans:
\[32,768 \times 4096\text{ bytes} = 134,217,728\text{ bytes} = 128\text{ MiB}\]
A \(1\text{ TiB}\) disk consists of approximately \(8,192\) block groups.
┌────────────────────────────────────────────────────────────────────────┐
│ STRUCTURE OF A STANDARD BLOCK GROUP │
│ │
│ ┌───────────────┬─────────────────┬───────────────┬──────────────┐ │
│ │ Superblock* │ Group Desc Table│ Block Bitmap │ Inode Bitmap │ │
│ │ (1 block) │ (N blocks) │ (1 block) │ (1 block) │ │
│ └───────────────┴─────────────────┴───────────────┴──────────────┘ │
│ ┌─────────────────────────────────┬──────────────────────────────┐ │
│ │ Inode Table │ Data Blocks │ │
│ │ (e.g. 512 blocks = 8192 inodes) │ (~32,250 blocks) │ │
│ └─────────────────────────────────┴──────────────────────────────┘ │
│ * Superblock and Group Descriptor backups exist only in selected groups│
│ when sparse_super is enabled. │
└────────────────────────────────────────────────────────────────────────┘
Block Group Descriptors (ext4_group_desc)
Immediately following the superblock resides the Block Group Descriptor Table (GDT). This table contains an entry for every single block group on the entire filesystem.
In classic 32-bit ext4, group descriptors were 32 bytes wide. With the modern 64bit feature enabled, descriptors expand to 64 bytes, splitting 64-bit block numbers into low and high 32-bit words:
struct ext4_group_desc {
__le32 bg_block_bitmap_lo; /* Lower 32 bits of block bitmap block */
__le32 bg_inode_bitmap_lo; /* Lower 32 bits of inode bitmap block */
__le32 bg_inode_table_lo; /* Lower 32 bits of inode table start block */
__le16 bg_free_blocks_count_lo; /* Free blocks count (lower 16 bits) */
__le16 bg_free_inodes_count_lo; /* Free inodes count (lower 16 bits) */
__le16 bg_used_dirs_count_lo; /* Number of directories in this group */
__le16 bg_flags; /* Group flags (e.g., INODE_UNINIT, BLOCK_UNINIT) */
__le32 bg_exclude_bitmap_lo; /* Snapshot exclusion bitmap */
__le16 bg_block_bitmap_csum_lo; /* CRC16/32c of block bitmap */
__le16 bg_inode_bitmap_csum_lo; /* CRC16/32c of inode bitmap */
__le16 bg_itable_unused_lo; /* Unused inodes count (lazy initialization) */
__le16 bg_checksum; /* CRC16/32c checksum of this descriptor */
/* --- 64-bit expansion fields (bytes 0x20 - 0x3F) --- */
__le32 bg_block_bitmap_hi; /* Upper 32 bits of block bitmap */
__le32 bg_inode_bitmap_hi; /* Upper 32 bits of inode bitmap */
__le32 bg_inode_table_hi; /* Upper 32 bits of inode table block */
__le16 bg_free_blocks_count_hi;
__le16 bg_free_inodes_count_hi;
__le16 bg_used_dirs_count_hi;
__le16 bg_itable_unused_hi;
__le32 bg_exclude_bitmap_hi;
__le16 bg_block_bitmap_csum_hi;
__le16 bg_inode_bitmap_csum_hi;
__u32 bg_reserved;
};Block and Inode Bitmaps
Each block group tracks its internal allocation states using two dedicated 1-block bitmaps:
- Block Bitmap: A single \(4\text{ KiB}\) block contains \(4096 \times 8 = 32,768\text{ bits}\). Each bit corresponds precisely to one physical block inside that block group.
0: Block is unallocated and free for writing.1: Block is allocated to a file, directory, or metadata structure.
- Inode Bitmap: Similarly, tracks whether each inode in the group’s inode table is free or allocated. A standard group with 8,192 inodes utilizes the first 8,192 bits (1024 bytes) of its inode bitmap block; the remaining bits in the bitmap block are padded with
1s.
Lazy Initialization (uninit_bg / flex_bg)
In early ext2/3 filesystems, mkfs was forced to zero out every block and inode bitmap across the entire hard drive before the filesystem could be mounted, taking hours on large disks. Modern ext4 includes uninitialized block groups: mkfs sets the EXT4_BG_INODE_UNINIT flag in bg_flags, allowing the kernel to skip reading or zeroing empty inode tables until files are actually created in that group.
The Inode Table Layout
The Inode Table is a contiguous array of on-disk inode structures located within the block group:
- In modern ext4, each inode is 256 bytes in size (expanded from ext2/3’s 128 bytes to accommodate nanosecond timestamps and inline extended attributes).
- A group containing \(8,192\text{ inodes}\) requires: \[8,192 \times 256\text{ bytes} = 2,097,152\text{ bytes} = 2\text{ MiB} = 512\text{ blocks of } 4\text{ KiB}\]
- Inode numbering is global and 1-indexed. Inode 1 is reserved (bad blocks); Inode 2 is the Root Directory (
/); Inode 8 is the Journal.
To locate on-disk Inode \(N\): \[\text{Group Index} = \frac{N - 1}{\text{s\_inodes\_per\_group}}\] \[\text{Table Offset} = (N - 1) \pmod{\text{s\_inodes\_per\_group}}\] \[\text{Physical Byte} = (\text{bg\_inode\_table} \times \text{block\_size}) + (\text{Table Offset} \times 256)\]
Superblock Redundancy and sparse_super
If every single block group stored a full backup copy of the superblock and the entire Group Descriptor Table, a multi-terabyte disk would waste gigabytes of storage duplicating static metadata.
Modern ext4 enables the sparse_super and sparse_super2 features: - In sparse_super, redundant backup copies of the superblock and GDT are placed only in Block Group 0, and groups that are powers of 3, 5, and 7 (Groups 1, 3, 5, 7, 9, 25, 27, 49, 81, 125, etc.). All other block groups begin immediately with their block bitmaps. - If Block Group 0 is corrupted, administrators can point repair tools directly to a known backup superblock: bash # Recover using backup superblock located in Block Group 32768 sudo e2fsck -b 32768 /dev/nvme0n1p2