Hi there. I continue the file systems article series (which I gave a short break) with FAT32. I shortly mentioned it in previous article, but had not discussed thoroughly. In this article, I will install FreeDOS to a FAT32 disk, examine its structure and compare it with MS-DOS.
As I mentioned before, FAT32 was developed in the 90's when the disk sizes reached above 2 GB limit of FAT16 and it was introduced to end users in August 1996 with Win95 OSR2 (bundled with MS-DOS 7.1). Therefore, no version of MS-DOS, which is sold separately (not Windows bundled), does support FAT32. And Microsoft also announced that OS installations to FAT32 disks will not be supported after WinXP. FAT32 is quite similar to FAT16, in terms of its structure. In this way, file system routines in DOS kernel were not rewritten and FAT32 support has been added to DOS kernel with only about 5KB of code [1].
Starting from very basics, i.e. MBR, the only difference with FAT32 here, is the partition type field in partition table. FAT32 formatted CHS disks have 0x0B and disks with LBA support have 0x0C in this field. I had mentioned this in my second blog post.
First 36 bytes of boot sector are same for both FAT16 and FAT32 but FAT32 has some extra fields. This was explained in my third blog post. The capabilities of FAT32 have been enhanced with newly added mirroring flags, root cluster entry as well as FSINFO sector. And the boot sector code is slightly different from that of FAT16.
First, I installed a FreeDOS VM, so that what I explained, would not remain in pure theory. I will not go into minor details of this installation, as I explained this in my previous article. It would be very good, if VM disk has a single partition larger than 2GB. And fdisk's FAT32 support must be turned on before partitioning. I checked the partition table of the VM with the following command:
hexdump -C FreeDOS.vdi | less
As it can be seen in the output below, partition type is 0x0B, i.e. FAT32 CHS.
002001c0 01 00 0b fe bf 09 3f 00 00 00 4b f5 7f 00 00 00 |......?...K.....|
It is CHS, because the disk is smaller than 8 GB. I installed FreeDOS to a disk larger than 8 GB and partition type value was 0x0C there.
The main difference between FAT16 and FAT32 is obviously in boot sector. In previous article, I put two tables about FAT32 boot sector data structure (in other words DOS 7.1 EBPB). Below, I put those two tables together:
Sector Offset | Size | Description |
---|---|---|
0x00 | 3 byte | JMP to the boot code |
0x03 | 8 byte | OEM Name |
0x0B | word | Bytes per sector |
0x0D | byte | Sectors per cluster |
0x0E | word | Reserved sectors |
0x10 | byte | Number of FATs |
0x11 | word | ReservedNote1 |
0x13 | word | ReservedNote2 |
0x15 | byte | Media descriptor byte |
0x16 | word | ReservedNote3 |
0x18 | word | Sectors per track |
0x1A | word | Number of heads |
0x1C | dword | Number of hidden sectors |
0x20 | dword | Total number of sectorsNote4 |
0x24 | dword | Sectors per FAT |
0x28 | word | Mirroring flagsNote5 |
0x2A | word | FAT versionNote6 |
0x2C | dword | Root directory cluster |
0x30 | word | FSINFO sector |
0x32 | word | Backup boot sector |
0x34 | 12 byte | ReservedNote7 |
0x40 | byte | Physical drive number |
0x41 | byte | ReservedNote8 |
0x42 | byte | Extended signature (0x28 or 0x29) |
0x43 | dword | Volume serial number |
0x47 | 11 byte | Volume label |
0x52 | 8 byte | File system typeNote9 |
Note1: On systems prior to FAT32, this field holds max. number of root directory entries because before FAT32 the root directory was limited in size. This field is now zero because FAT32 removes this restriction.
Note2: In older FAT versions, this field holds total number of sectors. In FAT32, this field is zero (since the number will not fit here anymore) and the value at offset 0x20 is used instead.
Note3: In older FAT versions, this fields holds the number of sectors per FAT, but since this value will not fit in a word with FAT32, the dword at offset 0x24 is used.
Note4: If this field is zero, OS reads the number of sectors from partition record.
Note5: Normally, FAT is always written in two copies and each file operation is written to both copies. With this flag, single table can be set to active.
Note6: This field is defined but not used. It is always zero.
Note7: In Microsoft documentation, this field is given as "boot file name". Normally, kernel file name to be loaded, appears hard coded in boot code. I guess, that this field is reserved to keep kernel file name in a fixed position in future.
Note8: This byte is always zero but Windows NT uses bits 0 and 1 as dirty bit. The details will be explained in FSINFO section.
Note9: Some OSes use this field to store the total number of sectors when it overflows a dword.
In the output of hexdump command above, boot sector comes right after MBR. MBR is of course in zeroth sector and boot sector is in 63rd sector, but since I didn't give -v parameter to hexdump, sectors filled with zeros are shown with just a '*' character.
00207e00 eb 58 90 46 52 44 4f 53 35 2e 31 00 02 08 20 00 |.X.FRDOS5.1... .|
00207e10 02 00 00 00 00 f8 00 00 3f 00 ff 00 3f 00 00 00 |........?...?...|
00207e20 4b f5 7f 00 ee 1f 00 00 00 00 00 00 02 00 00 00 |K...............|
00207e30 01 00 06 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
00207e40 80 00 29 f7 16 04 38 46 52 45 45 44 4f 53 32 30 |..)...8FREEDOS20|
00207e50 31 36 46 41 54 33 32 20 20 20 fc fa 29 c0 8e d8 |16FAT32 ..)...|
OEM Name: FRDOS5.1
Bytes per sector: 512 byte
Sectors per cluster: 8
Reserved sectors: 32
Number of FATs: 2
Media descriptor: 0xF8 (Harddisk)
Sectors per track: 0x3F = 63
Number of heads: 0xFF = 255
Hidden sectors: 0x3F = 63
Total number of sectors: 0x7F F54B = 8 385 867
--- Below part is not compatible with FAT16 ---
Sectors per FAT: 0x1FEE = 8174
Mirroring flags: 0
FAT Version: 0
Root directory cluster: 2
FSINFO Sector: 1*
Backup boot sector: 6*
Physical drive number: 0x80
Extended signature: 0x29
Volume Serial Number: 3804-16F7
Volume Label: FREEDOS2016
File system: FAT32
* These fields will be explained later in this article.
Reading the boot sector data manually, is obviously hard. This is how it's look like in disk editor:
Unfortunately, all the information does not fit to the screen. Therefore, I did a trick and changed character size from 8x16 to 8x14 pixels before starting disk editor with following code:
mov ax,1111
mov bl,0
int 10
mov bl,0
int 10
Source: stackoverflow. In the answer, the guy wrote that above code snippet turns 25 line mode on, whereas it should be 28 lines, I guess. Maybe, this is just because of an incompatibility issue in VBox BIOS code. In the same answer, it is mentioned that giving AX=1112h would enable 43 lines mode. I could fit whole sector info to one screen, but it would became quite difficult to read, so I gave up.
FSINFO Sector
FAT32 has two more important sectors besides the boot sector itself. One of them is the FSINFO sector: file system information sector. I briefly explained this in the boot sector article. Until FAT32, free space on disk used to be calculated by counting free clusters. Let's omit the technical details for a moment. Maximum number of clusters in FAT16 is 216 = 65 536 while this increased to ≈268M in FAT32. This means that the previous free space calculation algorithm would be running 4096 times slower. To solve this, FSINFO sector is added to FAT32, which keeps the number of free and occupied sectors. Even though, there is a pointer to this sector in boot sector, the value of the pointer is almost always 1, which means FSINFO sector follows boot sector. Its structure is as follows:
Sector Offset | Size | Description |
---|---|---|
0x00 | 4 byte | Sector signature 'RRaA' |
0x04 | 480 byte | Reserved |
0x1E4 | 4 byte | Sector signature 'rrAa' |
0x1E8 | dword | Number of free clusters |
0x1EC | dword | Number of occupied clusters |
0x1F0 | 12 byte | Reserved |
0x1FC | 4 byte | Sector signature 0x0,0x0,0x55,0xAA |
Compiled from Wikipedia
The number of free and occupied clusters may not be actual, if a disk is not properly unmounted. In WinNT, when a FAT32 disk is connected, the zeroth bit of byte 0x41 of boot sector is set (dirty bit) and reset when unmounted. When a disk is connected (again) and if this bit is not zero, it indicates that it hasn't been unmounted properly. In this case, the user is asked to run CHKDSK because the number of free and occupied clusters might be (presumably) not correct. Similarly, if an IO error occurs, first bit of byte 0x41 is set and the user is asked to perform a surface scan when the disk is remounted. 0xFFFF FFFF is written to these fields during formatting. This is an invalid value and OS is expected to calculate and write actual values here.
hexdump output of these fields are given below:
00208000 52 52 61 41 00 00 00 00 00 00 00 00 00 00 00 00 |RRaA............|
00208010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
*
002081e0 00 00 00 00 72 72 41 61 be dd 0f 00 ed 18 00 00 |....rrAa........|
002081f0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 55 aa |..............U.|
This output shows that there are 0x0F DDBE = 1 039 806 free clusters. Based on the boot sector, each cluster consists of 8 sectors of 512 bytes. This gives 4061.742 MB of free space:
And the data on the disk is calculated as 24.925 MB in 0x18ED = 6381 occupied clusters. This value is of course, not the total size of all files but total space occupied on the disk. I had mentioned the difference between them and "slack space" concept in the clusters section of FAT article.
Backup Boot Sector
In FAT32, a backup of boot sector is written to the sixth sector against any data corruption. In theory, this backup can be written to any sector, but MS does not recommend to keep this backup anywhere except the sixth sector, in their official documentation. Only Win95's boot sector code tries to read this (FreeDOS or WinXP do not) if an IO error occurs. If the pointer to this sector is in an unreadable sector, it doesn't make much sense to have a backup at all. Although, this protects against viruses that destroy boot sector, it does not prevent any new generation virus from destroying its backup, too. This backup could only help disk recovery programs to restore the boot sector, if the original one is corrupted. By the way, there is also a backup FSINFO sector after backup boot sector.
I previously mentioned that, FAT32 support has started with Win95 OSR2 and continued until WinXP is out of support. I wanted to install OSR2 in VBox to play with (I think it's not supported under VBox, I got blue screen all the time when scanning devices), so I installed it in vmware. It's kinda tricky: I downloaded a Win95b system disk image from bootdisk.com. I created a bootable .iso with k3b, using the floppy image and OSR2 setup files, which I had downloaded from ITU software server. I created a VM in vmware, with 128 MB RAM, 12 GB disk and a floppy drive. I selected Win95 as operating system. Then I booted it with the .iso I created. When I was on A:\> prompt, I created a single partition with FDISK on entire disk. I rebooted the VM with CD. While the VM is booting, you have to be quick to press Esc and select CD drive to boot. Otherwise, the VM will try to boot from hard disk and halt in "Missing operating system" screen. After booting, I formatted the disk, created a directory named "setup", copied installation files on CD there and finally ran SETUP.EXE from this directory. Setup program tries to access to the directory, where the installation was started, for device drivers. If you started the installation from CD, you will be asked for that CD all the time, when a new device is connected, and this is annoying. The rest of the installation is pretty straightforward.
Is it worth installing this? I think no. There is no significant difference in the boot sector data area. Actually, values are more or less the same as FreeDOS except for the disk size (obviously). Similarly, I can say the same for 32 and 64-bit WinXP, no fundamental difference. Boot sector can be viewed with disk editor under Win95, but it detects Windows, activates read-only mode and does not allow it to be changed. In WinXP, hard disk can be read with HxD, but with WinXP, it is impossible to connect to any webpage and download a file any more due to TLS incompatibility. For this reason, I configured file sharing in WinXP, downloaded HxD installation file to my computer and copied it to the VM over file sharing.
Note: Latest Fedora, doesn't allow SMB1 protocol as client (which XP supports), therefore it is necessary to add "client min protocol = NT1" in [global] stanza in smb.conf to connect*.
File Allocation Table (FAT)
The location of the table is calculated by adding hidden sectors value to reserved sectors value, like FAT16. In other words, it is located after boot sector by the number of hidden sectors.
fat_start = hidden_sectors + reserved_sectors (1)
data_start = fat_start + number_of_FATs * sectors_per_FAT (2)
The function, to convert cluster number to sector number is same for both FATs:
clus2sect(c) = (c - 2) * sectors_per_cluster + data_start (3)
To obtain directory tree, root directory cluster value is read from boot sector and its absolute sector is calculated by substituting it in the formula (3). Before parsing the directory tree, let's have a look at FAT. Boot sector is in sector 63 (hidden sectors) and 32 sectors are reserved for it (reserved sectors). From the formula (1), FAT is found in sector 95 (=63+32). The .vdi file header is 0x200000 bytes long, i.e. 512 * 4096. So, .vdi file header is 4096 times 512 byte blocks, plus 95 more blocks for MBR and boot sector, makes 4191, this is the FAT location:
dd if=~/VirtualBox\ VMs/FreeDOS/FreeDOS.vdi \
bs=512 skip=4191 | hexdump -C | less
bs=512 skip=4191 | hexdump -C | less
00000000 f8 ff ff 0f ff ff ff 0f 03 00 00 00 04 00 00 00 |................|
00000010 05 00 00 00 ff ff ff 0f 00 00 00 00 ff ff ff 0f |................|
[SNIP]
The logic behind the entries is the same as in previous versions of FAT, so there is no need to dwell on all entries. I've only included first eight entries here. I added FAT32 support to my fatread code in github*:
Cluster0: 0xFFF FFF8 (0x0000)
Cluster1: 0xFFF FFFF (0x0004)
Cluster2: 0x3 (0x0008)
Cluster3: 0x4 (0x000C)
Cluster4: 0x5 (0x0010)
Cluster5: 0xFFF FFFF (0x0014)
Cluster6: 0x0 (0x0018)
Cluster7: 0xFFF FFFF (0x001C)
...
Zeroth entry is media descriptor byte or FAT ID, like FAT16 and 12, but notice that the first 4 bits of all entries are zero. First entry contains an end of file (EoF) or end of chain (EoC) mark. Second entry is pointing to the third, third to fourth, fourth to fifth and fifth entry contains an EOF. From boot sector, remember that the root directory is starting from the second cluster. So, we found the root directory. Sixth cluster is empty and seventh contains another EOF.
Although, entries are 32-bit in size, only low 28-bits of them are used. Highest nibble is reserved. Therefore, theoretical upper limit of the number of clusters is 228 = 268 435 456. Since 12 values have a special usage, the practical limit is 12 less than theoretical limit. These special values are, values between 0x0FFF FFF8 and 0x0FFF FFFF for EOF, 0x0FFF FFF7 for bad cluster, 0x0 for free cluster and reserved values 0x1 and 0x0FFF FFF6. Additionally, usage of the values between 0x0FFF FFF0 and 0x0FFF FFF5 is discouraged due to compatibility reasons.
Bit 27 of first entry can be used as dirty bit, like byte 0x41 of boot sector. If this bit is reset during mount, OS tries to scan the disk, or at least assumes that the values in FSINFO sector are unreliable. Similarly, bit 26 is used for IO errors.
*Note: FAT32 table takes up a large space (e.g. 2 * 8174 sectors). If there is not much data in .vdi disk, FAT will consist of zero entries. Because these entries are also not stored in .vdi file (unless the virtual disk is Preallocated), fatread is unaware of these empty blocks and returns erroneous values especially with large cluster numbers. For example, it returns FAT ID on the 259840th cluster of FreeDOS, because it actually started to process second FAT:
Directories and Directory Table
From the boot sector, I know, that the root directory starts on second cluster and I also found in previous section, that it continues up to fifth cluster. fat_start = 95 and sectors_per_FAT = 8174. Then, from the formula (2), data_start = 16443 and from the formula (3) clust2sect(2) = 16443. But there is a problem with this calculation as it can be seen below:
dd if=~/VirtualBox\ VMs/FreeDOS/FreeDOS.vdi bs=512 skip=$((4096+16443)) | hexdump -C | head
00000000 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d |----------------|
00000010 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 2d 0d 0a 54 61 64 |-----------..Tad|
The note marked with '*' above, actually explains the problem. Because empty blocks are not kept in dynamically allocated .vdi file, incorrect sector content appears, even though the calculation is correct. I checked root directory contents using DISKEDIT from inside the VM. Actually, when I start DISKEDIT from C:\, it automatically opens the root directory. To prove, that my calculation is correct, I pressed Alt+P, gave 16443 and saw the same data (F2: hexadecimal view).
Contents of the root directory |
I saw "FREEDOS2016" entry at the beginning of the root directory. I opened .vdi file with hexdump -C ~/VirtualBox\ VMs\FreeDOS\FreeDOS.vdi | less command and searched the first entry by typing "/FREEDOS2016", and found this string at the 0x407600 offset of the file.
These entries here are also similar to those in previous FAT versions. Most important difference is that the cluster numbers are 32-bit. Low word is at offset 0x1A and high word is at 0x12. There also are some new features that come with MS-DOS 7 and WinNT. I put an updated version of the table in FAT article here:
Offset | Size | Description |
---|---|---|
0x00 | 8 byte | File name |
0x08 | 3 byte | File extension |
0x0B | 1 byte | File attributes |
0x0C | 1 byte | MSDOS: Reserved WinNT: Case information |
0x0D | 1 byte | Create time (in msec.) |
0x0E | word | Create time |
0x10 | word | Create date |
0x12 | word | Access date |
0x14 | word | Cluster num. (high word) |
0x16 | word | Modify time |
0x18 | word | Modify date |
0x1A | word | Cluster num. (low word) |
0x1C | dword | File size |
Byte 0x0C is ignored by MS-DOS and Win95. In WinNT and XP, third bit of 0xC byte indicates lowercase filename and fourth bit indicates lowercase extension:
0x00: TEST000.TXT
0x08: test000.TXT
0x10: TEST000.txt
0x18: test000.txt
Time and date format is same as FAT16: H: hours, M: Minutes and S: Seconds:
Offset 0x0F, 0x17 | Offset 0x0E, 0x16 | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
H4 | H3 | H2 | H1 | H0 | M5 | M4 | M3 | M2 | M1 | M0 | S4 | S3 | S2 | S1 | S0 |
Likewise, Y: Year, M: Month, D: Day:
Offset 0x11, 0x13, 0x19 | Offset 0x10, 0x12, 0x18 | ||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Y6 | Y5 | Y4 | Y3 | Y2 | Y1 | Y0 | M3 | M2 | M1 | M0 | D4 | D3 | D2 | D1 | D0 |
As mentioned previously, time resolution in FAT16 is two seconds because seconds are represented with five bits. In FAT32, byte 0x0D represents 10 milliseconds. Thereby, time resolution is reduced to 10 msecs but only values between 0..199 are valid.
Example:
0b706e40 4e 54 4c 44 52 20 20 20 20 20 20 27 08 00 00 10 |NTLDR '....|
0b706e50 8e 38 50 53 01 00 00 10 8e 38 8f 1c c0 d0 03 00 |.8PS.....8......|
NTLDR file properties byte is 0x27. This means archive, hidden, read-only and system file flags are set. The value at 0xC is 0x8, so the filename is in lowercase. Extension doesn't exist, so bit 4 is irrelevant. File creation and modification dates are same: 0x1000. Second bit of hour field is 1, so it is 02:00. Dates are also same: 0x388E = 0011100 0100 01110 = 1980+28/04/14. Access date is: 0x5350 = 0101001 1010 10000 = 1980+41/10/16, because I recently checked file properties. File start at cluster 0x011C8F and its size is 0x3D0C0 = 250048 bytes.
Long File Name (LFN) Support
Since FAT32 does not have any enhancements to FAT16 VFAT, detailed information is given in previous article.
FAT32 Boot Code
a. FreeDOS Boot Code
I copied FreeDOS boot code from my VM, compared it with the codes in github and found out that (LBA supporting) boot32lb.asm is running. I downloaded it and added my comments in, prefixed with "; --". This can be downloaded here. Line numbers, given in the rest of the article are w.r.t. the file with my comments.
Boot code is loaded to 0:0x7C00 by default (line 54) and execution continues from real_start label (l. 117) with a jmp instruction. Between the lines 60 and 116, pointers to the boot sector data are defined. Like FAT16 code, the kernel will be loaded at 0x60:0 address, therefore the code copies itself to 0x1FE0:0 and resumes its execution from this address (lines 123-129). The pointer at line 131 points to the address to load the kernel. "Loading FreeDOS" is output to the screen at line 141 and in the following calc_params block, fat_start and data_start values are calculated using formulas (1) and (2), lines 155 and 163.
In FAT16, reserved_sectors value is mostly 1, and FAT starts right after the boot sector. In FAT32, this value is always greater than one, as there is FSINFO and backup boot sectors between boot sector and FAT. Since root directory cluster is given in boot sector, it doesn't need to be calculated as in FAT16.
There is an interesting code block between the lines 169 and 178. In a loop, the value of AX which is initially 512, is compared with bytes_per_sector and multiplied by 2 if not equal. At each step, the operand of the shift operation on line 278 is increased by one (self modifying code) and this shift operation is used while calculating the location (abs. sect. values) of cluster entries in FAT. This code seems to support sectors bigger than 512 bytes, in short.
At line 189, sector number of root directory cluster is calculated and read with readDisk function at line 194. KERNEL.SYS is searched in root directory entries, between the lines 201 and 212. DI is increased while searching and if its value exceeds bytes_per_sector value, then next sector is read (l. 216) and DX is decreased by one, which contains sectors_per_cluster initially. If all sectors in a cluster have been read, then DX will be zero (line 216), so given a cluster number in EAX, next_cluster function returns its consecutive cluster number. All instructions between the lines 188 and 220 repeats until KERNEL.SYS is found or there are no more entries in directory table. If it's found, its cluster number is loaded into EAX (ff_done label). This is translated to abs. sect. number using convert_cluster function (line 232) and entire cluster is read sector by sector with readDisk (line 236). The function on line 232 returns carry, if the next cluster of the file contains EoF mark. This indicates that the file has been completely read. In this case, the execution is handed over to the kernel at boot_success label.
I will not go into the details of individual functions here. I tried to explain all of them with my comments in the code.
b. Windows Boot Code
While searching in internet for Windows boot code, I found some resources about it in personal webpage of Jens Elkner from UNI Magdeburg. Since Win95 boot code is not open source, I will briefly explain this code dump. In this section, I have given code references with offset address instead of line numbers.
The most important detail about Windows boot code is that it consists of two parts. The real boot sector code is responsible for loading the rest, which is in the third sector of a Win95 partition and in twelfth sector of a WinXP partition.
Win95 looks to have inherited DOS boot code. At the beginning of the code, the values on a diskette parameter table are modified (0x7C6E, 0x7C81, etc.). It is dubious that this table is ever used with disks. If the boot media is a floppy (0x7C8E), the execution continues from 0x7CB5 to process boot sector data. If boot media is a disk (or has an MBR to be more precise), this MBR is read, the partition record is found by comparing hidden_sectors value with starting LBA address value in partition entries. They must be equal if it's correct partition entry. Partition type byte of the booting partition is ORed with 2 (at 0x7CAA) and written to 0x7C02 (overwriting the NOP command). This will be compared with 0xE at address 0x7D40. Because if LBA is supported, then partition type can be either 0xC or 0xE (0xC OR 2 = 0xE). In this case, function 0x42 of int 0x13 can be used. It is really strange that MS is not checking LBA support with code, but I also think every computer, which is not so old in 90s, was supporting LBA.
At 0x7CC4 CX=3. This value will enter to read_disk function at 0x7D31 as 2. Thus, two more sectors after the boot sector will be read to memory. These two sectors are FSINFO sector and second part of the boot sector. If reading fails here (0x7CD2), the code will try to load backup boot sector (0x7CD9) and it jumps to the beginning of second phase boot sector at 0x8000, if it has been loaded successfully. Btw, the instruction at 0x7CD4 nonfunctional (from my understanding) but because of 0xF8 value (media descriptor), it may have a special meaning, for example it could be used as variable somewhere in code.
Between 0x7D03 and 0x7D30, there are functions that show errors and reboot computer. Between here and error strings, there are CHS and LBA disk read functions as well. Btw, just behind the error messages, there are four strange pointers, pointing a relative address to themselves. MS oddities again.
Second phase of boot code contains a lot of unnecessary CLI/STI blocks. data_start is calculated up to 0x8016 and stored in [BP-04] at 0x801B. [BP-08] is written with -1 for future use (0x801F). With SHLD instruction at 0x803E, EDX is shifted left by 16-bits and high word of EAX is written to DX. In other words, the value in EAX is written to DX:AX with a single instruction. This instruction is in many places in code with its counterpart, because EAX is used in calculations with 32-bit values but sector number is given to read_disk function in DX:AX. Hey, Microsoft, if you had optimized read_disk function instead?! Two shift operations in 0x8047 write DX:AX to EAX (inverse of SHLD). Another oddity in read_disk is, that a DAP packet is created before checking LBA support. Huh, this will not be used if LBA is not supported, right?!
The root directory cluster read at 0x8028, is translated to abs. sect. number between the offsets 0x8050 and 0x8067 and root directory table is read to the memory 0:0x700 (0x8068 to 0x8073). The entry of IO.SYS is checked on the table (0x8081) and if found, the code will branch from 0x8084 to the routine at 0x809F that loads the file into the memory. In this routine, cluster number of IO.SYS is found at 0x80A2 and written to DX:AX (0x80CF). Only four sectors from this cluster are read to 0:0x700 (0x80D9 and 0x80DC). At 0x80E4 and at 0x80EA, 'MZ' signature at the beginning of IO.SYS and first two chars of the code are checked, respectively. If these are consistent, the file is run at 0x70:0x200, otherwise an "Invalid system disk" error pops up (but why?). IO.SYS has to read the rest of its sectors by itself.
Given a cluster number in DX:AX, the routine between 0x80FD and 0x811F calculates its consecutive cluster number. At 0x8120'de, a sub-function is called to find in which FAT sector the given cluster is. The sector number found is written to the variable at 0x801F. Thus, e.g. if the first cluster of IO.SYS is 3, its entry is in the first sector of FAT and most likely its next cluster is 4, whose entry is also in the same sector as third. So, the same sector of FAT doesn't need to be read again and again. If the value in EAX (0x8138) is the same as the last read FAT sector (in [BP-08]), the JE instruction at 0x813C will jump to the end of the function.
In WinXP, on the other hand, the floppy table is not processed anymore. The code loads the secondary part directly from the twelfth sector to 0x8000 and jumps there. LBA support is checked in the code on the fly. There is a lot of space filled with zeroes in boot sector, when unnecessary parts are removed from the code. Second phase code is almost the same as Win95 except unnecessary CLI/STI's are removed and as I wrote above, sector number is constantly kept in EAX and read_disk takes the parameter from EAX, thus no shift operation is needed. The difference between two codes is 63 bytes. Btw, WinXP loads NTLDR file to 0x:2000:0, instead of IO.SYS. In short, the oddities in Win95 boot code doesn't exist in WinXP boot code and it is more optimized.
[1]: https://en.wikipedia.org/wiki/File_Allocation_Table#FAT32