12 Ocak 2021 Salı

Disk Sistemi Nedir, Ne Değildir? #4: FAT


Merhaba. Serinin bu yazısında dosya yerleşim tablosu (file allocation table), yani FAT'tan bahsedeceğim. Bu, ilk başta asıl amacımdı ama ilk yazıda söz ettiğim gibi, boot sektör olmadan FAT, MBR olmadan da boot sektör havada kalırdı.

Temel Kavramlar
FAT, diskte boot sektöründen sonra gelen ve kümelerin (cluster) boş olup olmadığını ve ardışığı kümeyi gösteren yapı olup, bir çeşit bağlı listedir. Kümeleri bir sonraki bölümde açıklayacağım.

Dizinler, kullanıcı tarafından doğrudan okunamayan ve dizin içindeki dosyaların listesinden (directory table) oluşan özel dosyalardır. Bu liste, dosya ve dizin girdilerinden (directory entry) oluşur.

Bilindiği gibi standart FAT'in, FAT12, FAT16 ve FAT32 olmak üzere türleri vardır. Bunun dışında bir çok üretici dosya girdilerindeki ayrılmış alanlarda farklı verileri (örn. dosya sahibi) tutarak FAT'i özelleştirmişlerdir. Standart olmadıklarından bunlara değinmeyeceğim. FAT sürüm numaraları, bir kümenin yerleşim tablosunda temsil edildiği bit sayısıdır ve ilginç şekilde, en karmaşık olanı FAT12'dir. Bunun dışında, exFAT, MS tarafından flash diskler için özelleştirilmiş bir FAT sürümüdür.

VFAT, özel bir sürüm olmayıp 8+3'ten uzun dosya adlarını (long file names - LFN) tutmak için mevcut sürümler üzerine bir genişlemedir. FAT normalde CP/M'den kalan mirasla sekiz karakter dosya adı ve üç karakter uzantı olmak üzere 8+3 dosya adlarını destekler. Microsoft bunu değiştirmek için bazı sihirler yapmıştır. 


Kümeler (Clusters)
Kümeler, FAT dosya sisteminin en küçük yapı taşıdır. Disk sisteminde sektör neyse dosya sisteminde de küme o'dur. Bir küme, bir veya birden fazla sektörden oluşur ve içinde dosyalardaki veriler bulunur veya elbette boş olabilir. Bir dosya da, bir veya birden fazla kümeden oluşur.

FAT ilk ortaya çıktığında (aslında FAT8 olarak ortaya çıktı) henüz küme kavramı yoktu. Sektörlerin FAT tarafından doğrudan adreslenmesi mevcut disklerde sorun olmuyordu. Kapasiteler zamanla artsa da, disk bütün olarak kullanılamadığında -kullanışsız da olsa- diski bölümleme seçeneği vardı. 

FAT12, 12 bitle, 4096 sektöre kadar kullanabiliyordu, yani en fazla 2MB (pratikte sınır daha düşük). 1984'te MSDOS 3.0, FAT16 desteğiyle geldi, öyle ki 20MB'lik sabit diskleriyle IBM PC AT'ler yeni çıkmıştı ve MS bu bilgisayarları kaybetmek istemiyordu. İlk FAT16, 16 bitle, 65 536 sektöre yani 32MB'a kadar destekleyebiliyordu. Bugün kullanılan FAT16'ysa, 1987'de Compaq DOS v3.31'le çıktı. Birim sayısı üst sınırı aynı kalmak üzere, belli sayıda sektör, küme adı altında gruplandırılacak, böylece birimler büyüyecek ve algoritmaya yalnız bir çarpan eklenerek adreslenebilir disk alanı arttırılacaktı: FAT12 disk bölümleri 16MB'a ve FAT16 disk bölümleri 2GB'a kadar. Adı geçen çarpansa önceki yazıda değindiğim "küme başına sektör" (sectors per cluster) değeridir [1].

90'ların ortasında disk kapasiteleri 2GB sınırına dayanmıştı. FAT16 da yeterli olmadığından, FAT32 geliştirildi ve adreslenebilir en büyük disk 2TB oldu. FAT32'yle birlikte küme sayısı çok arttığı için, diskteki boş alanı hesaplamak veya ilk boş kümeyi bulmak problem haline geldi, bu nedenle başka FAT dosya sistemleri muhtemelen olmayacak.

Küme sayısının fazla olması performans sorunlarına yol açar, ancak az olması da, aynı diskin daha büyük kümelerden oluşması demektir. Küme, dosya sistemindeki temel birim olduğundan, bundan küçük veya eşit her dosya bir küme kaplar. Yani 1 KB'lık bir dosya 4KB'lik kümelerin olduğu diskte 4KB yer kaplar. Benzer şekilde 5KB'lık bir dosya, diskte iki kümesiyle 8KB yer kaplayacaktır. Bu harcanan alana "slack space" adı verilir. Küçük dosyaların çok olması bu harcanan alanı arttırır.

