Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 8 Min.

FAT12-Dateisystem

Ein vollständiger FAT12-Treiber wird implementiert.
© dotnetpro
[Link auf :]Die zurückliegende Folge dieser Serie [1] behandelte den Aufbau der Grundstrukturen einer Command-Shell. Die Command-Shell bietet nicht nur eigene Befehle an, sondern ist auch in der Lage, Anwendungsprogramme von der Festplatte zu laden und auszuführen.Diese letzte Folge der Serie zeigt nun, wie man ein Dateisystem implementiert. Dafür wird ein FAT12-Treiber entwickelt, der sowohl lesend als auch schreibend auf das FAT12-Dateisystem des Selbstbau-Betriebssystems zugreifen kann. Tabelle 1 gibt einen Überblick über die Funktionen, die dafür entwickelt und eingebaut werden.

Tabelle 1: Dateisystem-Funktionen

Funktionsname Beschreibung
PrintRoot Directory() Gibt den Inhalt des FAT12 Root Directorys aus
CreateFile() Erzeugt eine neue Datei
OpenFile() Öffnet eine Datei
WriteFile() Führt eine Schreiboperation in einer geöffneten Datei aus
ReadFile() Führt eine Leseoperation in einer geöffneten Datei aus
SeekFile() Ändert den aktuellen Lese/Schreib-File-Offset in einer geöffneten Datei
EndOfFile() Überprüft, ob beim Lesen das Ende der Datei erreicht wurde
CloseFile() Schließt eine geöffnete Datei

Das FAT12 Root Directory

Wie bereits aus den vorangegangenen Folgen dieser Artikelserie bekannt ist, nutzt das Selbstbau-Betriebssystem eine FAT12-Partition, die einerseits einen Boot-Sektor enthält und andererseits auch alle notwendigen Betriebssystem-Komponenten, wie beispielsweise den Kernel.Die Abkürzung FAT steht für File Allocation Table. Diese Datenstruktur beschreibt, aus welchen Teilen (Clustern) eine Datei oder ein Ordner im Dateisystem besteht. Ein Cluster hat hier eine Größe von 512 Bytes und ist damit genauso groß wie ein physischer Sektor auf der Festplatte. In unserer File Allocation Table werden zwölf Bits verwendet, um diese Cluster zu adressieren – davon leitet sich der Name FAT12 ab.Eine Datei kann maximal 4096 Cluster (212) zu jeweils 512 Bytes umfassen und hat somit eine maximale Größe von 2 MByte (4096 × 512 Bytes). Das FAT12-Dateisystem besteht aus den folgenden logischen Abschnitten (siehe auch Bild 1):
Die vier logischen Abschnitte des FAT12-Dateisystems (Bild 1) © Autor
  • Boot-Sektor,
  • FAT Tables,
  • Root Directory,
  • Data Area.
Auf den Boot-Sektor folgen die FAT Tables, welche die einzelnen Cluster der Dateien auflisten. Historisch bedingt sind die FAT Tables aus Gründen der Redundanz zweimal vor­handen: Die Sektoren 1 bis 9 gehören zur Originalfassung und die Sektoren 10 bis 18 zur Kopie.Nach den FAT Tables kommt das Root Directory, also das Inhaltsverzeichnis des FAT12-Dateisystems. Darin sind die einzelnen Dateien und Ordner mit ihren Attributen abgelegt. Das Root Directory hat immer eine fixe Größe von 14 Sektoren. Jeder Eintrag im Root Directory hat eine fixe Länge von 32 Bytes – daher können im Root Directory des FAT12-Dateisystems insgesamt 224 Dateien gelistet sein (224 Dateien × 32 Bytes = 7168 Bytes = 14 Sektoren). Auf das Root Directory folgt die Data Area, in der die einzelnen Cluster der Dateien abgelegt werden.Den Anfang macht der altbekannte Befehl dir, welcher die im Root Directory vermerkten Dateien auf dem Bildschirm auflistet. Jeder Eintrag im Root Directory wird über die Struktur RootDirectoryEntry abgebildet, die bereits in Listing 1 vorgestellt wurde.
Listing 1: Die Struktur RootDirectoryEntry
struct RootDirectoryEntry
{
   unsigned char FileName[8];
   unsigned char Extension[3];
   unsigned char Attributes[1];
   unsigned char Reserved[2];
   unsigned CreationSecond: 5;
   unsigned CreationMinute: 6;
   unsigned CreationHour: 5;
   unsigned CreationDay: 5;
   unsigned CreationMonth: 4;
   unsigned CreationYear: 7;
   unsigned LastAccessDay: 5;
   unsigned LastAccessMonth: 4;
   unsigned LastAccessYear: 7;
   unsigned char Ignore[2];
   unsigned LastWriteSecond: 5;
   unsigned LastWriteMinute: 6;
   unsigned LastWriteHour: 5;
   unsigned LastWriteDay: 5;
   unsigned LastWriteMonth: 4;
   unsigned LastWriteYear: 7;
   unsigned short FirstCluster;
   unsigned int FileSize;
} __attribute__ ((packed));

