Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Does mkfs.fat.c calculate number of data clusters properly? #125

Open
ckurtz22 opened this issue Jun 20, 2019 · 2 comments
Open

Does mkfs.fat.c calculate number of data clusters properly? #125

ckurtz22 opened this issue Jun 20, 2019 · 2 comments

Comments

@ckurtz22
Copy link

ckurtz22 commented Jun 20, 2019

dosfstools/src/mkfs.fat.c

Lines 799 to 800 in 91978a2

clust12 = 2 * ((long long)fatdata1216 * sector_size + nr_fats * 3) /
(2 * (int)bs.cluster_size * sector_size + nr_fats * 3);

dosfstools/src/mkfs.fat.c

Lines 819 to 820 in 91978a2

clust16 = ((long long)fatdata1216 * sector_size + nr_fats * 4) /
((int)bs.cluster_size * sector_size + nr_fats * 2);

dosfstools/src/mkfs.fat.c

Lines 847 to 848 in 91978a2

clust32 = ((long long)fatdata32 * sector_size + nr_fats * 8) /
((int)bs.cluster_size * sector_size + nr_fats * 4);

src/mkfs.fat.c lines 799/819/847
The space in the FAT's that are reserved (the first 2 entries in each) is added to the total amount of space allocated for FAT/data. In reality, shouldn't that space be subtracted, since it is already inherently part of the space allocated for the FAT's? If we are trying to find the total amount of space that can be allocated toward data clusters (the actual cluster and the entry in the FAT), then that would be all of the data for FATs and data, minus the reserved space that is not used.

This is supported by the fact in the atari_format code (line 986), the reserved entry space is subtracted instead of added.

dosfstools/src/mkfs.fat.c

Lines 980 to 989 in 91978a2

/* The factor 2 below avoids cut-off errors for nr_fats == 1 and
* size_fat == 12
* The "2*nr_fats*size_fat/8" is for the reserved first two FAT entries
*/
clusters =
(2 *
((long long)fatdata * sector_size -
2 * nr_fats * size_fat / 8)) / (2 * ((int)bs.cluster_size *
sector_size +
nr_fats * size_fat / 8));

It could very well be that I am completely misunderstanding what is happening, I am just trying to figure out how FAT works myself.

@etlow
Copy link

etlow commented Aug 12, 2022

Nice spot! I have gone through the code and agree with you, the space for the two reserved FAT entries should be subtracted from the remaining space.

Relevant summary of FAT

I am going to summarise what I have learnt over the past few days studying FAT in case it is helpful for anyone else.

Terms

Sector - should probably be set to hard disk sector but a few other values also work e.g. 512, 4096
Cluster - allocatable unit, comprises a number of sectors, number of clusters per sector set when formatting

Structure of FAT file system

All these regions occupy an integer number of sectors. After the specified number of sectors for one region, the next region immediately begins, so these regions are back-to-back.

Reserved sectors

Can be set to an arbitrary number of sectors.

  • FAT12/16 needs at least 1 sector to accommodate the boot sector
  • FAT32 also has a backup boot sector and information sector extending up to sector 8, mkfs.fat currently sets this to 32 for FAT32 volumes so this is not a problem

FAT

The origin of the FAT name. Each entry is 12 bits for FAT12, 16 bits (2 bytes) for FAT16, and 32 bits (4 bytes) for FAT32. The first two entries are special, each subsequent entry corresponds to a cluster in the data area.
There are normally two of these FATs, but this is configurable.

Root directories

Each entry is 32 bytes. E.g. 512 entries is 32 (512 byte) sectors. The length of this region is 0 for FAT32 and the root directory is stored in data clusters.

Data

Composed of clusters storing file contents.

More information about FAT can be found online.

mkfs.fat discussion

Now we can talk about mkfs.fat.

Size determination algorithm

Definitions/initialisation

  • align(sectors) returns sectors rounded up to multiple of cluster size
  • nr_fats = number of redundant FATs
  • num_sectors = total number of sectors on volume (possibly reduced to multiple of sectors per track)

