In my last blog posting I have talked about how a PC boots up, and how you can write your own boot loader. As you have seen it is quite easy to spin up your own OS and print out some characters to the console. But at some point, you need to have the functionality to load additional files into memory, because you can’t do that much work with 512 bytes available in the boot sector. In the case of my OS, it must load the Second Stage Boot Loader (x32 code) into memory and execute it.
The Second Stage Boot Loader loads the real Kernel from the file system, switches the processor into Long Mode (x64), and finally executes the Kernel. Today I want to layout the foundation of this workflow by implementing some assembly code that loads a file from a FAT12 partition into memory and prints out its content to the console.
How to read data from a Floppy Disk
Before we talk about the FAT12 file system itself, we must learn how you can read data from a floppy disk. Maybe you are wondering why I have chosen a floppy disk as a storage system? The answer to this question is quite simple: reading data from a floppy disk is the easiest way for implementing a hobby OS – you just need a few lines of assembly code. If you want to read data from a traditional hard disk, much more code and work is involved, especially when you are leaving the x16 Real Mode, and switching your CPU into x32 or x64 mode.
A floppy disk itself is structured into so-called Sectors. A sector has always a size of 512 bytes. That’s the reason why our boot loader is only limited to 512 bytes: because the BIOS only reads the first sector of 512 bytes from your boot device into memory. If you want to read more data into memory, you must read additional sectors from your floppy disk. Therefore, the smallest amount of data you can read is one sector of 512 bytes.
Accessing your floppy disk is done through the BIOS interrupt 0x13. As I have said in the last blog posting, these BIOS interrupts are only available in the x16 Real Mode. As soon as we have switched our CPU into the x32 Protected Mode (or x64 Long Mode), you need to implement your own device driver to access the floppy disk. You will learn more about this in one of the next upcoming blog postings.
Interrupt 0x13 accepts a few input parameters in the various CPU registers:
- AH: Function code: 0x2 for reading sectors
- AL: The number of sectors to read
- CH: Track
- CL: Sector
- DH: Head
- DL: Drive number: 0x0 for the boot drive
- ES:BX: Memory location where the read sectors are stored
To better understand all these things, we must talk more about the disk geometry of a floppy disk. Let’s have a look at the following picture, where you see a simplified version of this geometry.
Each floppy disk has 80 Tracks per each side (a side is called a Head). Each track has 18 Sectors. So, we can apply the following formula:
80 tracks * 18 sectors * 2 heads * 512 bytes per sector = 1474560 bytes / 1024 =
1440 KB = 1.44 MB
And there we have our 1.44 MB storage capacity from the past. The following assembly code shows you how to use the 0x13 interrupt within the assembly function LoadSectors.
LoadSectors: PUSH DX MOV AH, 0x02 MOV AL, DH MOV CH, BYTE [Track] MOV CL, BYTE [Sector] MOV DH, BYTE [Head] MOV DL, 0 INT 0x13 JC DiskError POP DX CMP DH, AL JNE DiskError ; Return... RET
Now you know how to read some sectors from a given location on the floppy disk into memory. Let’s have now a look at the FAT12 file system.
The FAT12 file system
The File Allocation Table (FAT) is a data structure of the FAT12 specification that is stored on a floppy disk or hard disk and describes which clusters are belonging to which file on that disk. You can compare the FAT to something like a “Table of Contents”. The number “12” in the FAT12 file system means that each entry in the FAT table consists of 12-bit entries.
But what is a cluster? A cluster is a data area on a storage device. On larger storage devices a cluster can span multiple sectors to form a logical data segment. But on a floppy disk a cluster just represents a single sector of 512 bytes. A FAT12 partition consists of 4 major sections:
- Boot Sector
- FAT Tables
- Root Directory
- Data Area
The following picture shows you the disk organization of the FAT12 file system.
- The Boot Sector is always the first sector on the boot device. You know that sector already from the last blog posting.
- The FAT Tables contain pointers to every cluster on the disk and indicate the number of the next cluster in the current cluster chain. The FAT tables are the only method of finding the location of files and directories on the whole disk. There are 2 FAT tables for redundancy. We ignore that fact, and only work with the first FAT table.
- The Root Directory is the primary directory of the disk. The Root Directory has a finite size: 14 sectors * 16 entries per sector = 224 possible entries. Therefore, each entry has a size of 32 bytes (512 bytes / 16 entries per sector).
- The Data Area contains the file and directory data and spans the remaining sectors of the disk.
The boot sector also contains at the beginning the so-called BIOS Parameter Block (BPB) that describes the basic disk geometry which is needed by the operating system to use the disk correctly. The BPB is a sequence of bytes that looks like the following:
|Starting Byte||Length in bytes||Description|
|11||2||Bytes per Sector|
|13||1||Sectors per Cluster|
|14||2||Number of reserved Sectors|
|16||1||Number of FATs|
|17||2||Maximum number of Root Directory entries|
|19||2||Total Sector count|
|22||2||Sectors per FAT|
|24||2||Sectors per Track|
|26||2||Number of Heads|
|32||4||Total Sector count for FAT32 (0 for FAT12 and FAT16)|
|54||8||File System Type|
The following code shows the BPB of our boot sector.
;********************************************* ; BIOS Parameter Block (BPB) for FAT12 ;********************************************* bpbOEM DB "KAOS " bpbBytesPerSector: DW 512 bpbSectorsPerCluster: DB 1 bpbReservedSectors: DW 1 bpbNumberOfFATs: DB 2 bpbRootEntries: DW 224 bpbTotalSectors: DW 2880 bpbMedia: DB 0xF0 bpbSectorsPerFAT: DW 9 bpbSectorsPerTrack: DW 18 bpbHeadsPerCylinder: DW 2 bpbHiddenSectors: DD 0 bpbTotalSectorsBig: DD 0 bsDriveNumber: DB 0 bsUnused: DB 0 bsExtBootSignature: DB 0x29 bsSerialNumber: DD 0xa0a1a2a3 bsVolumeLabel: DB "KAOS DRIVE " bsFileSystem: DB "FAT12 "
The Root Directory contains 16 entries where each entry is 32 bit long. Each entry describes and points to some file or subdirectory on the disk and has the following format:
|Offset in Bytes||Length in Bytes||Description|
|18||2||Last Access Date|
|22||2||Last Write Time|
|24||2||Last Write Date|
|26||2||First Logical Cluster|
|28||4||File Size (in bytes)|
You can see here quite nicely the 8.3 file format specification. Based on this information, it is also quite simple to search and find a given file in the Root Directory. When you read the 32-bit entries from the Root Directory, you just must check for the requested file name – in the 8.3 file format specification. In the boot sector code, I have defined for this reason the variable FileName that contains the string value “HELLO TXT”. That’s the file that we want to read from our FAT12 partition.
The First Logical Cluster field specifies the cluster where the file or subdirectory begins. This field gives you in addition the value of the FAT index that points to the next cluster of the file. But you must bear in mind that the first 2 entries in the FAT table are reserved. Therefore, the entry 2 of the FAT contains the description for the physical sector 33 on the disk (entry 0 and 1 are reserved). When the First Logical Cluster field of the Root Directory entry has the value 2, the file content starts on the physical sector 33 on the disk. The following list shows the possible values of a FAT entry of 12 bits:
- 0x000: Unused
- 0x001: Reserved Cluster
- 0x002 – 0xFEF: The cluster is in use, and the value represents the next cluster
- 0xFF0 – 0xFF6: Reserved Cluster
- 0xFF7: Bad Cluster
- 0xFF8 – 0xFFF: Last Cluster in a file
When the value of the FAT entry is between 0x002 and 0xFEF, it just points to the next cluster which belongs to this file. Therefore, a file in the FAT12 file system can spread multiple clusters which are not stored adjacent to each other on the disk. The following picture illustrates this very important concept in more detail.
Each entry in the FAT table is 12 bits long, because 12 bits are the minimum number of bits required to address our 2880 sectors on the floppy disk. The only challenge with 12-bit entries is the fact that one FAT entry is stored in multiples of a byte, because 2 FAT entries are stored in 3 bytes. Therefore, you need some bit manipulations when you interpret the individual FAT entries.
By now we know how we can read sectors from a floppy disk, and you also know how files are stored and organized in a FAT12 partition. Let’s glue these concepts now together and read a file from a FAT12 partition into a given memory location. When we load a file into memory, we only need to load each cluster which is referenced by the FAT table with the BIOS interrupt 0x13. But here we have now a small problem: the cluster number from the FAT table is a linear address, but when we use the interrupt 0x13, we need a Sector/Track/Head address. To perform this conversion, you have the assembly function LBA2CHS:
LBA2CHS: XOR DX, DX DIV WORD [bpbSectorsPerTrack] INC DL MOV BYTE [Sector], DL XOR DX, DX DIV WORD [bpbHeadsPerCylinder] MOV BYTE [Head], DL MOV BYTE [Track], AL ; Return... RET
With all that knowledge in your hand, and the helper functions that I have mentioned so far, you are now able to read a file from a FAT12 partition. The whole magic happens in the function LoadRootDirectory that loads the file that is stored in the variable FileName into memory. This function is performing the following tasks:
- It loads the Root Directory into the memory location specified by the variable ROOTDIRECTORY_AND_FAT_OFFSET.
- It searches for the file that is stored in the variable FileName in the Root Directory.
- When the file is found, it loads the FAT table into the memory location specified by the variable ROOTDIRECTORY_AND_FAT_OFFSET. We just override the loaded Root Directory, because we don’t need it anymore.
- As soon as the FAT table is loaded into memory, we load the requested file into the memory location specified by the variable IMAGE_OFFSET.
As soon as the file is loaded into memory, it is quite simple to print out its content:
In this blog posting you have learned how you can read a file from a FAT12 partition into memory and print out its content. This was a lot of stuff to be covered, but it is needed, because in the next blog posting I will show you how you can load the Second Stage Boot Loader into memory for its execution. Stay tuned…
Thanks for your time,