typedef struct RootDirectoryEntry 
   RootDirectoryEntry; 
Um das Root Directory ausgeben zu können, wird es beim Initialisieren des FAT12-Treibers von der Festplatte mithilfe der Funktion LoadRootDirectory() geladen, siehe Listing 2. Gespeichert wird der Inhalt des Root Directorys an der in der Konstanten ROOT_DIRECTORY_BUFFER definierten Hauptspeicheradresse. Zugleich wird auch die komplette FAT Table in den Hauptspeicher geladen, die Zieladresse dafür ist in der Konstanten FAT_BUFFER hinterlegt.
Listing 2: Das Root Directory laden
static void LoadRootDirectory()
{
   // Calculate the Root Directory Size:
   // 14 sectors: => 32 * 224 / 512
   short rootDirectorySectors =
      32 * ROOT_DIRECTORY_ENTRIES / 
      BYTES_PER_SECTOR;
   // Calculate the LBA address of the Root 
   // Directory: 19: => 2 * 9 + 1
   short lbaAddressRootDirectory =
      FAT_COUNT * SECTORS_PER_FAT + 
      RESERVED_SECTORS;
   // Load the whole Root Directory (14 sectors)
   // into memory
   ROOT_DIRECTORY_BUFFER = 
      malloc(rootDirectorySectors * 
      BYTES_PER_SECTOR);
   ReadSectors(
      (unsigned char *)
         ROOT_DIRECTORY_BUFFER, 
      lbaAddressRootDirectory, 
      rootDirectorySectors);
   // Load the whole FAT (18 sectors)
   // into memory
   FAT_BUFFER = malloc(FAT_COUNT * 
      SECTORS_PER_FAT * BYTES_PER_SECTOR);
   ReadSectors((unsigned char *)FAT_BUFFER, 
      FAT1_CLUSTER, FAT_COUNT * 
      SECTORS_PER_FAT);
} 
Ist das Root Directory geladen, kann man über die 224 Einträge iterieren und sie auf dem Bildschirm ausgeben. Das erledigt die Funktion PrintRootDirectory().Wie Sie in Listing 3 erkennen können, wird ein Root-Directory-Eintrag nur dann ausgegeben, wenn er einen Datei­namen aufweist. Fehlt der Name, ignoriert die Funktion Print-RootDirectory() den Eintrag und setzt die Arbeit mit dem nächsten Eintrag fort.
Listing 3: Das Root Directory anzeigen
void PrintRootDirectory()
{
   char str[32] = "";
   int fileCount = 0;
   int fileSize = 0;
   int i;
   RootDirectoryEntry *entry = (RootDirectoryEntry *)
      ROOT_DIRECTORY_BUFFER;
   for (i = 0; i < ROOT_DIRECTORY_ENTRIES; i++)
   {
      if (entry->FileName[0] != 0x00)
      {
         // Print out the file size
         itoa(entry->FileSize, 10, str);
         printf(str);
         printf(" bytes");
         printf("\t");
         // Extract the name and the extension
         char name[9] = "";
         char extension[4] = "";
         substring(entry->FileName, 0, 8, name);
         substring(entry->FileName, 8, 3, 
            extension);
         // Convert everything to lower case
         tolower(name);
         tolower(extension);
         // Print out the file name
         int pos = find(name, ' ');
         char trimmedName[9] = "";
         substring(name, 0, pos, trimmedName);
         printf(trimmedName);
         printf(".");
         printf(extension);
         printf("\n");
         // Calculate the file count and the file size
         fileCount++;
         fileSize += entry->FileSize;
      }
      // Move to the next Root Directory Entry
      entry = entry + 1;
   }
   // Print out the file count and the file size
   printf("\t\t");
   itoa(fileCount, 10, str);
   printf(str);
   printf(" File(s)");
   printf("\t");
   itoa(fileSize, 10, str);
   printf(str);
   printf(" bytes");
   printf("\n");
} 

Neue Dateien erzeugen