Sözü edilen kümelere karşılık, dosya yerleşim tablosunda bir sayı bulunur. Bu sayı kümenin boş veya dolu olduğu ve doluysa dosyanın devamının nerede olduğunu tutar. Eğer bir küme, dosyanın son kümesiyse FAT'da dosya sonu imi (end of file (EOF) veya end of chain mark (EOC)) bulunur. İşletim sistemi diski okurken GÇ hatalarıyla karşılaşırsa, o sektöre ait kümeyi FAT'da bozuk küme olarak işaretler.


Dosya Yerleşim Tablosu
FAT'in ayrıntısına girmeden önce, diskteki yapıların yerleşimine göz atalım (yandaki görsel). Bildiğimiz gibi disketler ve diğer bölümlenmeyen ortamlar dışında, diskin en başında MBR bulunuyor. MBR'deki bölüm kaydı boot sektörleri işaret ediyor. FAT görüldüğü gibi hemen hemen hep iki kopya olarak tutuluyor ve başlangıcı (fat_start), önceki yazıda belirttiğim gibi, boot sektöründeki gizli ve ayrılmış sektörlerin toplamına eşit. FAT sayısı ve tabloların sektör cinsinden uzunluğu yine boot sektöründe var. Bunlar çarpılıp fat_start'a eklenince kök dizinin (mor alan) başladığı sektör bulunuyor (root_dir_start). Kök dizinin büyüklüğü, içinde kaç girdi için yer ayrıldığına bağlı. Bu değer, root_dir_start'a eklenince, 2. kümenin başlangıcı yani veri alanı (data_start) bulunuyor. Sıfırıncı ve birinci kümelerin özel anlamları var ve bunlar disk üzerinde yer almıyor. 

Bunlar önceki yazıdan bilinen şeyler. data_start bulunduktan sonra hangi kümenin hangi sektörde başladığını bulmak için, küme numarasının iki eksiği kümenin içerdiği sektör sayısıyla çarpılıyor:

clus2sect(c) = (c - 2) * sectors_per_cluster + data_start [2]

Bu arada çizim, basit olması için tek disk bölümü içeriyor ve tabii ki ölçekli değil.


FAT12
FAT12, disketlerde bugün hala kullanılmakta (disketlerin ne kadar kullandığı ayrı bir konu). Normalde 12 bitten, 4096 kümeye kadar destekliyor ancak ayrılmış küme numaraları çıkarılınca bu sınır 4078 oluyor.

FAT12'yi incelemek için, önceki yazıda oluşturduğum FreeDOS sistem disketini kullanacağım. (Hatırlatma: dd ile 1 474 560 byte'lık freedos.ima dosyasını oluşturdum. FreeDOS sanal makinasına takıp FORMAT A: /S ile formatladım). Bunu, içeriğindeki veri MSDOS disketine göre küçük olmasından ötürü seçtim. Diskette KERNEL.SYS ve COMMAND.COM olmak üzere iki dosya var. Dosya büyüklüklüklerini 512'ye (sektör başına byte*küme başına sektör değerinden bir kümenin byte cinsinden büyüklüğü) bölüp yukarı yuvarladığımda ilki 92 ve ikincisi 131 küme uzunlukta. Aşağıdaki komutla disket kalıp dosyasına bakıyorum:

hexdump -C -v freedos.ima | less

Önceki yazıda, disket için fat_start değerinin 1 olduğunu hesaplamıştım. Dolayısıyla FAT 512. byte'dan (0200h) başlıyor. FAT içeriğinin bir parçası aşağıda:

00000200  f0 ff ff 03 40 00 05 60  00 07 80 00 09 a0 00 0b  |....@..`........|
[ SNIP ]
00000280  05 57 80 05 59 a0 05 5b  c0 05 5d f0 ff 5f 00 06  |.W..Y..[..].._..|
[ SNIP ]
00000340  0d d7 80 0d d9 a0 0d db  c0 0d dd e0 0d df 00 0e  |................|
00000350  ff 0f 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
[ SNIP ]

Daha önce belirttiğim gibi her girdi 12 bit uzunlukta, yani 1.5 byte. Daha zor olamazmış gibi her girdi little endian olarak tutuluyor. n. girdiyi bulmak için n'i 1.5 ile çarpıp ondalık varsa aşağı yuvarlamak gerekiyor. Bunun kolay yolu girdiyi n + (n >> 1) işlemine sokmak:

Girdi No.0123456...n
Yeri0134679...n + (n >> 1)

Sıfırıncı girdinin düşük anlamlı sekiz biti sıfırıncı byte'da ve kalan yüksek anlamlı 4 bit, birinci byte'ın düşük anlamlı yarısında, çünkü little endian. Başka bir deyişle ilk word'un son on iki biti. Bütün çift sayılı girdiler için böyle.

Birinci girdinin düşük anlamlı 4 biti, birinci byte'ın bir önceki girdiye ait olmayan yarısında. Yani yüksek anlamlı yarısında ve kalanı bir sonraki byte'da. Bu da tüm tek sayılı girdiler için genellenebilir. Bunun kodunu vermek açıklamaktan kolay:

for(int k = 0; k < clusters; k++)    {
  cur_cluster = (k >> 1) + k;     // 1.5 ile carpma
  if(k & 1)        // tek sayili kume icin 4 kaydir
    fat_entry = *((unsigned short *) &FAT[cur_cluster]) >> 4;
  else        // cift sayili kume icin son 12 biti al
    fat_entry = *((unsigned short *) &FAT[cur_cluster]) & 0x0FFF;
  printf("%d -> %d [%X] \n", k, fat_entry, fat_entry);
}

Bu kodun bütünü github'ımda var. 

Örnek FAT bloğu için bu kodun ürettiği çıktıyla aşağıdaki listeyi hazırladım (parantez içindeki değerler offset): 

Küme 0: 0xFF0 (0x0000)
Küme 1: 0xFFF (0x0001)
Küme 2: 0x003 (0x0003)
Küme 3: 0x004 (0x0004)
Küme 4: 0x005 (0x0006)
...
Küme90: 0x05B (0x0287)
Küme91: 0x05C (0x0288)
Küme92: 0x05D (0x028A)
Küme93: 0xFFF (0x028B)
Küme94: 0x05F (0x028D)
...
Küme222: 0x0DF (0x034D)
Küme223: 0x0E0 (0x034E)
Küme224: 0xFFF (0x0350)
Küme225: 0x0 (0x0351)
Küme226: 0x0 (0x0353)
...

İlk iki girdinin özel anlamı var buna sonra geleceğim. Zaten veri ikinci kümede başlıyordu. İkinci girdinin değeri 3, yani ikinci kümedeki verinin devamı 3. kümede. Üçüncü girdinin değeri 4, yani bunun devamı dördüncü kümede... Bu zincir 93. kümeye kadar sürüyor. 93. kümede 0xFFF işareti var. Bunun anlamı, dosya veya dizi/zincir sonu, dosyanın o kümede bittiğini belirtiyor. İngilizcesi end of file (EOF) veya end of chain (EOC). Bu im yalnız dosyaların değil, dizinlerin de sonunu gösterdiğinden EOC daha doğru denilebilir ama dizinlerin de aslında dosyaları barındıran özel bir dosya olduğu gözönüne alınırsa EOF da mantıklı. Dizinleri ele alırken buna değineceğim. 0xFF8 ile 0xFFF arasındaki tüm değerler dosya sonu imi. 225. kümeden itibaren FAT sonuna kadar sıfırlar dolu. Sıfır o kümenin boş olduğu anlamına geliyor. Elbette bütün kümelere bakmak gerekiyor ancak yukarıdaki örnekte 226. kümeye kadar iki dosya olduğunu söyleyebiliriz. 

İşletim sistemi bir kümeyi okurken kümede bozuk sektör bulursa, o kümeyi 0xFF7 ile bozuk olarak işaretler ve o küme kullanılmaz. 0x001 değeri ayrılmıştır çünkü birinci küme özel olduğundan diğer kümeler onu gösteremez. 0xFF0 ile 0xFF6 arası değerler de ayrılmıştır. Geri kalan değerlerin de kümenin ardışığını göstermekte kullanıldığını gördük. İkinci kümede hangi dosyanın olduğunu, dizin tablosuna baktığımızda bulacağız.

Sıfırıncı kümede FAT ID adı verilen değer bulunur. FAT ID aslında boot sektöründeki medya tanımlayıcı byte'ın, ilgili FAT girdisi uzunluktaki halidir. Medya tanımlayıcının çoğunlukla 0xF0 (disket) veya 0xF8 (disk) olduğunu söylemiştim. O halde FAT12 disketlerde 0xFF0, disklerde 0xFF8 ve FAT16 disklerde 0xFFF8'dir. Birinci kümede her zaman dosya sonu imi bulunur.


FAT16
FAT16'da girdilerin 16 bit olması dışında hiç bir fark yoktur. Küme sayısının teorik limiti 65 536 olsa da, ayrılmış değerleri çıkarınca 65 524 kümeyi destekler.

FAT16 örneği olarak, FreeDOS sanal makinasının diskini inceleyeceğim. Bu makinada bir çok dosya olduğundan, FAT yukarıdaki örnekten daha çok EOF içeriyor. VirtualBox sanal makinalarında disk .vdi formatında. Bu formatın tanımlarına girmek bu yazının amacını aşıyor. Tek bilinmesi gereken, .vdi dosyalarda disk verisi 0x200000 offsetinde başlıyor (aslında disk verisinin göstericisi 0x158 offsetinde ancak 0x200000 değeri kodda gömülü yani tüm .vdi dosyalarda aynı). Dolayısıyla .vdi dosyayı hexdump ile açıp başlangıç offsetine kadar PageDown ile inmem gerek:

hexdump -C ~/VirtualBox\ VMs/FreeDOS/FreeDOS.vdi | less

Alternatif olarak:

dd if=~/VirtualBox\ VMs/FreeDOS/FreeDOS.vdi \
bs=512 skip=4096 | hexdump -C | less

Yukarıdaki skip değeri MBR için 4096, boot sektörü için 4159 ve FAT için 4160 olmalı. Ayrılmış sektörler 1 olduğundan, boot sektöründen hemen sonra FAT geliyor. Dikkat edilirse .vdi dosyada içi sıfır olan çok blok olduğundan, hexdump'la -v parametresini kullanmadım.

00208000  f8 ff ff ff ff ff ff ff  ff ff ff ff ff ff ff ff  |................|
00208010  ff ff 0a 00 ff ff 0c 00  0d 00 ff ff 95 0a ff ff  |................|
[ SNIP ]
00208030  ff ff ff ff ff ff 1c 00  ff ff ff ff 2a 00 ff ff  |............*...|
[ SNIP ]
0020b220  ff ff 00 00 ff ff ff ff  00 00 00 00 00 00 00 00  |................|
0020b230  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
0020b240  00 00 00 00 00 00 00 00  00 00 00 00 00 00 28 19  |..............(.|
0020b250  ff ff 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
[ SNIP ]

Girdilerin hepsi 16 bit olduğundan okumak daha kolay. Kodu da oldukça basit:

for(int k = 0; k < clusters; k++)    {
    fat_entry = *(unsigned short *)&FAT[k << 1];
    printf("%d -> %d [%X] \n", k, fat_entry, fat_entry);
}

Bu kodu yazarken .vdi dosyaları da açabilecek şekilde yazdım. Böylece bu kodla FreeDOS.vdi dosyasını da okuyabiliyorum. Yukarıdaki bloğu incelediğimde;

Küme0: 0xFFF8 (0x0000)
Küme1: 0xFFFF (0x0002)
Küme2: 0xFFFF (0x0004)
...
Küme9: 0x000A (0x0012)
Küme10: 0xFFFF (0x0014)
Küme11: 0x000C (0x0016)
Küme12: 0x000D (0x0018)
Küme13: 0xFFFF (0x001A)
Küme14: 0x0A95 (0x001C)
Küme15: 0xFFFF (0x001E)
...
Küme30: 0x002A (0x003C)
...
Küme2709: 0xFFFF (0x152A)
...
Küme6416: 0xFFFF (0x3220)
Küme6417: 0x0000 (0x3222)
Küme6418: 0xFFFF (0x3224)
...
Küme6439: 0x1928 (0x324E)
Küme6440: 0xFFFF (0x3250)
...

Sıfırıncı girdi, FAT12'deki gibi FAT ID veya medya tanımlayıcı byte, elbette 16-bit olarak. Bu bir disk olduğundan değeri 0xFFF8. Birinci kümede de yine bir dosya sonu imi var. Dosya sonu imi 0xFFF8 ile 0xFFFF arası değerler, bozuk kümeler 0xFFF7 ile işaretleniyor ve ayrılmış değerler 0xFFF0 ile 0xFFF6 arası. FAT12'deki değerlerin başında 0b1111 olan 16-bitlik hali.

Burada, küme11, 12'yi, küme12, 13'ü ve küme13 EOF'u gösterirken, ilginç şekilde küme 14, 2709'u gösteriyor. Yani bir parçası ondördüncü kümede olan dosyanın sonraki parçası 2709'da. Küme2709'a baktığımda burada EOF var. Benzer şekilde küme30, 42. kümeyi gösteriyor. Yukarıdaki örnekte görülmüyor ama, 30. kümeden sonra dizi şu şekilde ileriyor: 30->42->51->74->210->245->EOF. Küme6416'da bir dosya sonlanıyor, bir sonraki küme boş olmasına rağmen, ondan sonraki kümede yine EOF var. Sonrasındaki on dokuz küme daha boş ama 6439 ve 6440'da bir dosyanın iki kümesi bulunuyor. Peki bu düzensizliğin nedeni nedir?

Fragmentasyon
Fragmentasyonun kelime anlamı parçalanma. Yukarıdaki çıktıda iki tür fragmentasyon örneği var. İlki, bir dosyaya ait kümelerin ard arda gelmemesi sonucu olan fragmentasyon. Küme14'te ve daha belirgin örneği küme30'da görülüyor. Kümeler ard arda olmadıklarından, bu dosyaları okumak için diskin farklı silindirlerdeki farklı sektörlerin karışık olarak okunması gerek. Bu da özellikle dönen disklerde erişim süresinin uzamasına neden olur. Buna neden olan en önemli iki bileşen, disk kafalarının doğru silindir üzerine getirilmesi için gereken zaman (seek time) ve okuma kafasının, veri içeren sektörün üzerine gelmesi için plakaların dönmesi gereken süredir (rotational latency). 

İkinci tür fragmentasyon, boş kümelerin ard arda gelmemesinden kaynaklanıyor. Örneğin 6417. ve 6419. kümeler boş ama öncesinde ve sonrasında veri var. Bu da önceden yazılmış görece küçük (veya büyük ama fragmente) dosyaların silinmesiyle oluşuyor. DOS, bir dosya oluşturulacağı zaman ilk boş kümeye değil, en sondaki boş bloğun ilk boş kümesinden itibaren yazıyor (birinci türden fragmentasyonu önlemek için).


Dizinler ve Dizin Tablosu
Buraya kadar, kümelerin diskte nasıl takip edildiğini anlattım. Ancak bir dosyanın hangi kümede başladığını bulmadan veya zincirin hangi dosyaya ait olduğunu bilmeden takip etmenin anlamı yok. Bu noktada yardıma dizin tabloları yetişiyor. Önceki yazıda FreeDOS boot ederken 3 farklı değer hesaplanmıştı: fat_start, root_dir_start ve data_start. Sonra kod, KERNEL.SYS'nin nerede başladığını (ilk kümesini), root_dir_start alanındaki kayıttan bularak belleğe yüklemişti. Bu alan kök dizinin başlangıcıdır.
 
FreeDOS boot sektörü

Yukarıdaki ekran görüntüsü yardımıyla kök dizinin başlangıcını bulalım. Bir ayrılmış sektör olduğuna göre (görselde 1.) bir sonraki sektör FAT başlangıcı. İki tane FAT var (2.) ve her biri 171 sektör (3.) uzunlukta, yani FAT toplam 342 sektör. Şu anda 63. sektörde (4.) olduğuma göre kök dizin 406. sektörde başlıyor. Diskeditörle Alt+P'ye basıp 406. sektöre ilerlerim:

FreeDOS Kök Dizin Tablosu

Veya yukarıda boot sektör için dd ile baştaki 4159 bloğu atlamam gerektiğini bulmuştum, o halde hiç hesap bile yapmadan linuxta aşağıdaki komutu kullanırım:

dd if=~/VirtualBox\ VMs/FreeDOS/FreeDOS.vdi bs=512 \
skip=$((4159+1+171*2)) | hexdump -C | less

İşte bu tablo, bir (kök) dizin tablosu ve ufak farklar dikkate alınmazsa kök dizindeki dosyaların listesini içeriyor. Her girdi 32 byte ve formatı şöyle:

OffsetBüyüklükAçıklama
0x008 byteDosya adı
0x083 byteDosya uzantısı
0x0B1 byteDosya özellikleri
0x0C2 byteAyrılmış
0x0EwordMSDOS: Ayrılmış
FreeDOS: Oluşturma zamanı
0x10word
MSDOS: Ayrılmış
FreeDOS: Oluşturma tarihi
0x122 byteAyrılmış
0x142 byteAyrılmış*
0x16word
Değiştirme zamanı
0x18word
Değiştirme tarihi
0x1AwordKüme numarası
0x1CdwordDosya büyüklüğü

Dosya adı, hepsi büyük harf olmak üzere 8 karakter. Eğer dosyanın adı 8 karakterden kısaysa bu alan boşluk (0x20) karakteriyle doldurulur. Uzantı da aynı şekilde. Dosya özellikleri aşağıdaki flag bitlerinden oluşuyor:

Bit    Açıklama
0Salt okunur
1Gizli
2Sistem dosyası
3Bölüm etiketi
4Dizin
5Arşiv
6Aygıt sürücüsü
7Ayrılmış










Salt okunur, adından da anlaşılabilir. Gizli dosyalar, dir komutuyla listelenmeyen dosyalar. Sistem dosyalarıysa, işletim sistemi çalışırken disk araçlarının dokunmaması gereken dosyalar. Bölüm etiketi, boot sektördeki etiketi ezen özel bir dosya. Dizin, adından anlaşılabilir. Arşiv biti, herhangi bir dosya değiştiğinde, değiştiren program tarafından set ediliyor ve yedekleme programları bu biti aktif olan dosyaları yedekleyip biti sıfırlıyor. Aygıt sürücü dosyaları diskte bulunmuyor ama bu bit, sürücülerin bellekteki girdilerinde bulunuyor.

0x0E ve 0x10 offsetlerindeki alanlar, MS-DOS 6 ve öncesine kadar kullanılmıyor. FreeDOS ve DOS7 (Win95), dosyanın oluşturulma zamanı ve tarihini burada tutuyor. DOS6'da bu alanlar sıfır ancak örn. DOS6 sanal makinasını DSL'le açıp bir dosya oluşturursam, DSL'deki FAT sürücüsü bu alana yazdığı için sıfır olmayabilir ama DOS6, bu alanı görmezden geliyor. 0x14 offsetinde benzeri bir durum var. DSL'de oluşturduğum dosyalarda bu alan sıfırdan farklı ancak ne olduğuna dair bir dökümantasyon bulunmuyor. Dosya erişim tarihinin kopyası olabilir.

0x16 offsetindeki değiştirme zamanı bütün FAT'lerde ortak ve bu değer little endian olarak saklanıyor. Formatı şu şekilde (Sa: Saat, Dk: Dakika, Sn: Saniye olup, saniyelere 5 bit ayrılmasından dolayı çözünürlük 2 saniyedir): 

Offset 0x17
Offset 0x16
Sa4Sa3Sa2Sa1Sa0Dk5Dk4Dk3Dk2Dk1Dk0Sn4Sn3Sn2Sn1Sn0

Aynı şekilde 0x18'deki değiştirme tarihi de tüm FAT'lerde ortak ve little endian biçimindedir. Yıl değeri 1980 ile başlar. (Yi: Yıl, Gü: Gün): 

Offset 0x19Offset 0x18
Yi6Yi5Yi4Yi3Yi2Yi1Yi0Ay3Ay2Ay1Ay0Gü4Gü3Gü2Gü1Gü0

0x1A offsetindeki küme numarası, dosyanın ilk kümesinin yerini gösterir. Dosyanın ilk kümesi bulunduktan sonra devamının nasıl bulunacağını biliyoruz. Son olarak dosya büyüklüğü, dosyanın byte cinsinden büyüklüğüdür.

Dosya adının ilk karakteri 00'sa bu, dizin listesinin sonunu gösterir, yani işletim sistemi ilk karakteri 00 olan bir girdi bulursa, listeyi okumayı durdurur. Bir dosya silinirse DOS, dosya adının ilk karakterini 0x5 veya 0xE5 ile değiştirip FAT'taki küme girdilerini sıfırlar. Başka bir deyişle 0x5 veya 0xE5'le başlayan girdiler silinmiş dosyalara aittir. Eğer silinmiş dosyaya ait kümelerin üzerine başka dosya yazılmamışsa ve dosya fragmente olmamışsa, dosyaya ait girdinin ilk karakteri düzeltilerek geri alınabilir. Silinmiş dosyalara, sonraki bölümde örnek vereceğim.

Şimdi yukarıdaki ekran görüntüsünde bulunan dizin tablosunu inceleyelim:

00000000  46 52 45 45 44 4f 53 32  30 31 36 08 00 00 00 00  |FREEDOS2016.....|
00000010  00 00 00 00 00 00 e2 b8  0b 4f 00 00 00 00 00 00  |.........O......|
[ SNIP ]
00000040  46 44 4f 53 20 20 20 20  20 20 20 10 00 00 e8 b8  |FDOS       .....|
00000050  0b 4f 00 00 00 00 e8 b8  0b 4f 03 00 00 00 00 00  |.O.......O......|
00000060  4b 45 52 4e 45 4c 20 20  53 59 53 20 00 00 f3 b8  |KERNEL  SYS ....|
00000070  0b 4f 00 00 00 00 40 ad  ab 48 1e 00 5d b6 00 00  |.O....@..H..]...|
[ SNIP ]
000000e0  41 64 00 69 00 73 00 6b  00 31 00 0f 00 22 00 00  |Ad.i.s.k.1..."..|
000000f0  ff ff ff ff ff ff ff ff  ff ff 00 00 ff ff ff ff  |................|
00000100  44 49 53 4b 31 20 20 20  20 20 20 10 00 00 73 1a  |DISK1      ...s.|
00000110  0c 4f 0c 4f 00 00 73 1a  0c 4f 02 00 00 00 00 00  |.O.O..s..O......|

İlk girdi FREEDOS2.016 ve özelliklerine bakıldığında, bunun bir bölüm etiketi olduğu görülüyor. Dolayısıyla bu, özel bir girdi. Kümesi ve büyüklüğü sıfır.

İkinci girdi olan "FDOS"un, kalan 4 karakteri ve uzantısı boşluk karakteriyle doldurulmuş ve özelliklerinden dizin olduğu görülüyor. Oluşturma ve değiştirme tarihi ve saati aynı. Saat 0xB8E8 yani: 10111 000111 01000 = 23:07:32 (saniye alanı 16 ama 2 sn çözünürlükteydi o nedenle ikiyle çarpılacak). Ve tarih: 0x4F0B yani: 0100111 1000 01011 = 1980+39/8/11. Bu dizin 3. kümeden başlıyor ve uzunluğu sıfır. Şimdi tarih ve saati bir kontrol edelim:


FreeDOS boot sektörü görselinden görüleceği üzere (görselde 5.) kök dizine 512 girdi ayrılmış. Girdilerin her biri 32 byte ve bir sektör de 512 byte ise kök dizin 32 sektör uzunluktadır. Daha önce kök dizinin 406. sektörde olduğunu hesaplamıştım, o halde bu diskte data_start 438. sektördedir. Formül [2]'den ikinci küme 438. ve üçüncü küme 438+16=454. sektörden başlar. Linux'ta;

dd if=~/VirtualBox\ VMs/FreeDOS/FreeDOS.vdi bs=512 \
skip=$((4160+171*2+32+16)) | hexdump -C | less

Diskeditörle 454. sektöre gittiğimde (Alt+P ile), sektörün görünümü değişiyor. Diskeditör burada bir dizin olduğunu anlayıp dizin görünümünde gösteriyor (yanda) ve bu görünümde, yukarıda yaptığım hesaplamaları kendiliğinden yapıyor. F2 ile Hex görünümüne geçersem, dizinin ilk iki girdisinin "." (dizinin kendisi) ve ".." olduğu görülüyor. Dizinin kendisi 3. kümede ve üst dizin kök dizin olduğundan küme sıfır. Genellersem, dizinin kendisini temsil eden "." girdisi, dizinin bulunduğu kümeyi; üst dizini temsil eden ".." girdisi, üst dizinin bulunduğu kümeyi gösterir. Üst dizin kök dizinse, gösterdiği küme sıfırdır. FAT16'yı incelerken 3. kümede EOF görmüştüm, yani bu dizinin içindeki dosya ve altdizinlerin listesi bu küme içinde listeleniyor. Dizin tablosu daha çok dosya içerdiğinde, büyüyüp birden fazla kümeye yayılabilir ama ne kadar büyük olursa olsun, kendi girdisindeki dosya büyüklüğü sıfırdır. Wikipedia'da FAT'ın tasarımının anlatıldığı maddede önemli bir cümle var: "Dizin tablosu dizini temsil eden özel bir dosya türüdür". Dizinler de dosyalar gibi kümelerde saklanır ve FAT'ta girdileri vardır ancak kök dizin istisnadır. FAT12 ve FAT16'da kök dizinin FAT girdisi olmaz. Dosya sistemi hiyerarşisinden bahsedebilmek için önce kök dizin bulunmalıdır. Bu nedenle, fragmente olmayacağı, taşınmayacağı ve diskin başında olacağı, tasarımla garantilenerek kök dizinin bulunması kolaylaştırılmıştır.

Bir sonraki KERNEL.SYS girdisini incelersek, oluşturma tarihi FDOS'la aynı, yalnız saniye alanı 3 farklı yani FDOS dizininden 5-6 sn sonra oluşturulmuş. Düzenleme tarihi olan 11.05.2016, kurulum CD'sindeki dosyanın tarihi olmalı. Dosya 0x1E kümesinde (sektör 886) başlıyor ve 0xB65D=46 685 byte uzunlukta.

Bu girdinin altında, adında sıfırlar ve küçük harfler olup kümesi sıfır, büyüklüğü 4Gb olan, gizli bir bölüm etkiketi var. Adında disk1 okunuyor ve kendisinden sonra gelen geçerli girdi de aynı ada sahip, ama yukarıdaki standarda uymuyor...


Uzun Dosya Adı (LFN) Desteği
Bilindiği gibi DOS'ta dosya adları 8+3 karakterle (büyük harfler, sayılar ve bazı noktalama işaretleri) kısıtlı ancak Win95'ten itibaren uzun dosya adları FAT'le kullanılabiliyordu. Bu, VFAT olarak adlandırılan bir genişlemedir ve dosya sistemi sürücüsü destekliyorsa tüm FAT sürümlerinde kullanılabilir.

Üst bölüm yeni sürümden, alt eski
VFAT'ın hilesi, asıl girdiden önce yer alan ve geçersiz görünen girdiler içinde uzun dosya adını tutmaktır. Yukarıdaki örnekte "DISK1" uzun olmasa da, küçük harflerinden dolayı bir VFAT girdisine sahiptir. Eski sürüm diskedit bu girdileri tanımazken, yeni sürüm bunları tanıyıp dosya türünü LFN olarak belirtmektedir (yandaki görsel).

Bu iyi bir örnek olmadığından, sanal makinayı DSL ile açıp FreeDOS disketini taktım ve terminalde aşağıdaki komutları uyguladım:


Dikkat: Disket, -t vfat parametresi verilmediğinde uzun dosya adı desteği olmadan mount edilir. vi ile dosyanın içine bir kaç karakter yazıp :wq ile kaydettim ve çıktım. Aynı dosya şöyle de oluşturulabilirdi:

echo test \
> "long file name (LFN) support on FAT file system.txt"

Dosya vi ile kaydedilirken, vi geçici bir .swp dosya oluşturup bunu siliyor. Bu noktada hexdump -C freedos.ima | less komutuyla kalıp dosyasına bakınca 0x2640 ile 0x2700 offsetleri arasında herbirinin başında 0xE5 karakterleri olan (silinmiş) dosya girdileri var. Demek ki uzun adlı dosyalar silinirken bütün girdilerinin başına 0xE5 karakteri geliyor. Benim ilgilendiğim asıl dosya 0x2780 offsetindeki LONGFI~1.TXT. Hatırlayanlar vardır, Win95'teki bir çok dosya DOS altında böyle görünürdü, çünkü uzun dosya adı girdileri DOS tarafından görmezden gelinirdi. Girdilerin daha kolay okunabilmesi için sıralamasını tersine çevireceğim (başındaki offset numaralarından gerçek sıra anlaşılabilir):

00002780  4c 4f 4e 47 46 49 7e 31  54 58 54 20 00 00 25 87  |LONGFI~1TXT ..%.|
00002790  26 52 26 52 00 00 25 87  26 52 f9 00 29 00 00 00  |&R&R..%.&R..)...|
00002760  01 6c 00 6f 00 6e 00 67  00 20 00 0f 00 d4 66 00  |.l.o.n.g. ....f.|
00002770  69 00 6c 00 65 00 20 00  6e 00 00 00 61 00 6d 00  |i.l.e. .n...a.m.|
00002740  02 65 00 20 00 28 00 4c  00 46 00 0f 00 d4 4e 00  |.e. .(.L.F....N.|
00002750  29 00 20 00 73 00 75 00  70 00 00 00 70 00 6f 00  |). .s.u.p...p.o.|
00002720  03 72 00 74 00 20 00 6f  00 6e 00 0f 00 d4 20 00  |.r.t. .o.n.... .|
00002730  46 00 41 00 54 00 20 00  66 00 00 00 69 00 6c 00  |F.A.T. .f...i.l.|
00002700  44 65 00 20 00 73 00 79  00 73 00 0f 00 d4 74 00  |De. .s.y.s....t.|
00002710  65 00 6d 00 2e 00 74 00  78 00 00 00 74 00 00 00  |e.m...t.x...t...|

Geçerli olan tek kayıt, asıl kayıt (0x2780). Dosya büyüklüğü ve küme numarası gibi değerler bu kayıtta. Uzun dosya adı girdilerinin ilk karakteri bir sıra numarası ve offset küçüldükçe artıyor. Girdi dizisinin sonuncusunda, ilk karakterin 6. bitinin bir olması gerek (örn. 0x2700'deki 0x44, dördüncü ve son girdi). Sıra numarasından sonra 16-bitlik 5 karakter var. Bu karakterler unicode'a çok benzeyen UCS karakter setinde. Böylelikle dosya adlarında Çince veya Kiril karakterler kullanmak mümkün. Normal bir girdide dosya özelliklerine karşılık gelen 11. byte her zaman 0x0F, yani gizli+sistem+salt-okunur+disk etiketi. Diskte birden fazla disk etiketinin olması hatalı, üstüne diğer özelliklerin de bulunması bu kaydı geçersiz kılarak işletim sistemi tarafından görmezden gelinmesine neden oluyor.

Uzun dosya adı girdilerinin 12. byte'ı her zaman sıfır ve 13. byte'da bir checksum bulunuyor. Bunun algoritması wikipedia'da VFAT long file names başlığında var. 14. byte'dan 25. byte'a kadar olan 12 byte, 6 karakter daha içeriyor. 26. byte'da bulunan küme numarası, bu girdilerde her zaman 0 ve kalan son 4 byte'da 2 karakter olmak üzere her girdi 13 karakter içeriyor. Dosya adları sıfır sonlandırmalı string'ler ve sonuncu girdideki karakterler 13 byte'dan azsa, kalan alanlar 0xFF ile dolduruluyor. Yukarıda dosya alanı tam dolduğundan görülmüyor ama DISK1 örneğinde 14. byte'da sıfır karakteri ve sonrasında 0xFF'ler açıkça görülebilir.

Bu yazıyla birlikte disk sistemi serisinin ana konularını tamamlamış oldum. FAT32 bu yazıya sığmadı. FAT32'de kök dizin mantığı tümüyle değiştiği için onu ayrı bir yazıda ele almak daha doğru. Bunun dışında GPT, NTFS gibi konularda da ileride ek yazılar yayınlayacağım.

[1]: https://en.wikipedia.org/wiki/File_Allocation_Table