Relevant portion of algorithm

  • fatdata = num_sectors - align(reserved_sectors) - align(root_dir_sectors) space available to FAT and data regions
  • clusters = (fatdata in bytes + bytes needed for first two special FAT entries * nr_fats) / (bytes occupied by a cluster + bytes occupied by the FAT entry for this cluster * nr_fats) floored, dividing space by bytes needed by each additional cluster, this is where plus should be minus
  • fatlength = ceiling((clusters + 2) * bytes per FAT entry / sector_size) FAT length in sectors needed to describe all the above clusters + two special entries
  • fatlength = align(fatlength)
  • clusters = (fatdata - nr_fats * fatlength) / cluster size floored, calculating actual space available to form clusters after subtracting the now calculated FAT length

Plus/minus

As mentioned above, the plus should be minus. Recall that FAT consists of two special entries + subsequent entries describing clusters. The total available space for cluster data + FAT data actually describing clusters is space available to entire FAT and data region minus special entries, and not plus.

However, this does not cause mkfs.fat to produce invalid filesystems. It is actually safe for the FAT to be too big. The only thing that could happen is that some sectors at the end of the FAT is unused, and the filesystem supports a few clusters less than if it was minus.

Even though overestimating the FAT size is ok, I still went ahead and analysed when the algorithm actually makes different decisions. I have actually calculated that with the default settings for FAT12, the wrong algorithm does not actually make any different decisions.

Useless analysis of FAT length/cluster calculation for FAT12

As described in the algorithm above, the plus sign slightly overestimates the space available for the FAT and data region. This could result in the number of sectors given to a FAT being more than necessary.

The number of sectors given to a FAT is calculated in two steps: calculate clusters without worrying about sector or other alignment first, and then calculate how many actual sectors needs to be given to the FAT. The two lines are reproduced below.

  • clusters = (fatdata in bytes + bytes needed for first two special FAT entries * nr_fats) / (bytes occupied by a cluster + bytes occupied by the FAT entry for this cluster * nr_fats) floored, dividing space by bytes needed by each additional cluster, this is where plus should be minus
  • fatlength = ceiling((clusters + 2) * bytes per FAT entry / sector_size) FAT length in sectors needed to describe all the above clusters + two special entries

With reference to the full algorithm, note that a wrong value for clusters only affects computation of fatlength. If fatlength ends up to be correct, subsequent computations will also be correct as they depend on fatlength.

As mentioned above, the number of FAT sectors fatlength has to be overestimated. For FAT12, one FAT sector can contain 341 entries, corresponding to either special entries or clusters. If the current available space is enough for around 100 or 200 clusters, only one FAT sector is needed, and the small difference caused by adding or subtracting the space occupied by the special entries do not make a difference.

Thus, FAT length will only change near integer values of sectors, e.g. if number of clusters is 339, 680, etc. It is sufficient to analyse the behaviour of the code near these values.

I rewrote the algorithm in JavaScript to compare the clusters value for both plus and minus, assuming the default settings.
FAT12 is being analysed, it is assumed to be selected
Sector size: 512
Sectors per cluster: 4
Number of FATs: 2

Let i be number of FAT sectors. As mentioned above, only integer values of sectors need to be analysed.

for (var i = 1; i < 10; i++) {
    // (In sectors) Space for number of clusters that i FAT sectors accommodates + space for i FAT sectors * number of FATs + (number of FATs - 1)
    // If this was one sector more, the wrong code could calculate a FAT one sector bigger while still fitting the same amount of clusters
    var fatdata = Math.floor(512 * i / 12 * 8 - 2) * 4 + i * 2 + 1;
    var clusters = Math.floor(2 * (fatdata * 512 + 2 * 3) / (2 * 4 * 512 + 2 * 3)); // Simulate floor division as in C code
    var clustplus = 2 * (fatdata * 512 + 2 * 3) / (2 * 4 * 512 + 2 * 3); // Floating point division
    var clustminus = 2 * (fatdata * 512 - 2 * 3) / (2 * 4 * 512 + 2 * 3);
    var fatlength = Math.ceil((((clusters + 2) * 3 + 1) >> 1) / 512);
    console.log(fatdata, clustplus, clustminus, fatlength);
}