Nachdem man sich den Inhalt des Dateisystems anzeigen lassen kann, geht es nun darum, eine neue Datei mithilfe der Funktion CreateFile() zu erzeugen. Listing 4 zeigt die Implementierung dieser Funktion.
Listing 4: CreateFile()
static void CreateFile(
      unsigned char *FileName,
      unsigned char *Extension)
{
   // Find the next free RootDirectoryEntry
   RootDirectoryEntry *freeEntry = 
      FindNextFreeRootDirectoryEntry();
   if (freeEntry != 0x0)
   {
      // Getting a reference to the
      // BIOS Information Block
      BiosInformationBlock *bib = 
         (BiosInformationBlock *)BIB_OFFSET;
      // Allocate the first cluster for the new file
      unsigned short startCluster = 
         FindNextFreeFATEntry();
      FATWrite(startCluster, 0xFFF);
      strcpy(freeEntry->FileName, FileName);
      strcpy(freeEntry->Extension, Extension);
      freeEntry->FirstCluster = startCluster;
      freeEntry->FileSize = 0;
      // Set the Date/Time information of the
      // new file
      freeEntry->LastWriteYear =
         freeEntry->LastAccessYear =
         freeEntry->CreationYear =
         bib->Year - FAT12_YEAROFFSET;
      freeEntry->LastWriteMonth =
         freeEntry->LastAccessMonth =
         freeEntry->CreationMonth =
         bib->Month;
      freeEntry->LastWriteDay = 
         freeEntry->LastAccessDay =
         freeEntry->CreationDay = bib->Day;
      freeEntry->LastWriteHour =
         freeEntry->CreationHour = bib->Hour;
      freeEntry->LastWriteMinute =
         freeEntry->CreationMinute = bib->Minute;
      freeEntry->LastWriteSecond =
         freeEntry->CreationSecond =
         bib->Second / 2;
      // Write the changed Root Directory and the 
      // FAT tables back to disk
      WriteRootDirectoryAndFAT();
      // Allocate a new cluster of 512 bytes in
      // memory, and copy the initial content
      // into it.
      // Therefore, we can make sure that the 
      // remaining bytes are all zeroed out.
      unsigned char *content = 
         (unsigned char *)
         malloc(BYTES_PER_SECTOR);
      memset(content, 0x00,
         BYTES_PER_SECTOR);
      // Write the intial file content to disk
      WriteSectors((unsigned int *)content, 
         startCluster + DATA_AREA_BEGINNING, 1);
      // Release the block of memory
      free(content);
   }
} 
Um eine neue Datei im FAT12-Dateisystem anzulegen, gilt es zunächst einen Eintrag im Root Directory zu finden, der noch nicht verwendet wird. Dies erledigt die Funktion FindNextFreeRootDirectoryEntry(). Ist ein freier Eintrag ermittelt, muss die Funktion FindNextFreeFATEntry() noch einen freien Start-Cluster in der FAT Table finden. Listing 5 zeigt die Implementierung beider Funktionen.
Listing 5: Hilfsfunktionen für das Erstellen einer neuen Datei
static RootDirectoryEntry 
      *FindNextFreeRootDirectoryEntry()
{
   RootDirectoryEntry *entry = 
      (RootDirectoryEntry *)
      ROOT_DIRECTORY_BUFFER;
   for (int i = 0; i < ROOT_DIRECTORY_ENTRIES; 
         i++)
   {
      if (entry->FileName[0] == 0x00)
         return entry;
      // Move to the next Root Directory Entry
      entry = entry + 1;
   }
   // A free Root Directory Entry was not found
   return 0x0;
}
static unsigned short FindNextFreeFATEntry()
{
   unsigned short Cluster = 1;
   unsigned short result = 1;
   while (result > 0)
   {
      Cluster++;
      // Calculate the offset into the FAT table
      unsigned int fatOffset =
         (Cluster / 2) + Cluster;
      unsigned long *offset =
         FAT_BUFFER + fatOffset;
      // Read the entry from the FAT
      unsigned short val = *offset;
      if (Cluster & 0x0001)
      {
         // Odd Cluster
         result = val >> 4; // Highest 12 Bits
      }
      else
      {
         // Even Cluster
         result = val & 0x0FFF; // Lowest 12 Bits
      }
   }
   return Cluster;
} 
Anschließend werden die Datums- und Uhrzeitinformationen der neuen Datei gesetzt und das Root Directory sowie die FAT Tables wieder zurück auf die Festplatte geschrieben. Zum Abschluss wird auch noch der mit Null-Werten initialisierte erste Cluster der neuen Datei auf die Festplatte geschrieben.

Dateien öffnen und schließen

