Simple file system for small disks.

The FAT12 file system is a simple, little-endian file system useful for the early stages OS development.

 

Segments of FAT12

Partition Boot Sector (PBS)

The PBS mixes executable code and metadata about the drive and file system. It is designed to be loaded and executed by the BIOS as part of the boot process.

struct Partition_Boot_Sector {
  uint8_t  jump[3];           /* Jump command to jump into boot_code */
  char     oem_name[8];       /* OEM name, contents don't really matter */
  uint16_t bytes_per_sect;    /* Bytes per sector */
  uint8_t  sects_per_cluster; /* Sectors per cluster */
  uint16_t nreserved;         /* Number of reserved sectors (including this PBR) */
  uint8_t  nfats;             /* Number of file allocation tables (FATs) */
  uint16_t nroot_dir_entries; /* Number of root directory entries */
  uint16_t nsectors;          /* Number of sectors on this partition */
  uint8_t  media_desc;        /* Media descriptor */
  uint16_t sects_per_fat;     /* Sectors per FAT */
  uint16_t sects_per_track;   /* Sectors per track */
  uint16_t nheads;            /* # of heads */
  uint32_t nhidden;           /* # of hidden sectors */
  uint32_t nsectors_ext;      /* # of sectors on partition (only used if nsectors = 0) */
  uint8_t  drive;             /* Drive number */
  uint8_t  reserved;          /* Reserved */
  uint8_t  boot_sig_ext;      /* Extended boot signature */
  uint32_t serial;            /* Volume serial number */
  char     label[11];         /* Text label for volume */
  char     type[8];           /* File system type as 8 char string */
  uint8_t  boot_code[448];    /* Boot sector code (binary executable) */
  uint16_t signature;         /* Boot sector signature (must be 0xAA55 little endian) */
};

File Allocation Table (FAT)

The file allocation table (FAT) is the most important data structure used by the FAT12 file system. FAT divides the file-system up into fixed size chunks called clusters (for example, 1 cluster = 512 bytes). Files are stored in units of clusters instead of bytes, so storing a very small file still causes that file to occupy an entire cluster. The clusters occupied by a file also need not be sequential, so the parts of a single file can be stored all over the disk.

The FAT itself is a compact, byte encoded linked list that is used to determine all of the clusters occupied by the contents of a given file. To load a file you find its directory entry which tells you the cluster its contents start in. Each link in the FAT12 table contains a 12-bit entry that either points to the next cluster or contains the terminal value between 0xFF8 and 0xFFF indicating this is the last cluster for the file. To load a file simply navigate the FAT link-by-link until you hit a terminal value.

As an example, here is how a file that occupies 3 clusters might be organized in the FAT:

  x000    x001    x002    x003    x004    x005    x006    x007    ...
  -       -       003     005     -       FFF     -       - 
              ___/  \____/  \____________/  |
             /     continues   continues    X
  File starts        @ 003       @ 005    terminal
  @ Cluster 002

To compute the offset into the partition of a given FAT:

/*
 * Return the offset from the start of the partition to the start of file
 * allocation table (FAT) nfat. nfat is in range [0, pbr->nfats).
 */
static uint32_t
fatoffset(Partition_Boot_Sector *pbs, uint32_t nfat)
{
  return pbs->bytes_per_sect * ((nfat * pbs->sects_per_fat) + pbs->nreserved);
}

Directory Entries

The root folder as well as all subdirectories are organized as a series of directory entries.

struct Directory_Entry {
  char     name[8];       /* Space padded filename ([0] = E5h if deleted, 05h if pending delete) */
  char     ext[3];        /* File extension */
  uint8_t  attributes;    /* File attributes bitfield */
  uint8_t  reserved[10];  /* Reserved, system dependent uses */
  uint16_t created_time;  /* [15:11] = hours(0..23), [10:5] = minutes, [4:0] = seconds/2 */
  uint16_t created_date;  /* [15:9] = year - 1980, [8:5] = month, [4:0] = day */
  uint16_t start_cluster; /* Cluster where this directory entry starts */
  uint32_t file_size;     /* Size of the file in bytes */
};

To calculate the offset of the root directory you can use:

/*
 * Return the offset of the start of the root directory.
 */
static uint32_t
rootdiroffset(Partition_Boot_Sector *pbs)
{
  return pbs->bytes_per_sect * ((pbs->nfats * pbs->sects_per_fat) + pbs->nreserved);
}

You can then read directory entry objects from the root directory until you find one where the first byte of name is 0x00. Once you have a directory entry you can read its corresponding FAT entry as follows:

/*
 * Return the value of the FAT entry for the given cluster.
 */
static uint16_t
fatentry(Partition_Boot_Sector *pbs, uint8_t *disk, uint16_t ncluster)
{
  uint16_t v, b1, b2;
  uint32_t foff;

  foff = fatoffset(pbs, 0);

  /*
   * Every 2 entries takes up 3 bytes. Compute the offset of the start of
   * 3 byte cluster for the requested entry.
   */
  foff += 3 * (ncluster / 2);

  switch(ncluster % 2) {
  case 0:
    /*
     * For the even case the number is split in the first 2 bytes of the 3 byte
     * cluster in little-endian order.
     */
    b1 = disk[foff];
    b2 = disk[foff + 1];
    v = ((b2 & 0x0F) << 8) | b1;
    break;
  case 1:
    /*
     * For the odd case the number is split in the second 2 bytes of the 3 byte
     * cluster in little-endian order.
     */
    b1 = disk[foff + 1];
    b2 = disk[foff + 2];
    v = ((b2 << 4) & 0xFF0) | ((b1 & 0xF0) >> 4);
    break;
  }

  return v;
}

To find the data for the cluster traverse to its cluster offset:

#define DIRENTSIZE 32 /* Size of a directory entry in bytes */

/*
 * Return the offset of the data segment following the root directory.
 */
static uint32_t
dataoffset(Partition_Boot_Sector *pbs)
{
  return rootdiroffset(pbs) + (pbs->nroot_dir_entries * DIRENTSIZE);
}

/*
 * Return the offset of the given cluster.
 */
static uint32_t
clusteroffset(Partition_Boot_Sector *pbs, uint16_t ncluster)
{
  /* Subtract two to account for first two FAT clusters holding special values */
  return dataoffset(pbs) + ((ncluster - 2) * pbs->sects_per_cluster * pbs->bytes_per_sect);
}

Last update on 7E4C0B, edited 2 times. 5/5thh