The wrong FAT length in sectors would be greater than actual. To cause this,clusters would also have to be overestimated because the mistake is in the initial calculation of clusters. The initial calculation of clusters has regular C floor division as the last step. Thus, this happens when the decimal portion of the floating point division is close to 0.99 or exceeds it, causing the division to give the correct value + 1 or more.

fatdata clustplus          clustminus        fatlength
  1359  339.25597269624575 339.2501218917601 1
  2725  680.2564602632862  680.2506094588006 2
  4095 1022.2554851292052 1022.2496343247196 3
  5461 1363.2559726962456 1363.25012189176   4
  6827 1704.2564602632863 1704.2506094588007 5
  8197 2046.2554851292052 2046.2496343247196 6
  9563 2387.255972696246  2387.25012189176   7
 10929 2728.256460263286  2728.2506094588007 8
 12299 3070.255485129205  3070.24963432472   9

As shown in the results above, there is no change in the FAT length calculated.

Also, the decimal digits seem to repeat every three FAT lengths, so I tried to simplify the expression for clusters below to find out why. Not sure if there is a more elegant way to simplify the expression, but I have done it.

Let pm be 1 if plus is used in algorithm else -1
Substituting fatdata into clusters, we get...

2 * ((Math.floor(512 * i / 12 * 8 - 2) * 4 + i * 2 + 1) * 512 + (pm) * 2 * 3) / (4 * 512 + 3);
((Math.floor(512 * i / 12 * 8 - 2) * 4 + i * 2 + 1) * 512 + pm * 2 * 3) / (4 * 512 + 3);
((Math.floor(512 * i / 3 * 2 - 2) * 4 + i * 2 + 1) * 512 + pm * 6) / (4 * 512 + 3);
((Math.floor(1024 * i / 3 - 2) * 4 + i * 2 + 1) * 512 + pm * 6) / (4 * 512 + 3);
((Math.floor(i / 3 + 341 * i - 2) * 4 + i * 2 + 1) * 512 + pm * 6) / (4 * 512 + 3);
(((Math.floor(i / 3) + 341 * i - 2) * 4 + i * 2 + 1) * 512 + pm * 6) / (4 * 512 + 3);
(((Math.floor(i / 3) + 341 * i - 2 + 0.5 * i + 0.25) * 4) * 512 + pm * 6) / (4 * 512 + 3);
((Math.floor(i / 3) + 341.5 * i - 1.75) * 4 * 512 + pm * 6) / (4 * 512 + 3);
((Math.floor(i / 3) + 341.5 * i - 1.75) * 4 * 512 + pm * 6) / (4 * 512 + 3);
((Math.floor(i / 3) + 341.5 * i - 1.75) * 4 * 512 / (4 * 512 + 3) * (4 * 512 + 3) + pm * 6) / (4 * 512 + 3);
((Math.floor(i / 3) + 341.5 * i - 1.75                                                                 ) * 2048 / 2051 * (4 * 512 + 3) + pm * 6) / (4 * 512 + 3);
((                                    Math.floor(i / 3) * 2048 + 341.5 * 2048 * i     - 1.75 * 2048           ) / 2051 * (4 * 512 + 3) + pm * 6) / (4 * 512 + 3);
((-Math.floor(i / 3) * 3            + Math.floor(i / 3) * 2051 + 341   * 2051 * i + i - 1.75 * 2051 + 1.75 * 3) / 2051 * (4 * 512 + 3) + pm * 6) / (4 * 512 + 3);
(-Math.floor(i / 3) * 3 + i + 5.25 + (Math.floor(i / 3) * 2051 + 341   * 2051 * i     - 1.75 * 2051           ) / 2051 * (4 * 512 + 3) + pm * 6) / (4 * 512 + 3);
(-Math.floor(i / 3) * 3 + i + 5.25 + (Math.floor(i / 3)        + 341          * i     - 1.75                  )        * (4 * 512 + 3) + pm * 6) / (4 * 512 + 3);
(-Math.floor(i / 3) * 3 + i + 5.25 + pm * 6) / (4 * 512 + 3) + Math.floor(i / 3) + 341 * i - 1.75;
(i % 3 - 0.75 + 6 + pm * 6) / (4 * 512 + 3) + Math.floor(i / 3) + 341 * i - 1.75;
(i % 3 - 0.75 + (plus is true) * 2 * 6) / (4 * 512 + 3) + Math.floor(i / 3) + 341 * i - 1.75;
(i % 3 - 0.75 + (plus is true) * 12) / (4 * 512 + 3) + Math.floor(i / 3) + 341 * i - 2 + 0.25;