Bei der Funktion CreateFile(), die Sie oben bereits kennengelernt haben, handelt es sich um eine statische C-Funktion, das heißt, dass sie sich außerhalb des kompilierten Moduls nicht aufrufen lässt, da ihr Gültigkeitsbereich beschränkt ist.Das Erstellen einer neuen Datei erfolgt mit der Funktion OpenFile(), nachdem die Datei im Write- oder Append-Modus geöffnet wurde. Im Dateisystem ist sie zu diesem Zeitpunkt noch nicht vorhanden. Der FAT12-Treiber implementiert aktuell die folgenden Datei-Modi:
  • r (read): Die Datei wird für einen Lesevorgang geöffnet.
  • w (write): Die Datei wird für einen Schreibvorgang geöffnet. Ist die Datei noch nicht vorhanden, wird sie erzeugt, ansonsten wird sie überschrieben.
  • a (append): Die Datei zum Anhängen von Daten öffnen.
Ist die Datei noch nicht vorhanden, wird sie erzeugt. Sobald Sie eine Datei mit OpenFile() öffnen, muss der Betriebssystem-Kernel diese intern auch irgendwie verwalten, damit Sie in weiterer Folge mit den Funktionen ReadFile() und WriteFile() auf die Datei zugreifen können. Dazu wird beim Öffnen der Datei ein sogenannter File Descriptor erzeugt, der in eine interne Kernel-Liste eingetragen wird. Die dafür genutzte Datenstruktur sieht so aus:

struct FileDescriptor
{
   unsigned char FileName[11];
   unsigned char Extension[3];
   unsigned long FileSize;
   unsigned long 
     CurrentFileOffset;
   char FileMode[2];
};
typedef struct FileDescriptor 
   FileDescriptor; 
In der Variablen CurrentFileOffset wird hier das aktuelle File Offset innerhalb der geöffneten Datei gespeichert. Dieser Wert ist sehr wichtig, wenn eine Datei später gelesen oder geschrieben werden soll. Die Funktion SeekFile() dient dazu den aktuellen File Offset zu ändern.Um nun eine geöffnete Datei in eine Kernel-Liste eintragen zu können, ist noch ein eindeutiger Schlüssel erforderlich. Dieser wird mit der Funktion HashFileName() erzeugt, die auf Basis des Dateinamens (und der angehängten Prozess-ID) einen eindeutigen Hashwert erzeugt. Die Hashfunktion basiert auf einem Algorithmus, der unter [2] näher beschrieben ist. Die Implementierung sieht wie folgt aus:

static unsigned long HashFileName(
      unsigned char *FileName)
{
   int hash = 0;
   int length = strlen(FileName);
   // To store 'P'.
   int p = 1;
   // For taking modulo.
   int m = 1000000007;
   for (int i = 0; i < length; i++)
   {
      hash += (FileName[i] - 'a') * p;
      hash = hash % m;
      p *= 41;
   }
   return hash;
} 
In Listing 6 sehen Sie die Funktion OpenFile(), die auch die Logik hinsichtlich der unterschiedlichen Datei-Modi implementiert. OpenFile() liefert den erzeugten Hash-Wert des hinzugefügten File Descriptors zurück. Dieser Hash-Wert ist das sogenannte File Handle, das die anderen I/O-Funktionen als Input-Parameter benötigen.
Listing 6: Die Funktion OpenFile()
unsigned long OpenFile(unsigned char *FileName, 
      unsigned char *Extension, char *FileMode)
{
   char pid[10] = "";
   // Construct the full file name
   char fullFileName[15];
   strcpy(fullFileName, FileName);
   strcat(fullFileName, Extension);
   // Find the Root Directory Entry for the
   // given program name
   RootDirectoryEntry *entry = 
      FindRootDirectoryEntry(fullFileName);
   // Check, if the requested file was found in the 
   // FAT12 file system partition
   if ((entry == 0x0) &&
         (strcmp(FileMode, "w") == 0))
   {
      // If the requested file was not found in the 
      // "write" mode, we just create a new empty 
      // file
      CreateFile(FileName, Extension);
   }
   else if ((entry == 0x0) &&
         (strcmp(FileMode, "r") == 0))
   {
      // If the requested file was not found in the 
      // "write" mode, we return a NULL value for 
      // the file handle
      return 0;
   }
   else if ((entry == 0x0) &&
         (strcmp(FileMode, "a") == 0))
   {
      // If the requested file was not found in the 
      // "append" mode, we just create a new 
      // empty file
      CreateFile(FileName, Extension);
   }
   else if ((entry != 0x0) &&
         (strcmp(FileMode, "w") == 0))
   {
      // If the requested file exists in the "write" 
      // mode, its content must be truncated.
      // Therefore, we delete and recreate the file
      DeleteFile(FileName, Extension);
      CreateFile(FileName, Extension);
   }
   // The PID of the current running task is 
   // concatenated to the file name
   // to make it unique across multiple running 
   // tasks.
   // Otherwise we would have a hash collision if 
   // the same file is opened across
   // multiple running tasks.
   tolower(fullFileName);
   ltoa(GetTaskState()->PID, 10, pid);
   strcat(fullFileName, pid);
   // Calculate a hash value for the given
   // file name
   unsigned long hashValue = 
      HashFileName(fullFileName);
   // Create a new FileDescriptor and store it in 
   // a system-wide Kernel list
   FileDescriptor *descriptor = (FileDescriptor *)
      malloc(sizeof(FileDescriptor));
   strcpy(descriptor->FileName, FileName);
   strcpy(descriptor->Extension, Extension);
   descriptor->FileSize = entry->FileSize;
   descriptor->CurrentFileOffset = 0;
   strcpy((char *)&descriptor->FileMode, 
      FileMode);
   AddEntryToList(FileDescriptorList,
      descriptor, hashValue);
   // If the requested file exists in the "append" 
   // mode, we set the FileOffset to the end of 
   // the file
   if (strcmp(FileMode, "a") == 0)
      descriptor->CurrentFileOffset =
         descriptor->FileSize;
   // Return the key of the newly added 
   // FileDescriptor
   return hashValue;
} 
Eine dieser Funktionen ist CloseFile(), die eine geöffnete Datei wieder schließt. Dazu wird einfach der entsprechende File Descriptor aus der Kernel-Liste entfernt:

