| name | osdev-filesystems |
| description | Filesystem implementation for OS kernels. FAT12/16/32 driver, ext2 reader, and VFS abstraction layer. BPB parsing, cluster chains, inodes, directory entries, and file operations. Use when implementing filesystems, VFS, or block device file I/O. |
| origin | MCC |
Filesystem Implementation for OS Kernels
Complete reference for VFS design, FAT32 drivers, and ext2 readers in bare-metal kernels.
When to Use
- Designing a VFS abstraction layer for your kernel
- Writing a FAT12/16/32 filesystem driver
- Implementing an ext2 read-only driver
- Parsing BPB, cluster chains, or inode block pointers
- Handling directory entries and long filenames
- Debugging file read failures or corrupt directory listings
VFS Layer
Why VFS Exists
Without a VFS, every program that reads a file must know which filesystem the file lives on and call the right driver directly. The VFS provides a single set of operations (open, read, write, close, readdir) that work regardless of whether the underlying storage uses FAT, ext2, or anything else. This is the same pattern as device abstraction -- hide the hardware behind a uniform interface.
Key Abstractions
| Object | Purpose |
|---|
| Superblock | Represents a mounted filesystem instance. Holds fs-level metadata (block size, total blocks, root inode). Each mount creates one. |
| Inode (vnode) | Represents a file, directory, or symlink on disk. Contains metadata (size, permissions, timestamps) and pointers to data. One per unique file in the cache. |
| Dentry | Maps a filename component to an inode. Cached to avoid repeated directory lookups. Forms the path resolution tree. |
| File | An open file descriptor. Points to an inode plus per-open state (file position, access mode). Multiple files can reference one inode. |
Operations Structs
Each filesystem driver registers function pointers that the VFS calls:
struct super_operations {
struct inode *(*alloc_inode)(struct superblock *sb);
void (*destroy_inode)(struct inode *);
int (*read_inode)(struct inode *);
int (*write_inode)(struct inode *);
int (*statfs)(struct superblock *, struct statfs *);
};
struct inode_operations {
struct dentry *(*lookup)(struct inode *dir, const char *name);
int (*create)(struct inode *dir, const char *name, int mode);
int (*mkdir)(struct inode *dir, const char *name, int mode);
int (*unlink)(struct inode *dir, const char *name);
int (*readlink)(struct inode *, char *buf, size_t len);
};
struct file_operations {
ssize_t (*read)(struct file *, *buf, count, *offset);
(*write)( file *, *buf, count, *offset);
(*readdir)( file *, dirent *buf, count);
(*open)( inode *, file *);
(*close)( file *);
(*seek)( file *, offset, whence);
};
Why function pointers: Each filesystem has radically different on-disk layout. FAT uses cluster chains, ext2 uses inode block pointers, ISO9660 uses extents. The VFS does not care -- it calls inode->ops->lookup() and the filesystem driver translates that into whatever disk reads are needed.
Mount Mechanism
- User calls
mount("/dev/sda1", "/mnt", "fat32")
- VFS looks up the filesystem type in a registry of registered drivers
- VFS calls the driver's
read_super() to parse the superblock from the device
- VFS creates a mount entry linking the path
/mnt to the new superblock
- Path resolution at
/mnt/... now dispatches to the FAT32 driver
Path Resolution
Split the path into components. For each component, call lookup() on the current directory inode. If a component is a mount point, switch to that mount's root inode. Continue until the final component is reached or an error occurs.
See references/vfs-design.md for the full VFS architecture, operation interfaces, and path resolution algorithm.
FAT32 Overview
FAT is a singly-linked list of clusters stored in a giant table. It is simple but that simplicity makes it an excellent first filesystem to implement.
BPB (BIOS Parameter Block)
The BPB lives in the first sector (the boot record) and describes the disk layout. Key fields:
| Offset | Size | Field | Purpose |
|---|
| 0x00 | 3 | Jump code | EB xx 90 -- jump over BPB data |
| 0x0B | 2 | bytes_per_sector | Almost always 512 |
| 0x0D | 1 | sectors_per_cluster | Power of 2: 1, 2, 4, 8, ... 128 |
| 0x0E | 2 | reserved_sectors | Sectors before the first FAT (includes boot sector) |
| 0x10 | 1 | num_fats | Number of FAT copies (usually 2) |
| 0x11 | 2 | root_entry_count | 0 for FAT32 (root dir is a cluster chain) |
| 0x13 | 2 | total_sectors_16 | 0 for FAT32, use total_sectors_32 instead |
| 0x16 | 2 | fat_size_16 | 0 for FAT32, use fat_size_32 instead |
| 0x20 | 4 | total_sectors_32 | Total sector count for FAT32 |
| 0x24 | 4 | fat_size_32 | Sectors per FAT (FAT32 only) |
| 0x2C | 4 | root_cluster | Starting cluster of root directory (FAT32) |
FAT Table
The File Allocation Table is an array of 32-bit entries (only low 28 bits used). Each entry corresponds to a cluster on disk. The entry value tells you the next cluster in the chain:
| Value | Meaning |
|---|
0x00000000 | Free cluster |
0x00000002 - 0x0FFFFFEF | Next cluster in chain |
0x0FFFFFF8 - 0x0FFFFFFF | End of chain (EOF) |
0x0FFFFFF7 | Bad cluster |
Cluster chain example: File starts at cluster 5. FAT[5] = 6, FAT[6] = 9, FAT[9] = 0x0FFFFFFF. The file occupies clusters 5, 6, 9.
Sector Calculation
uint32_t first_fat_sector = reserved_sectors;
uint32_t first_data_sector = reserved_sectors + (num_fats * fat_size_32);
uint32_t first_sector_of_cluster(uint32_t cluster) {
return first_data_sector + (cluster - 2) * sectors_per_cluster;
}
Why cluster - 2: Clusters 0 and 1 are reserved in the FAT table (entry 0 holds the media type, entry 1 is reserved). The first actual data cluster is cluster 2, which maps to the first sector of the data region.
Directory Entries (32 bytes each)
| Offset | Size | Field |
|---|
| 0x00 | 8 | Filename (space-padded, no dot) |
| 0x08 | 3 | Extension (space-padded) |
| 0x0B | 1 | Attributes (0x01=RO, 0x02=Hidden, 0x04=System, 0x08=VolumeID, 0x10=Dir, 0x20=Archive) |
| 0x14 | 2 | First cluster high 16 bits (FAT32) |
| 0x1A | 2 | First cluster low 16 bits |
| 0x1C | 4 | File size in bytes |
Entry 0x00 byte meaning: 0x00 = end of directory, 0xE5 = deleted entry, anything else = first char of filename.
Long File Names (LFN)
LFN entries use attribute byte 0x0F (Volume + System + Hidden + ReadOnly) as a marker. They are stored in reverse order immediately before the standard 8.3 entry. Each LFN entry holds 13 UCS-2 characters across three fragmented fields (offsets 0x01, 0x0E, 0x1C).
See references/fat32-layout.md for complete BPB struct, FAT entry format, directory entry struct, LFN layout, and file read implementation.
ext2 Overview
ext2 uses fixed-size inodes that point to data blocks, organized into block groups for locality. It is more complex than FAT but supports UNIX permissions, hard links, and efficient large file access.
Superblock
Always at byte offset 1024 from the start of the partition. Always 1024 bytes long. Key fields:
| Offset | Size | Field |
|---|
| 0 | 4 | s_inodes_count -- total inodes |
| 4 | 4 | s_blocks_count -- total blocks |
| 24 | 4 | s_log_block_size -- block size = 1024 << this value |
| 32 | 4 | s_blocks_per_group |
| 40 | 4 | s_inodes_per_group |
| 56 | 2 | s_magic -- must be 0xEF53 |
| 76 | 2 | s_inode_size -- size of each inode struct (128 for rev 0, typically 256 for rev 1+) |
Block size: 1024 << s_log_block_size. Common values: 0 = 1KB, 1 = 2KB, 2 = 4KB.
Block Groups
The partition is divided into block groups. Each group contains:
- Superblock backup (in groups 0, 1, and powers of 3, 5, 7 if sparse_super feature)
- Block Group Descriptor Table (immediately after superblock, describes all groups)
- Block bitmap (1 block, tracks which blocks in this group are allocated)
- Inode bitmap (1 block, tracks which inodes in this group are allocated)
- Inode table (multiple blocks, holds the actual inode structs)
- Data blocks (the rest of the group)
Block Group Descriptor (32 bytes per group):
| Offset | Size | Field |
|---|
| 0 | 4 | bg_block_bitmap -- block ID of block bitmap |
| 4 | 4 | bg_inode_bitmap -- block ID of inode bitmap |
| 8 | 4 | bg_inode_table -- starting block ID of inode table |
Inode Structure
128 bytes minimum (may be larger). Key fields:
| Offset | Size | Field |
|---|
| 0 | 2 | i_mode -- type (file/dir/symlink) + permissions |
| 4 | 4 | i_size -- file size in bytes (low 32 bits) |
| 28 | 4 | i_blocks -- count of 512-byte blocks allocated |
| 40 | 60 | i_block[15] -- 15 x 4-byte block pointers |
Block pointer scheme (i_block[0..14]):
i_block[0..11]: 12 direct block pointers (point straight to data blocks)
i_block[12]: 1 indirect block pointer (points to a block full of direct pointers)
i_block[13]: 1 double indirect (pointer -> indirect blocks -> data blocks)
i_block[14]: 1 triple indirect (pointer -> double indirect -> indirect -> data)
Why this scheme: 12 direct pointers handle small files (up to 48KB with 4KB blocks) with zero indirection cost. The indirect levels scale to terabytes while keeping the inode struct small and fixed-size.
Directory Entries
Directories are files whose data blocks contain a linked list of variable-length entries:
| Offset | Size | Field |
|---|
| 0 | 4 | inode -- inode number of this entry |
| 4 | 2 | rec_len -- total size of this entry (used to find next entry) |
| 6 | 1 | name_len -- length of the name |
| 7 | 1 | file_type -- 1=file, 2=dir, 7=symlink |
| 8 | N | name -- filename (not null-terminated, use name_len) |
rec_len gotcha: rec_len includes padding to align the next entry. The last entry in a block has rec_len extending to the end of the block. When an entry is deleted, its rec_len is added to the previous entry's rec_len.
Root directory: Always inode 2.
See references/ext2-layout.md for complete superblock fields, block group descriptor, inode struct, indirect block resolution, and reading a file by path.
Reading a File (FAT32 Example)
Step-by-step walkthrough of reading /BOOT/KERNEL.BIN:
- Parse BPB: Read sector 0, extract bytes_per_sector, sectors_per_cluster, reserved_sectors, num_fats, fat_size_32, root_cluster
- Calculate layout:
fat_start = reserved_sectors;
data_start = reserved_sectors + (num_fats * fat_size_32);
- Read root directory: Read clusters starting at root_cluster. Scan 32-byte directory entries for name
"BOOT " with attribute 0x10 (directory)
- Enter subdirectory: Extract the starting cluster from the matched entry (high word at offset 0x14, low word at offset 0x1A). Read that cluster chain and scan entries for
"KERNEL BIN"
- Follow cluster chain: Extract the file's starting cluster. Read FAT entries to build the full chain: cluster -> FAT[cluster] -> FAT[FAT[cluster]] -> ... until EOF marker
- Read data: For each cluster in the chain, calculate
sector = data_start + (cluster - 2) * sectors_per_cluster, read sectors_per_cluster sectors, copy to output buffer. Stop when total bytes copied equals the file size from the directory entry.
Common Pitfalls
| Pitfall | Consequence | Fix |
|---|
| Forgetting cluster - 2 offset | Reading wrong sectors, corrupt data | Always subtract 2 when converting cluster to sector |
| Not masking FAT32 entries to 28 bits | Top 4 reserved bits corrupt cluster numbers | entry & 0x0FFFFFFF before interpreting |
| Treating FAT12 entries like FAT16/32 | FAT12 uses 12-bit entries packed across byte boundaries | Handle the 1.5-byte alignment correctly |
| Hardcoding 512-byte sectors | Fails on disks with 1024/2048/4096 byte sectors | Always use bytes_per_sector from BPB |
| Reading ext2 superblock at offset 0 | Gets garbage (boot code or empty space) | Superblock is at byte 1024, always |
| Wrong inode size assumption | Inode table reads are misaligned | Read s_inode_size from superblock (not always 128) |
| Ignoring rec_len in ext2 directories | Name fields run together, parsing breaks | Advance by rec_len, not by name_len + 8 |
| Not handling LFN entries in FAT | Only 8.3 names visible, breaks user expectations | Check attribute 0x0F and reconstruct long names |
| Caching entire FAT in memory for large disks | Huge memory cost on FAT32 (up to 128MB table) | Cache FAT sectors on demand, not the whole table |
Related Skills
osdev-boot -- bootloader reads the first filesystem sectors to load the kernel
osdev-memory -- page allocator provides memory for filesystem caches and buffers
osdev-interrupts -- disk controller interrupts signal I/O completion