Math.floor(i / 3) + 341 * i - 2 is an integer. Then the maximum decimal point is:

(2 - 0.75 + (plus is true) * 12) / (4 * 512 + 3) + (0) + 0.25;
(1.25 + (1) * 12) / 2051 + 0.25; // = 0.25646026328620186
(1.25 + (0) * 12) / 2051 + 0.25; // = 0.2506094588005851

which is nowhere near enough to make cluster go up by 1. Feel free to check that the decimal digits are indeed equal to the clustplus and clustminus values for fatlength of 2, 5, and 8.

Since clusters stays the same, FAT length stays the same, and plus or minus never changes the calculation for FAT12 with the default settings.

FAT length/cluster calculation for FAT16/FAT32

However, the same cannot be said for FAT16 and FAT32. I did not analyse the math in as much detail as I did for FAT12, but I modified the mkfs.fat source code to brute force every single block value for plus and minus to find cases where they are different. It seems like in all the cases below, the wrong plus version formats the disk with 2 less clusters.

The code can be found at etlow@8adc555.

The output is below. Basically, 8224 to 8239 blocks with the default settings causes the number of clusters available to be 4093 instead of 4095 if minus was used.

8224 blocks: FAT16, clusters: + 4093, - 4095
16432 blocks: FAT16, clusters: + 8189, - 8191
24640 blocks: FAT16, clusters: + 12285, - 12287
32848 blocks: FAT16, clusters: + 16381, - 16383
41056 blocks: FAT16, clusters: + 20477, - 20479
49264 blocks: FAT16, clusters: + 24573, - 24575
57472 blocks: FAT16, clusters: + 28669, - 28671
65680 blocks: FAT16, clusters: + 32765, - 32767
73888 blocks: FAT16, clusters: + 36861, - 36863
82096 blocks: FAT16, clusters: + 40957, - 40959
90304 blocks: FAT16, clusters: + 45053, - 45055
98512 blocks: FAT16, clusters: + 49149, - 49151
106720 blocks: FAT16, clusters: + 53245, - 53247
114928 blocks: FAT16, clusters: + 57341, - 57343
123136 blocks: FAT16, clusters: + 61437, - 61439
147616 blocks: FAT16, clusters: + 36861, - 36863
164016 blocks: FAT16, clusters: + 40957, - 40959
180416 blocks: FAT16, clusters: + 45053, - 45055
196816 blocks: FAT16, clusters: + 49149, - 49151
213216 blocks: FAT16, clusters: + 53245, - 53247
229616 blocks: FAT16, clusters: + 57341, - 57343
246016 blocks: FAT16, clusters: + 61437, - 61439
9085616 blocks: FAT32, clusters: + 1134589, - 1134591
9085632 blocks: FAT32, clusters: + 1134589, - 1134591
10118816 blocks: FAT32, clusters: + 1263613, - 1263615
10118832 blocks: FAT32, clusters: + 1263613, - 1263615
11152016 blocks: FAT32, clusters: + 1392637, - 1392639
11152032 blocks: FAT32, clusters: + 1392637, - 1392639
last blocks value tested: 11999984