int CloseFile(unsigned long FileHandle)
{
   // Find the file which needs to be closed
   ListEntry *entry = 
      GetEntryFromList(
         FileDescriptorList, FileHandle);
   if (entry != 0x0)
   {
      // Close the file by removing it from the list
      RemoveEntryFromList(
         FileDescriptorList, entry);
      return 0;
   }
   else
      return -1;
} 

Dateien schreiben

Legt man eine neue Datei mit OpenFile() an, so hat diese zunächst einmal noch keinen Inhalt. Dieser wird erst durch die Funktion WriteFile() auf die Festplatte geschrieben. Wie Sie bereits aus der ersten Folge dieser Serie [3] wissen, besteht eine Datei im FAT12-Dateisystem aus einer einfach verketteten Liste von Clustern zu 512 Bytes, die in der FAT Table abgelegt sind. Der Eintrag der Datei im Root Directory zeigt auf den ersten Cluster der Datei (Bild 2).
Aufbau des FAT12-Dateisystems (Bild 2) © Autor
Die Funktion WriteFile() erwartet im ersten Parameter das File Handle der bereits geöffneten Datei, im zweiten Parameter werden die Nutzdaten übergeben und im dritten Parameter die Länge der Nutzdaten.Im ersten Schritt muss nun basierend auf dem aktuellen File Offset der Cluster, in den die Nutzdaten geschrieben werden sollen, von der Festplatte in den Hauptspeicher geladen werden. Wie Sie in Listing 7 erkennen können, muss die Funktion WriteFile() auch dafür sorgen, dass die Datei im FAT12-Dateisystem passend vergrößert wird, wenn sich das aktuelle File Offset außerhalb der aktuellen Dateigröße befindet. Das Vergrößern der Datei erfolgt dynamisch über die Funktion AllocateNewClusterToFile().
Listing 7: Laden des richtigen Clusters
// Allocate a file buffer
unsigned char *file_buffer =
   (unsigned char *)malloc(BYTES_PER_SECTOR);
// Calculate from the current file position the 
// cluster and the offset within that cluster
unsigned long cluster = 
   descriptor->CurrentFileOffset / 
   BYTES_PER_SECTOR;
unsigned long offsetWithinCluster = 
   descriptor->CurrentFileOffset – 
   (cluster * BYTES_PER_SECTOR);
unsigned short currentFatSector = 
   entry->FirstCluster;
// Loop until we reach the cluster that we want 
// to write to. If necessary, new clusters will be 
// created and added for the file.
for (int i = 0; i < cluster; i++)
{
   // Read the next Cluster from the FAT table
   unsigned short nextFatSector = 
      FATRead(currentFatSector);
   // The next cluster is the last one in the chain
   if (nextFatSector >= EOF)
   {
      // Allocate a new cluster for the file
      unsigned short newFatSector = 
         AllocateNewClusterToFile(
         currentFatSector);
      // Set the current sector
      currentFatSector = newFatSector;
   }
   else
   {
      // Set the current sector
      currentFatSector = nextFatSector;
   }
}
// When the data is stored across the last 
// boundary of the current sector, we have 
// allocate an additional cluster to the file
if ((offsetWithinCluster + Length >= 
      BYTES_PER_SECTOR) && (descriptor->FileSize 
      < descriptor->CurrentFileOffset + Length))
{
   // Allocate a new cluster for the file
   AllocateNewClusterToFile(currentFatSector);
}
// Calculate the following disk sector
unsigned short fatSectorFollowing = 
   FATRead(currentFatSector);
// Read the specific sector from disk
ReadSectors((unsigned char *)file_buffer, 
   currentFatSector + DATA_AREA_BEGINNING, 1);
// Read the following logical sector, when the data is stored across 2 disk sectors
if (offsetWithinCluster + Length >= 
      BYTES_PER_SECTOR)
{
   ReadSectors((unsigned char *)(file_buffer + 
      BYTES_PER_SECTOR), 
      fatSectorFollowing + 
      DATA_AREA_BEGINNING, 1);
} 
Wie Sie in Listing 8 erkennen können, wird dafür der nächste freie FAT-Cluster über die Funktion FindNextFreeFATEntry() ermittelt. Über FATWrite() wird dieser neue FAT-Cluster dann in den aktuell letzten FAT-Cluster eingetragen und die Datei somit erweitert. In den letzten FAT-Cluster wird schlussendlich noch der Wert 0xFFF eingetragen, der das Ende der Datei (EOF) signalisiert.
Listing 8: Vergrößern einer Datei
static unsigned short AllocateNewClusterToFile(
      unsigned short CurrentFATSector)
{
   // Allocate a new cluster for the file
   unsigned short newFatSector = 
      FindNextFreeFATEntry();
   FATWrite(CurrentFATSector, newFatSector);
   FATWrite(newFatSector, 0xFFF);
   // Zero-initialize the new cluster and write it 
   // to disk
   unsigned char *emptyContent = 
      (unsigned char *)
      malloc(BYTES_PER_SECTOR);
   memset(emptyContent, 0x00, 
      BYTES_PER_SECTOR);
   WriteSectors((unsigned int *)emptyContent, 
      newFatSector + DATA_AREA_BEGINNING, 1);
   // Release the block of memory
   free(emptyContent);
   // Return the new allocated FAT sector
   return newFatSector; 
In Listing 9 sehen Sie die Implementierung der Funktion FATWrite(), die auf den ersten Blick ein wenig kompliziert erscheint, da sich ein 12-Bit langer FAT-Eintrag über zwei Bytes erstreckt und somit ein wenig Bit-Arithmetik betrieben werden muss.
Listing 9: Einen FAT-Eintrag schreiben
static void FATWrite(
      unsigned short Cluster,
      unsigned short Value)
{
   // Calculate the offset into the FAT table
   unsigned int fatOffset = (Cluster / 2) + Cluster;
   if (Cluster % 2 == 0)
   {
      // Even Cluster
      FAT_BUFFER[fatOffset + 0] = (0xFF & Value);
      FAT_BUFFER[fatOffset + 1] =
         ((0xF0 & (FAT_BUFFER[fatOffset + 1])) | 
         (0x0F & (Value >> 8)));
   }
   else
   {
      // Odd Cluster
      FAT_BUFFER[fatOffset + 0] =
         ((0x0F & (FAT_BUFFER[fatOffset + 0])) | 
         ((0x0F & Value) << 4));
      FAT_BUFFER[fatOffset + 1] =
         ((0xFF) & (Value >> 4));
   }
} 
Nachdem WriteFile() den gesuchten Cluster von der Festplatte in den Hauptspeicher geladen hat, kopiert die Funk­tion memcpy() die übergebenen Nutzdaten an die entsprechende Stelle im Hauptspeicher. Schlussendlich schreibt die Funktion WriteSectors() die Nutzdaten wieder auf die Festplatte zurück.Außerdem werden noch das aktuelle Zugriffsdatum der Datei und die Dateigröße im Root Directory passend geändert. Listing 10 zeigt den für diesen Zweck innerhalb der Funktion WriteFile() zusätzlich erforderlichen Code.
Listing 10: Dateiänderungen durchführen
// Copy the requested data into the destination 
// disk sector
memcpy(file_buffer + offsetWithinCluster, 
   Buffer, Length);
// Write the specific sector to disk
WriteSectors((unsigned int *)file_buffer, 
   currentFatSector + DATA_AREA_BEGINNING, 1);
// Write the following logical sector, when the 
// data is stored across 2 disk sectors
if (offsetWithinCluster + Length >= 
      BYTES_PER_SECTOR)
{
   WriteSectors((unsigned int *)
      (file_buffer + BYTES_PER_SECTOR), 
      fatSectorFollowing + 
      DATA_AREA_BEGINNING, 1);
}
// Release the file buffer
free(file_buffer);
// Set the last Access and Write Date
SetLastAccessDate(entry);
// Set the current file position within
// the FileDescriptor
descriptor->CurrentFileOffset += Length;
// Check if the file size has changed
if (descriptor->CurrentFileOffset > 
      entry->FileSize)
{
   // Change the data in the RootDirectory
   entry->FileSize =
      descriptor->CurrentFileOffset;
   descriptor->FileSize =
      descriptor->CurrentFileOffset;
}
// Write the RootDirectory and the FAT
// tables back to disk
WriteRootDirectoryAndFAT(); 

Dateien lesen

Was noch fehlt, ist eine Funktion ReadFile(), mit der man Daten aus einer Datei lesen kann. Auch diese Funktion erwartet als Parameter das File Handle der geöffneten Datei, den reservierten Hauptspeicherbereich und die gewünschte Länge der zu lesenden Daten.ReadFile() liest die Daten immer basierend auf dem aktuellen File Offset, das im File Descriptor hinterlegt ist. Wie Sie in Listing 11 erkennen können, wird am Schluss das aktuelle File Offset im File Descriptor entsprechend den gelesenen Bytes erhöht, wodurch der nächste Aufruf von ReadFile() die Daten am aktuellen File Offset innerhalb der Datei zurückliefert. Dadurch lässt sich die Datei durch mehrmalige Aufrufe von ReadFile() komplett auslesen. Dass das Ende der Datei erreicht ist, signalisiert die Funktion EndOfFile(), die 1 zurückliefert, sobald keine Nutzdaten mehr vorhanden sind:
Listing 11: Lesen einer Datei
// Allocate a file buffer
unsigned char *file_buffer = 
   (unsigned char *)malloc(BYTES_PER_SECTOR);
// Calculate from the current file position the 
// cluster and the offset within that cluster
unsigned long cluster =
   descriptor->CurrentFileOffset / BYTES_PER_SECTOR;
unsigned long offsetWithinCluster = 
   descriptor->CurrentFileOffset - 
   (cluster * BYTES_PER_SECTOR);
unsigned short fatSector = entry->FirstCluster;
// Loop until we reach the cluster that
// we want to read
for (int i = 0; i < cluster; i++)
{
   // Read the next Cluster from the FAT table
   fatSector = FATRead(fatSector);
}
// Calculate the following disk sector
unsigned short fatSectorFollowing = 
   FATRead(fatSector);
// Check for the EndOfFile condition
if (descriptor->CurrentFileOffset + Length > 
      descriptor->FileSize)
{
   Length = descriptor->
      FileSize – descriptor->CurrentFileOffset;
}
// Read the specific sector from disk
ReadSectors((unsigned char *)file_buffer, 
   fatSector + DATA_AREA_BEGINNING, 1);
// We also read the following sector, when the 
// requested data is stored across 2 disk sectors
if (offsetWithinCluster + Length > BYTES_PER_SECTOR)
{
   ReadSectors((unsigned char *)file_buffer + 
      BYTES_PER_SECTOR, 
      fatSectorFollowing + 
      DATA_AREA_BEGINNING, 1);
}
// Copy the requested data into the
// destination buffer
memcpy(Buffer, file_buffer + 
   offsetWithinCluster, Length);
// Set the current file position within
// the FileDescriptor
descriptor->CurrentFileOffset += Length;
// Release the file buffer 

int EndOfFile(unsigned long FileHandle)
{
   // Find the file from which we want to check 
   // the EndOfFile condition
   ListEntry *entry = 
      GetEntryFromList(FileDescriptorList, 
      FileHandle);
   FileDescriptor *descriptor = 
      (FileDescriptor *)entry->Payload;
   if (descriptor->CurrentFileOffset == 
      descriptor->FileSize)
      return 1;
   else
      return 0;
} 
Auf Basis der vorgestellten Funktionen können Sie nun bereits sehr mächtige Funktionalitäten in unserem Selbstbau-Betriebssystem implementieren, wie zum Beispiel das Kopieren einer Datei. Den dafür erforderlichen Code finden Sie in Listing 12.
Listing 12: Kopieren einer Datei
unsigned char buffer[512] = "";
// Open both files
unsigned long fileHandleSource = 
   OpenFile("BIGFILE ", "TXT", "r");
unsigned long fileHandleTarget = 
   OpenFile("TARGET  ", "TXT", "w");
// Check if the source file was opened
if (fileHandleSource == 0)
   printf("The source file could not be 
   opened.\n");
// Check if the target file was opened
if (fileHandleSource == 0)
   printf("The target file could not be opened or 
   created.\n");
if ((fileHandleSource != 0) &&
      (fileHandleTarget != 0))
{
   // Copy the source file to the target file
   while (!EndOfFile(fileHandleSource))
   {
      ReadFile(fileHandleSource,
         (unsigned char *)&buffer, 512);
      WriteFile(fileHandleTarget,
         (unsigned char *)&buffer, 512);
   }
   // Close both file handles
   CloseFile(fileHandleSource);
   CloseFile(fileHandleTarget);
   printf("File copied.\n");
} 
Die bereits erwähnte Funktion SeekFile() erlaubt es, den aktuellen File Offset der geöffneten Datei zu verändern, um an eine gewünschte Position innerhalb der Datei zu springen:

int SeekFile(unsigned long FileHandle,
   unsigned long NewFileOffset)
{
   // Find the file from which we want to read
   ListEntry *entry = 
      GetEntryFromList(FileDescriptorList, 
      FileHandle);
   FileDescriptor *descriptor = 
      (FileDescriptor *)entry->Payload;
   if (descriptor != 0x0)
   {
      descriptor->CurrentFileOffset = 
         NewFileOffset;
      return 0;
   }
   else
      return -1;
} 
Mithilfe von SeekFile() sind Sie in der Lage, jeden beliebigen Teil einer Datei auszulesen und weiterzuverarbeiten. Damit verfügen Sie nun über einen vollständigen FAT-Treiber, mit dessen Hilfe Sie in der Lage sind, mit dem FAT12-Dateisystem zu interagieren, um persistente Daten zu verwalten.

Fazit

Im Rahmen dieses Artikels wurde ein FAT12-Treiber entwickelt, der in der Lage ist Dateien im FAT12-Dateisystem zu lesen und zu schreiben. Damit ist die inzwischen auf 15 Teile angewachsene Artikelserie zur Betriebssystem-Entwicklung an ihrem Ende angelangt. Das Selbstbau-Betriebssystem hat inzwischen ein Stadium erreicht, in dem man damit bereits etliche sinnvolle Aufgaben realisieren kann. Wenn auch nicht in dieser Serie, geht der Ausbau des Betriebssystems dennoch weiter. Wer möchte kann den Fortgang im GitHub-Repository des Autors [4] verfolgen.
Projektdateien herunterladen

Fussnoten

  1. Klaus Aschenbrenner, Programme ausführen, Das eigene Betriebssystem, Teil 14, dotnetpro 2/2024, Seite 91 ff.,
  2. Ujjawal Gupta, String Hashing,
  3. Klaus Aschenbrenner, Kaos64: Die ersten Schritte, dotnetpro 1/2023, Seite 76 ff.,
  4. GitHub-Repository des Autors, https://github.com/sqlpassion/osdev

Neueste Beiträge

Managed DevOps Pools - Azure DevOps Pipelines Security
Agent Pools als Managed Service mit einfacher Integration in private Netzwerke und Authentisierung mittels Managed Identity tragen deutlich zur Sicherheit der Agent-Infrastruktur bei.
7 Minuten
7. Aug 2025
Arbeiten mit Tabellen und KI in Dataverse
Microsoft unterstützt die zentrale Datenmanagement-Lösung Dataverse in Power Apps mit KI-Features.
7 Minuten
6. Aug 2025
Browser-Apps mit Avalonia entwickeln - Avalonia
Klassische UI-Frameworks finden ihren Weg in den Browser
7 Minuten
11. Aug 2025
Miscellaneous

Das könnte Dich auch interessieren

Sicher ist sicher - Azure DevOps Pipelines Security
Als integraler Bestandteil der Entwicklungsumgebung ist Azure DevOps Pipelines oft Ziel von Angriffen. Da ist es gut zu wissen, wo die Schwachstellen des Systems liegen.
14 Minuten
16. Jun 2025
CodeProject.AI Server in neuer Version - Lokaler AI-Server
CodeProject.AI Server (jetzt in Version 2.1.10) ist ein lokal installierter, selbstgehosteter, schneller, kostenloser und Open Source Artificial Intelligence Server für jede Plattform und jede Sprache.
2 Minuten
Für Einsteiger: Backend-Webentwicklung mit .NET - Microsoft
Auf YouTube bietet Microsoft eine Videoserie für Einsteiger in die Backend-Webentwicklung mit .NET.
2 Minuten
13. Feb 2024
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige