We use cookies to enhance your browsing experience, serve personalized content, and analyze our traffic.
By clicking "Accept All", you consent to our use of cookies. See our
Privacy Policy
for more information.
Phase 7 Goals: By the end of this phase, your kernel will read and write files from disk. You'll have an ATA driver for disk access, FAT filesystem support, and a VFS layer that abstracts filesystem differences.
Your kernel can allocate memory—but when you reboot, everything is gone. Persistent storage is what makes computers useful: saving documents, loading programs, keeping user data between sessions.
Key Insight: A filesystem transforms raw disk sectors into a hierarchy of named files and directories. The VFS layer lets your kernel support multiple filesystem types through a unified interface.
Storage Architecture
Storage devices communicate in blocks (usually 512 bytes). You can't read or write a single byte from disk—you read an entire sector. This is why we need:
Block device drivers: Talk to hardware (ATA, SATA, NVMe)
Block cache: Keep frequently-used sectors in RAM
Filesystems: Organize blocks into files and directories
Old hard drives used CHS (Cylinder-Head-Sector) addressing—specify physical location on spinning platters. Modern drives use LBA (Logical Block Addressing)—just a sector number from 0 to N. The drive controller handles the physical mapping.
Historical Context
ATA Disk Driver
ATA/IDE Overview
ATA (AT Attachment), also called IDE, is the classic interface for hard drives. Modern systems use SATA or NVMe, but ATA is simple to program and still supported in emulators—perfect for learning.
ATA/IDE controller architecture showing primary and secondary channels with master/slave drive configuration and I/O port assignments
PIO (Programmed I/O) mode is the simplest way to transfer data: the CPU reads/writes each word directly. It's slow but requires no DMA setup—ideal for bootloaders and simple kernels.
/* ata.c - ATA PIO read sector (complete implementation) */
#include "io.h"
#include "ata.h"
/**
* Wait for drive to be ready (BSY clear)
*/
static void ata_wait_bsy(uint16_t io_base) {
while (inb(io_base + 7) & ATA_SR_BSY);
}
/**
* Wait for data to be ready (DRQ set)
*/
static void ata_wait_drq(uint16_t io_base) {
while (!(inb(io_base + 7) & ATA_SR_DRQ));
}
/**
* Check for drive errors
* @return 0 if no error, error code otherwise
*/
static int ata_check_error(uint16_t io_base) {
uint8_t status = inb(io_base + 7);
if (status & ATA_SR_ERR) {
return inb(io_base + 1); /* Return error register */
}
if (status & ATA_SR_DF) {
return -1; /* Drive fault */
}
return 0;
}
/**
* Read a single sector using PIO mode
* @param lba Logical block address (28-bit)
* @param buffer 512-byte buffer for data
*/
void ata_read_sector(uint32_t lba, uint8_t* buffer) {
uint16_t io_base = ATA_PRIMARY_IO;
/* Wait for drive not busy */
ata_wait_bsy(io_base);
/* Select drive (master) and set LBA mode with bits 24-27 */
outb(io_base + 6, ATA_MODE_LBA | ATA_DRIVE_MASTER |
((lba >> 24) & 0x0F));
/* Set sector count = 1 */
outb(io_base + 2, 1);
/* Set LBA address (bits 0-23) */
outb(io_base + 3, lba & 0xFF); /* LBA low */
outb(io_base + 4, (lba >> 8) & 0xFF); /* LBA mid */
outb(io_base + 5, (lba >> 16) & 0xFF); /* LBA high */
/* Send READ SECTORS command */
outb(io_base + 7, ATA_CMD_READ_PIO);
/* Wait for data ready */
ata_wait_drq(io_base);
/* Check for errors */
if (ata_check_error(io_base)) {
kprintf("ATA: Read error at LBA %d\n", lba);
return;
}
/* Read 256 words (512 bytes) */
for (int i = 0; i < 256; i++) {
uint16_t data = inw(io_base); /* Read 16 bits */
buffer[i * 2] = data & 0xFF; /* Low byte */
buffer[i * 2 + 1] = (data >> 8); /* High byte */
}
}
Read/Write Operations
For writing, the process is similar but we send data to the drive:
/**
* Write a single sector using PIO mode
* @param lba Logical block address (28-bit)
* @param buffer 512-byte buffer of data to write
*/
void ata_write_sector(uint32_t lba, const uint8_t* buffer) {
uint16_t io_base = ATA_PRIMARY_IO;
/* Wait for drive not busy */
ata_wait_bsy(io_base);
/* Select drive and set LBA mode */
outb(io_base + 6, ATA_MODE_LBA | ATA_DRIVE_MASTER |
((lba >> 24) & 0x0F));
/* Set sector count = 1 */
outb(io_base + 2, 1);
/* Set LBA address */
outb(io_base + 3, lba & 0xFF);
outb(io_base + 4, (lba >> 8) & 0xFF);
outb(io_base + 5, (lba >> 16) & 0xFF);
/* Send WRITE SECTORS command */
outb(io_base + 7, ATA_CMD_WRITE_PIO);
/* Wait for drive ready to receive data */
ata_wait_drq(io_base);
/* Write 256 words (512 bytes) */
for (int i = 0; i < 256; i++) {
uint16_t data = buffer[i * 2] | (buffer[i * 2 + 1] << 8);
outw(io_base, data);
}
/* Flush cache to ensure data is written */
outb(io_base + 7, ATA_CMD_CACHE_FLUSH);
ata_wait_bsy(io_base);
}
/**
* Identify drive - get drive information
* @param buffer 512-byte buffer for IDENTIFY data
* @return 0 on success, -1 if no drive
*/
int ata_identify(uint8_t* buffer) {
uint16_t io_base = ATA_PRIMARY_IO;
/* Select master drive */
outb(io_base + 6, ATA_MODE_LBA | ATA_DRIVE_MASTER);
/* Zero out sector count and LBA registers */
outb(io_base + 2, 0);
outb(io_base + 3, 0);
outb(io_base + 4, 0);
outb(io_base + 5, 0);
/* Send IDENTIFY command */
outb(io_base + 7, ATA_CMD_IDENTIFY);
/* Check if drive exists */
if (inb(io_base + 7) == 0) {
return -1; /* No drive */
}
/* Wait for BSY to clear */
ata_wait_bsy(io_base);
/* Check for ATAPI or SATA by examining LBA mid/high */
if (inb(io_base + 4) != 0 || inb(io_base + 5) != 0) {
return -1; /* Not ATA drive */
}
/* Wait for DRQ or error */
while (1) {
uint8_t status = inb(io_base + 7);
if (status & ATA_SR_ERR) return -1;
if (status & ATA_SR_DRQ) break;
}
/* Read IDENTIFY data */
for (int i = 0; i < 256; i++) {
uint16_t data = inw(io_base);
buffer[i * 2] = data & 0xFF;
buffer[i * 2 + 1] = (data >> 8);
}
return 0;
}
/**
* Initialize ATA driver and detect drives
*/
void ata_init(void) {
uint8_t identify[512];
kprintf("ATA: Detecting drives...\n");
if (ata_identify(identify) == 0) {
/* Extract drive info from IDENTIFY data */
uint32_t sectors = *(uint32_t*)&identify[120]; /* Total LBA28 sectors */
uint32_t size_mb = sectors / 2048; /* 512 bytes × 2048 = 1 MB */
/* Model string is at offset 54, 40 bytes, swapped */
char model[41];
for (int i = 0; i < 40; i += 2) {
model[i] = identify[54 + i + 1];
model[i + 1] = identify[54 + i];
}
model[40] = '\0';
kprintf("ATA: Primary Master: %s (%d MB)\n", model, size_mb);
} else {
kprintf("ATA: No drive detected on Primary Master\n");
}
}
Warning: Real disk drivers should use DMA (Direct Memory Access) for performance—PIO ties up the CPU for every byte. Also implement proper error handling, timeouts, and interrupt-driven I/O for production use.
Partition Tables
Before filesystems, we need to understand how disks are divided. Partition tables split a physical disk into logical sections, each with its own filesystem.
Master Boot Record layout in sector 0 containing 446 bytes of bootloader code, four 16-byte partition entries, and the 0x55AA boot signature
MBR Partitions
The Master Boot Record (MBR) partitioning scheme dates back to IBM PC DOS. It lives in sector 0 and supports up to 4 primary partitions (or 3 primary + 1 extended with logical partitions inside).
GPT (GUID Partition Table) is the modern replacement for MBR, required for UEFI boot and disks > 2 TB. It uses GUIDs (128-bit identifiers) for partition types.
FAT (File Allocation Table) is the most widely-supported filesystem. Designed by Microsoft in 1977, it's simple enough to implement in a weekend yet used everywhere: USB drives, SD cards, UEFI boot partitions.
FAT filesystem disk layout with boot sector (BPB), dual File Allocation Tables, root directory region, and cluster-based data area
FAT FILESYSTEM LAYOUT
═══════════════════════════════════════════════════════════════
┌──────────────────────────────────────────────────────────────┐
│ BOOT SECTOR (1 sector) │
│ BPB (BIOS Parameter Block) + Boot code │
├──────────────────────────────────────────────────────────────┤
│ RESERVED SECTORS │
│ (Usually 1 for FAT12/16, 32 for FAT32) │
├──────────────────────────────────────────────────────────────┤
│ FAT #1 (File Allocation Table) │
│ Linked list of clusters - one entry per cluster │
├──────────────────────────────────────────────────────────────┤
│ FAT #2 (Backup copy) │
│ Usually identical to FAT #1 │
├──────────────────────────────────────────────────────────────┤
│ ROOT DIRECTORY (FAT12/16 only) │
│ Fixed size at fixed location │
├──────────────────────────────────────────────────────────────┤
│ DATA AREA │
│ Clusters containing files and directories │
│ (FAT32 root directory is here too) │
└──────────────────────────────────────────────────────────────┘
FAT VARIANTS:
┌─────────┬───────────────┬─────────────┬───────────────────┐
│ Variant │ Cluster Entry │ Max Size │ Use Case │
├─────────┼───────────────┼─────────────┼───────────────────┤
│ FAT12 │ 12 bits │ 16 MB │ Floppy disks │
│ FAT16 │ 16 bits │ 2 GB │ Small drives │
│ FAT32 │ 28 bits* │ 2 TB │ USB, SD cards │
└─────────┴───────────────┴─────────────┴───────────────────┘
* FAT32 uses only 28 bits; top 4 bits reserved
Boot Sector
The BIOS Parameter Block (BPB) in sector 0 describes the filesystem geometry:
/* fat.h - FAT16/32 structures */
/* FAT16 Boot Sector (BIOS Parameter Block) */
typedef struct {
uint8_t jmp[3]; /* Jump instruction to boot code */
uint8_t oem_name[8]; /* OEM identifier */
uint16_t bytes_per_sector; /* Usually 512 */
uint8_t sectors_per_cluster; /* 1, 2, 4, 8, 16, 32, 64, or 128 */
uint16_t reserved_sectors; /* Sectors before first FAT */
uint8_t fat_count; /* Number of FATs (usually 2) */
uint16_t root_entry_count; /* FAT12/16: max root entries */
uint16_t total_sectors_16; /* Total sectors (if < 65536) */
uint8_t media_type; /* Media descriptor (0xF8 = HDD) */
uint16_t fat_size_16; /* Sectors per FAT (FAT12/16) */
uint16_t sectors_per_track; /* For CHS geometry */
uint16_t head_count; /* For CHS geometry */
uint32_t hidden_sectors; /* Sectors before partition */
uint32_t total_sectors_32; /* Total sectors (if >= 65536) */
/* Extended Boot Record (FAT16) */
uint8_t drive_number; /* BIOS drive number */
uint8_t reserved;
uint8_t boot_signature; /* 0x29 if following fields valid */
uint32_t volume_id; /* Volume serial number */
uint8_t volume_label[11]; /* Volume name */
uint8_t fs_type[8]; /* "FAT16 " */
} __attribute__((packed)) fat16_boot_t;
/* FAT32 Extended Boot Record */
typedef struct {
/* First 36 bytes same as FAT16 */
uint8_t jmp[3];
uint8_t oem_name[8];
uint16_t bytes_per_sector;
uint8_t sectors_per_cluster;
uint16_t reserved_sectors;
uint8_t fat_count;
uint16_t root_entry_count; /* Must be 0 for FAT32 */
uint16_t total_sectors_16;
uint8_t media_type;
uint16_t fat_size_16; /* Must be 0 for FAT32 */
uint16_t sectors_per_track;
uint16_t head_count;
uint32_t hidden_sectors;
uint32_t total_sectors_32;
/* FAT32 specific */
uint32_t fat_size_32; /* Sectors per FAT (FAT32) */
uint16_t ext_flags;
uint16_t fs_version;
uint32_t root_cluster; /* First cluster of root dir */
uint16_t fs_info; /* FSInfo sector number */
uint16_t backup_boot; /* Backup boot sector */
uint8_t reserved[12];
uint8_t drive_number;
uint8_t reserved2;
uint8_t boot_signature;
uint32_t volume_id;
uint8_t volume_label[11];
uint8_t fs_type[8]; /* "FAT32 " */
} __attribute__((packed)) fat32_boot_t;
FAT Table
The File Allocation Table is essentially a linked list of clusters. Each entry either points to the next cluster in a chain, or marks end-of-file, bad cluster, etc.
/* fat.c - FAT table operations */
/**
* FAT driver context
*/
typedef struct {
block_device_t* device;
fat32_boot_t bpb;
uint32_t fat_start_lba; /* LBA of first FAT */
uint32_t data_start_lba; /* LBA of first data cluster */
uint32_t root_dir_lba; /* LBA of root directory */
uint32_t total_clusters;
uint8_t fat_type; /* 12, 16, or 32 */
} fat_fs_t;
/**
* Read a FAT entry (get next cluster in chain)
*/
uint32_t fat_get_entry(fat_fs_t* fs, uint32_t cluster) {
uint32_t fat_offset;
uint32_t fat_sector;
uint32_t entry_offset;
uint8_t sector[512];
if (fs->fat_type == 32) {
fat_offset = cluster * 4;
} else if (fs->fat_type == 16) {
fat_offset = cluster * 2;
} else { /* FAT12 */
fat_offset = cluster + (cluster / 2); /* cluster * 1.5 */
}
fat_sector = fs->fat_start_lba + (fat_offset / 512);
entry_offset = fat_offset % 512;
/* Read FAT sector */
fs->device->read(fs->device, fat_sector, 1, sector);
if (fs->fat_type == 32) {
return (*(uint32_t*)§or[entry_offset]) & 0x0FFFFFFF;
} else if (fs->fat_type == 16) {
return *(uint16_t*)§or[entry_offset];
} else { /* FAT12 */
uint16_t val = *(uint16_t*)§or[entry_offset];
if (cluster & 1) {
return val >> 4; /* Odd cluster: high 12 bits */
} else {
return val & 0x0FFF; /* Even cluster: low 12 bits */
}
}
}
/**
* Check if cluster marks end of chain
*/
int fat_is_eof(fat_fs_t* fs, uint32_t cluster) {
if (fs->fat_type == 32) return cluster >= 0x0FFFFFF8;
if (fs->fat_type == 16) return cluster >= 0xFFF8;
return cluster >= 0xFF8; /* FAT12 */
}
/**
* Convert cluster number to LBA
*/
uint32_t fat_cluster_to_lba(fat_fs_t* fs, uint32_t cluster) {
return fs->data_start_lba +
(cluster - 2) * fs->bpb.sectors_per_cluster;
}
Directory Entries
Directories are files containing 32-byte entries. Each entry describes a file or subdirectory.
/* FAT Directory Entry (32 bytes) */
typedef struct {
uint8_t name[8]; /* 8.3 filename (space padded) */
uint8_t ext[3]; /* Extension */
uint8_t attr; /* Attributes */
uint8_t reserved; /* Windows NT: lowercase flags */
uint8_t create_time_ms; /* Creation time (ms) */
uint16_t create_time; /* Creation time */
uint16_t create_date; /* Creation date */
uint16_t access_date; /* Last access date */
uint16_t cluster_high; /* High 16 bits of cluster (FAT32) */
uint16_t modify_time; /* Last modification time */
uint16_t modify_date; /* Last modification date */
uint16_t cluster_low; /* Low 16 bits of starting cluster */
uint32_t file_size; /* File size in bytes */
} __attribute__((packed)) fat_dir_entry_t;
/* Attribute flags */
#define FAT_ATTR_READ_ONLY 0x01
#define FAT_ATTR_HIDDEN 0x02
#define FAT_ATTR_SYSTEM 0x04
#define FAT_ATTR_VOLUME_ID 0x08 /* Volume label entry */
#define FAT_ATTR_DIRECTORY 0x10
#define FAT_ATTR_ARCHIVE 0x20
#define FAT_ATTR_LFN 0x0F /* Long filename entry */
/* Special first-byte values */
#define FAT_ENTRY_FREE 0xE5 /* Deleted entry */
#define FAT_ENTRY_END 0x00 /* End of directory */
#define FAT_ENTRY_KANJI 0x05 /* Actually 0xE5 (Kanji) */
/**
* Get starting cluster from directory entry
*/
uint32_t fat_entry_cluster(fat_dir_entry_t* entry) {
return ((uint32_t)entry->cluster_high << 16) | entry->cluster_low;
}
/**
* Parse 8.3 filename from directory entry
*/
void fat_parse_filename(fat_dir_entry_t* entry, char* out) {
int i, j = 0;
/* Copy name (remove trailing spaces) */
for (i = 0; i < 8 && entry->name[i] != ' '; i++) {
out[j++] = entry->name[i];
}
/* Add dot and extension if present */
if (entry->ext[0] != ' ') {
out[j++] = '.';
for (i = 0; i < 3 && entry->ext[i] != ' '; i++) {
out[j++] = entry->ext[i];
}
}
out[j] = '\0';
}
File Operations
/**
* List directory contents
*/
void fat_list_directory(fat_fs_t* fs, uint32_t cluster) {
uint8_t sector[512];
fat_dir_entry_t* entries;
char filename[13];
uint32_t lba = fat_cluster_to_lba(fs, cluster);
/* Read directory cluster */
for (int s = 0; s < fs->bpb.sectors_per_cluster; s++) {
fs->device->read(fs->device, lba + s, 1, sector);
entries = (fat_dir_entry_t*)sector;
for (int i = 0; i < 16; i++) { /* 16 entries per sector */
if (entries[i].name[0] == FAT_ENTRY_END) {
return; /* End of directory */
}
if (entries[i].name[0] == FAT_ENTRY_FREE) {
continue; /* Deleted entry */
}
if (entries[i].attr == FAT_ATTR_LFN) {
continue; /* Long filename (skip for now) */
}
if (entries[i].attr & FAT_ATTR_VOLUME_ID) {
continue; /* Volume label */
}
fat_parse_filename(&entries[i], filename);
if (entries[i].attr & FAT_ATTR_DIRECTORY) {
kprintf(" <DIR> %s\n", filename);
} else {
kprintf(" %8d %s\n", entries[i].file_size, filename);
}
}
}
/* Follow cluster chain for larger directories */
uint32_t next = fat_get_entry(fs, cluster);
if (!fat_is_eof(fs, next)) {
fat_list_directory(fs, next);
}
}
/**
* Find file in directory by name
* @return Starting cluster, or 0 if not found
*/
uint32_t fat_find_file(fat_fs_t* fs, uint32_t dir_cluster,
const char* name) {
uint8_t sector[512];
fat_dir_entry_t* entries;
char filename[13];
uint32_t cluster = dir_cluster;
while (!fat_is_eof(fs, cluster)) {
uint32_t lba = fat_cluster_to_lba(fs, cluster);
for (int s = 0; s < fs->bpb.sectors_per_cluster; s++) {
fs->device->read(fs->device, lba + s, 1, sector);
entries = (fat_dir_entry_t*)sector;
for (int i = 0; i < 16; i++) {
if (entries[i].name[0] == FAT_ENTRY_END) {
return 0; /* Not found */
}
if (entries[i].name[0] == FAT_ENTRY_FREE) {
continue;
}
if (entries[i].attr == FAT_ATTR_LFN) {
continue;
}
fat_parse_filename(&entries[i], filename);
/* Case-insensitive compare */
if (strcasecmp(filename, name) == 0) {
return fat_entry_cluster(&entries[i]);
}
}
}
cluster = fat_get_entry(fs, cluster);
}
return 0; /* Not found */
}
/**
* Read file contents into buffer
*/
int fat_read_file(fat_fs_t* fs, uint32_t cluster,
uint8_t* buffer, uint32_t size) {
uint32_t bytes_read = 0;
uint32_t cluster_size = fs->bpb.sectors_per_cluster * 512;
while (!fat_is_eof(fs, cluster) && bytes_read < size) {
uint32_t lba = fat_cluster_to_lba(fs, cluster);
uint32_t to_read = (size - bytes_read < cluster_size) ?
(size - bytes_read) : cluster_size;
/* Read cluster (may be multiple sectors) */
for (int s = 0; s < fs->bpb.sectors_per_cluster; s++) {
fs->device->read(fs->device, lba + s, 1,
buffer + bytes_read + s * 512);
}
bytes_read += cluster_size;
cluster = fat_get_entry(fs, cluster);
}
return bytes_read;
}
Long Filenames (LFN): FAT supports names longer than 8.3 using special directory entries with attribute 0x0F. These "VFAT" entries store up to 13 UTF-16 characters each, placed before the short name entry. Implementing LFN support is a good exercise!
Virtual File System (VFS)
VFS Design
The Virtual File System provides a common interface for all filesystems. Programs use the same open(), read(), write() calls whether accessing FAT, ext2, or network files.
VFS abstraction layer providing a unified open/read/write/close interface across multiple filesystem implementations
/* vfs.c - VFS implementation */
#include "vfs.h"
#include <string.h>
vfs_node_t* vfs_root = NULL;
/**
* Read from a file
*/
uint32_t vfs_read(vfs_node_t* node, uint32_t offset,
uint32_t size, uint8_t* buffer) {
if (node && node->read) {
return node->read(node, offset, size, buffer);
}
return 0;
}
/**
* Write to a file
*/
uint32_t vfs_write(vfs_node_t* node, uint32_t offset,
uint32_t size, const uint8_t* buffer) {
if (node && node->write) {
return node->write(node, offset, size, buffer);
}
return 0;
}
/**
* Open a file (increment refcount, etc.)
*/
void vfs_open(vfs_node_t* node) {
if (node && node->open) {
node->open(node);
}
}
/**
* Close a file
*/
void vfs_close(vfs_node_t* node) {
if (node && node->close) {
node->close(node);
}
}
/**
* Read directory entry at index
*/
dirent_t* vfs_readdir(vfs_node_t* node, uint32_t index) {
/* Check this is actually a directory */
if (node && (node->flags & 0x7) == VFS_DIRECTORY && node->readdir) {
return node->readdir(node, index);
}
return NULL;
}
/**
* Find file in directory by name
*/
vfs_node_t* vfs_finddir(vfs_node_t* node, const char* name) {
if (node && (node->flags & 0x7) == VFS_DIRECTORY && node->finddir) {
return node->finddir(node, name);
}
return NULL;
}
/**
* Resolve a path to a VFS node
* e.g., "/home/user/file.txt" → vfs_node_t*
*/
vfs_node_t* vfs_namei(const char* path) {
if (!path || path[0] != '/') {
return NULL; /* Must be absolute path */
}
vfs_node_t* node = vfs_root;
char component[VFS_NAME_MAX];
int i = 1; /* Skip leading slash */
while (path[i]) {
/* Extract path component */
int j = 0;
while (path[i] && path[i] != '/') {
component[j++] = path[i++];
}
component[j] = '\0';
if (j == 0) {
i++; /* Skip consecutive slashes */
continue;
}
/* Handle . and .. */
if (strcmp(component, ".") == 0) {
if (path[i]) i++;
continue;
}
/* TODO: handle ".." by tracking parent */
/* Look up component in current directory */
node = vfs_finddir(node, component);
if (!node) {
return NULL; /* Not found */
}
/* Follow mount points */
if (node->flags & VFS_MOUNTPOINT) {
node = node->ptr;
}
if (path[i]) i++; /* Skip slash */
}
return node;
}
Mounting
Mounting attaches a filesystem to a directory in the tree. The mount point becomes the root of the mounted filesystem.
/* Mount table */
typedef struct mount_point {
char path[VFS_PATH_MAX];
vfs_node_t* node; /* Covering node (mount point) */
vfs_node_t* root; /* Root of mounted filesystem */
struct mount_point* next;
} mount_point_t;
static mount_point_t* mount_table = NULL;
/**
* Mount a filesystem
* @param path Mount point (must exist as directory)
* @param fs_root Root node of filesystem to mount
*/
int vfs_mount(const char* path, vfs_node_t* fs_root) {
/* Find mount point directory */
vfs_node_t* mount_node = vfs_namei(path);
if (!mount_node || !(mount_node->flags & VFS_DIRECTORY)) {
return -1; /* Mount point must be existing directory */
}
/* Create mount entry */
mount_point_t* mp = kmalloc(sizeof(mount_point_t));
strcpy(mp->path, path);
mp->node = mount_node;
mp->root = fs_root;
mp->next = mount_table;
mount_table = mp;
/* Mark node as mount point */
mount_node->flags |= VFS_MOUNTPOINT;
mount_node->ptr = fs_root;
kprintf("VFS: Mounted filesystem at %s\n", path);
return 0;
}
/**
* Unmount a filesystem
*/
int vfs_unmount(const char* path) {
mount_point_t** pp = &mount_table;
while (*pp) {
if (strcmp((*pp)->path, path) == 0) {
mount_point_t* mp = *pp;
/* Clear mount point flag */
mp->node->flags &= ~VFS_MOUNTPOINT;
mp->node->ptr = NULL;
/* Remove from list */
*pp = mp->next;
kfree(mp);
kprintf("VFS: Unmounted %s\n", path);
return 0;
}
pp = &(*pp)->next;
}
return -1; /* Not mounted */
}
Example: Initializing the Filesystem
void fs_init(void) {
/* Initialize ATA driver */
ata_init();
/* Initialize VFS with a simple root */
vfs_root = initrd_init(); /* Initial ramdisk as root */
/* Parse MBR on first ATA drive */
mbr_parse(&ata_device);
/* Mount FAT partition */
fat_fs_t* fat = fat_init(&ata_device, partition_start);
if (fat) {
vfs_mount("/mnt/disk", fat->root);
}
/* Now we can access files! */
vfs_node_t* file = vfs_namei("/mnt/disk/README.TXT");
if (file) {
char buffer[512];
uint32_t bytes = vfs_read(file, 0, 511, buffer);
buffer[bytes] = '\0';
kprintf("File contents:\n%s\n", buffer);
}
}
Integration
What You Can Build
Phase 7 Project: A kernel that can read and write files! Your OS now has persistent storage, can access disk drives, navigate directories, and manage files through a unified VFS interface.
Project: File Browser Shell Commands
Add these commands to your shell:
Filesystem shell commands (ls, cat, write) interacting with the VFS layer to browse directories and read/write files
/* shell_fs.c - Filesystem shell commands */
/**
* ls - List directory contents
*/
void cmd_ls(const char* path) {
if (!path) path = ".";
vfs_node_t* dir = vfs_namei(path);
if (!dir) {
kprintf("ls: cannot access '%s': No such file or directory\n", path);
return;
}
if ((dir->flags & 0x7) != VFS_DIRECTORY) {
kprintf("ls: '%s': Not a directory\n", path);
return;
}
kprintf("Contents of %s:\n", path);
kprintf("%-12s %8s %s\n", "TYPE", "SIZE", "NAME");
kprintf("%-12s %8s %s\n", "----", "----", "----");
uint32_t index = 0;
dirent_t* entry;
while ((entry = vfs_readdir(dir, index++))) {
vfs_node_t* child = vfs_finddir(dir, entry->name);
const char* type = "???";
if (child) {
switch (child->flags & 0x7) {
case VFS_FILE: type = "FILE"; break;
case VFS_DIRECTORY: type = "DIR"; break;
case VFS_CHARDEVICE: type = "CHAR"; break;
case VFS_BLOCKDEVICE:type = "BLOCK"; break;
}
}
kprintf("%-12s %8d %s\n", type,
child ? child->length : 0,
entry->name);
}
}
/**
* cat - Display file contents
*/
void cmd_cat(const char* path) {
if (!path) {
kprintf("Usage: cat <filename>\n");
return;
}
vfs_node_t* file = vfs_namei(path);
if (!file) {
kprintf("cat: '%s': No such file\n", path);
return;
}
if ((file->flags & 0x7) != VFS_FILE) {
kprintf("cat: '%s': Not a regular file\n", path);
return;
}
uint8_t* buffer = kmalloc(file->length + 1);
uint32_t bytes = vfs_read(file, 0, file->length, buffer);
buffer[bytes] = '\0';
kprintf("%s", buffer);
if (bytes > 0 && buffer[bytes-1] != '\n') {
kprintf("\n");
}
kfree(buffer);
}
/**
* hexdump - Display file in hex
*/
void cmd_hexdump(const char* path) {
vfs_node_t* file = vfs_namei(path);
if (!file) {
kprintf("hexdump: '%s': No such file\n", path);
return;
}
uint8_t buffer[256];
uint32_t offset = 0;
uint32_t bytes;
while (offset < file->length && offset < 256) {
bytes = vfs_read(file, offset, 16, buffer);
if (bytes == 0) break;
kprintf("%08X: ", offset);
/* Hex bytes */
for (int i = 0; i < 16; i++) {
if (i < bytes) {
kprintf("%02X ", buffer[i]);
} else {
kprintf(" ");
}
if (i == 7) kprintf(" ");
}
/* ASCII */
kprintf(" |");
for (int i = 0; i < bytes; i++) {
char c = buffer[i];
kprintf("%c", (c >= 32 && c < 127) ? c : '.');
}
kprintf("|\n");
offset += bytes;
}
}
Exercises
Exercise 1: File Write Support
Implement FAT file writing: find free clusters, update the FAT chain, and write directory entries for new files. Add a echo "text" > file shell command.
Intermediate
Exercise 2: Block Cache
Implement a block cache that keeps recently-read sectors in memory. Use a hash table for O(1) lookups and LRU eviction when the cache is full.
Performance
Exercise 3: Long Filename Support
Add VFAT long filename support to the FAT driver. Parse LFN entries (attribute 0x0F) and reconstruct UTF-16 filenames. This lets you access files with names longer than 8.3.
Feature
Exercise 4: Initial Ramdisk (initrd)
Create a simple filesystem in memory (loaded by bootloader). Store essential files like shell and config before real disk is mounted. GRUB's tar-based initrd is a good model.
Boot
Next Steps
With memory and files working, it's time for the big one: processes. In Phase 8, we'll implement task switching, system calls, and user mode so programs can run independently.
PHASE 8 PREVIEW: PROCESSES & USER MODE
═══════════════════════════════════════════════════════════════
Your kernel can now:
✓ Read/write disk sectors (ATA driver)
✓ Navigate FAT filesystem
✓ Access files through VFS abstraction
✓ Mount multiple filesystems
Next, you'll build:
┌─────────────────────────────────────────────────────────┐
│ PROCESS MANAGEMENT │
└─────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ KERNEL SPACE (Ring 0) │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Scheduler │ │ Memory │ │ File │ │
│ │ (picks │ │ Manager │ │ System │ │
│ │ next task)│ │ (per-proc) │ │ (VFS) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ┌──────▼────────────────▼────────────────▼──────┐ │
│ │ SYSTEM CALL INTERFACE │ │
│ │ fork(), exec(), read(), write(), exit() │ │
│ └──────────────────────┬────────────────────────┘ │
╠══════════════════════════╪════════════════════════════╣
│ USER SPACE (Ring 3) │ │
│ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Process │ │ Process │ │ Process │ │
│ │ PID 1 │ │ PID 2 │ │ PID 3 │ │
│ │ (init) │ │ (shell) │ │ (app) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└──────────────────────────────────────────────────────┘
You'll implement:
• Process Control Block (PCB) - task state
• Context switching - save/restore registers
• System calls - int 0x80 or syscall instruction
• User mode - Ring 3 with memory protection
• fork() / exec() / exit() / wait()
Key Takeaways from Phase 7:
Block Devices: Disks are accessed in sectors (512 bytes); you can't read individual bytes directly
ATA PIO: Simple polling-based I/O; production systems use DMA for performance
Partition Tables: MBR for legacy (4 partitions, 2TB limit), GPT for modern (128 partitions, huge disks)