The relevant modification I made is this for loop.

    /* Test code */
    struct params par;
    backup_params(&par);
    for (unsigned long long test_blocks = 64; test_blocks < 12000000; test_blocks += 16) {
        devinfo.size = test_blocks * BLOCK_SIZE;
        blocks = (devinfo.size - part_sector * sector_size) / BLOCK_SIZE;
        orphaned_sectors = ((devinfo.size - part_sector * sector_size) % BLOCK_SIZE) / sector_size;

        plus = TRUE;
        establish_params(&devinfo);
        setup_tables(); /* Sets align_structures so params must be restored */

        restore_params(&par);
        unsigned plus_fat = fat_entries;

        plus = FALSE;
        establish_params(&devinfo);
        setup_tables();

        if (plus_fat != fat_entries) {
            printf("%llu blocks: FAT%d, clusters: + %u, - %u\n",
                    blocks, size_fat, plus_fat - 2, fat_entries - 2);
        }
        restore_params(&par);
    }
    printf("last blocks value tested: %llu\n", blocks);

    exit(0);

The values above can be checked with the current mkfs.fat as follows.

$ mkfs.fat -v -C file.img 8239
mkfs.fat 4.2 (2021-01-31)
file.img has 2 heads and 32 sectors per track,
hidden sectors 0x0000;
logical sector size is 512,
using 0xf8 media descriptor, with 16448 sectors;
drive number 0x80;
filesystem has 2 16-bit FATs and 4 sectors per cluster.
FAT size is 20 sectors, and provides 4093 clusters.
There are 4 reserved sectors.
Root directory contains 512 slots and uses 32 sectors.
Volume ID is b8fc8c58, no volume label.

Changing the plus to minus and compiling, we get 4095 clusters as expected.

$ dosfstools/src/mkfs.fat -v -C file.img 8239
mkfs.fat 4.2+git (2021-01-31)
file.img has 2 heads and 32 sectors per track,
hidden sectors 0;
logical sector size is 512,
using 0xf8 media descriptor, with 16448 sectors;
drive number 0x80;
filesystem has 2 16-bit FATs and 4 sectors per cluster.
FAT size is 16 sectors, and provides 4095 clusters.
There are 4 reserved sectors.
Root directory contains 512 slots and uses 32 sectors.
Volume ID is df50bb2a, no volume label.

@etlow
Copy link

etlow commented Aug 12, 2022

Other proposed changes

While looking at the code, I also noticed a few other things. Not sure if I am hijacking this thread to talk about it.

Alignment to cluster boundary

In the current mkfs.fat code, all regions are aligned to cluster boundaries. However, I do not see why this has to be done. The FAT region is made up of FAT entries which are 12, 16, or 32 bits long, and the root directory region consists of directory entries which are 32 bytes long. These two regions do not contain any structures which are inherently one cluster long or even one sector long, so there is no benefit to align them. The reserved sector region should already be aligned as it is right at the start of the partition, so the only region that needs to be aligned is the data region which consists of clusters.

If I understand FAT correctly, one of the purpose of the reserved sectors region is to be used for alignment. Thus, only the number of reserved sectors has to be adjusted after knowing the length of the FAT region and root directory region. Changing the number of reserved sectors will shift the FAT region and root directory region so that the data region can start at a cluster boundary.

Alignment to sectors per track

According to the comments in the code, this is needed by DOS and mtools. However, I have removed the alignment and it still works with the current version of mtools. It also works for me on Windows 10. I don't have a DOS system convenient for testing, so I could not check that. Does anyone know of a way I can test that this does not work?

Tiny filesystem alignment disabling

The current code disables alignment if the number of sectors is below 8192. After the other alignment changes, alignment only saves at most one cluster, and alignment can still be disabled manually with the -a flag.

I have forked the code and implemented these changes, and I can store files in it with Windows 10 and mtools without any issue. I am unsure about creating an unsolicited pull request as it is a lot of changes.

The changes can be found at etlow/dosfstools@master...align.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants