11 Ağustos 2019 Pazar

Disk Sistemi Nedir, Ne Değildir? #2: MBR


Merhaba. Önceki yazıda disk donanımına değinmiştim. Bu yazı biraz daha yazılım ağırlıklı olacak ama kesmeler ve GÇ portlarıyla ilgili bir yazı ne kadar donanımdan bağımsız olabilirse o kadar. Diskin en temel veri yapısından başlayalım.

Master Boot Record (MBR)
MBR, sabit disklerin ilk sektöründe bulunan, disk bölümlerini (partition) tutan ve bir miktar da kod içeren bir yapı. Bilgisayar açılırken BIOS devreye girer ve işi bittiğinde CMOS'taki boot sequence ayarlarına göre depolama cihazlarının hangisi erişilebilirse onun ilk sektörünü belleğe okur. 1980'lerden beri bu süreç pek değişmedi. Boot edilen ortam disket sürücüyse, okunan boot sektör; sabit diskse MBR'ydi. BIOS için boot sektörüyle MBR'nin farkı yoktur. BIOS, boot etmeye uygun ilk sürücünün birinci sektörünü belleğin 0000:7C00 adresine kopyalar ve işletim sisteminin yüklenmesi için buraya bir far jmp yapar. 1980'lerden bahsettiğimden ve gerçel mod (real mode) adresleme kullandığımdan açıktır ki henüz korumalı modla ilgimiz yok.

MBR'de bulunan kod, hangi disk bölümünün boot etmeye uygun olduğunu saptar, bu bölümün ilk sektörünü alıp belleğe yükler ve oraya dallanır. MBR, disk bölümlerini bir tabloda tutar.

Bu yazıda MBR'yi okumak için Windows'ta HxD adlı editörü kullandım. HxD'yi yönetici olarak çalıştırıp "Open Disk" (Ctrl + Shift + D) dedim ve fiziksel disklerden birini seçtim. "Open as Readonly" olmasını tavsiye ederim.

Linux kullanıcıları daha şanslı çünkü aşağıdaki komut yeterli:

sudo dd if=/dev/sda count=1 bs=512 | hexdump -C -v

Assembly öğrenmeye çalışırken 3-4 ay uğraşıp MBR'yi görüntüleyebilmek benim için büyük bir başarı gibi gelmişti. Sene bindokuzyüz...


HxD'den aldığım ekran görüntüsü yukarıda. Görüntüyü ben renklendirdim. Kırmızı alan kod, mavi alan disk bölümleme tablosu ve yeşil alan MBR imzası. Sektör doğru olsa bile BIOS 0x55 0xAA'yı görmeden boot etmez. Peki neden 0x55 0xAA? Çünkü (0101 0101 1010 1010)2.

Benzer çıktı linux'ta şöyle:


Hem Windows hem Linux'ta GRUB olduğundan yukarıdaki çıktılar GRUB MBR kodu içeriyor. Kodu incelemeden önce, mavi işaretlediğim bölümleme tablosuna bakalım. CHS ve LBA'ya önceki yazıda değindim. Önceki yazıda debug.exe ile yaptığım örnekte de kod ve imza kolayca görülebiliyor.


Disk Bölümleme Tablosu
Bölümleme tablosu, MBR'nin 01BEh offsetinden başlayan 4x16 byte'lık alandır. Her birincil (primary) disk bölümü 16 byte uzunlukta bir kayıtla ifade edilir. Genişletilmiş (extended) disk bölümlerini şimdilik konu dışında tutarsam, bir önceki ifadeye göre 4'ten fazla (birincil) disk bölümü oluşturulamaz. Aşağıdaki tabloda kayıtların yapısı görülüyor:

OffsetBüyüklükAçıklama
0x00byteBootable Flag
0x013 byteBaşlangıç CHS adresi
0x04byteBölümün türü
0x053 byteBitiş CHS adresi
0x08dwordBaşlangıç LBA adresi
0x0CdwordBölümdeki sektör sayısı

Bootable Flag, açılışta o disk bölümünden boot edileceğini gösteriyor. Her MBR'de bir ve yalnız bir tane bulunması gerekir. Bu değer 00h ise bölüm boot edilebilir değildir. 80h, boot edilebilir olduğunu gösterir ancak bazı MBR kodları yalnız yedinci bite bakar. [01h, 7Fh] arası değerler geçersizdir.

CHS adresi kısmını çok kısa atlıyorum.

Bölüm türü alanı, bölümdeki dosya sistemini belirtse de bunun her zaman önemi yoktur. İşletim sistemi ve yükleyiciler bunu çoğunlukla dikkate almaz. Bu değerin bir standardı yoktur. 80'lerde Microsoft ve IBM tarafından kullanılan bazı değerler vardı. minix ve linux yazılırken o zamana kadar kullanılmamış değerler rastgele seçildi ve sonradan de facto standartlaştı. Bununla ilgili ayrıntılı bilgi ve liste Wikipedia'da var: https://en.wikipedia.org/wiki/Partition_type

Başlangıç LBA adresi, başlangıç CHS adresinin LBA'ya çevrilmesiyle elde edilen değerdir. Bölümdeki sektör sayısının görevi adından da anlaşılabilir. Bu değer 512 byte'la çarpılınca disk bölümünün büyüklüğü bulunur ama bu biçimlendirilebilir (formatlanabilir) alan değil ham alandır. İkisinin farkına boot sektörüne geçince değineceğim.

CHS adresleri karışık bir biçimde tutulur. Üç byte'lık alanın ilk byte'ı kafa numarasıdır. İkinci byte'ın düşük anlamlı altı biti sektör numarasını tutar. Üçüncü byte silindir numarasının düşük anlamlı sekiz bitidir. İkinci byte'ın yüksek anlamlı iki biti de silindir numarasının yüksek anlamlı iki bitini içerir. Aşağıda bunu görselleştirmeye çalıştım:

1. Byte 2. Byte 3. Byte
H7H6 H5H4 H3H2 H1H0 C9C8 S5S4 S3S2 S1S0 C7C6 C5C4 C3C2 C1C0

Peki bu tanıdık geliyor mu? Int 13h! 1. Byte DH'ya, 2. Byte CL'ye ve 3. Byte CH'ye. Birbirine mükemmel bir şekilde uyuyor.

Önceki yazıda CHS, 8 GB üstü disklerde yetersiz yazmıştım:
Yaygara yapmadan daha iyimser bir yaklaşımla; MBR kodunun, disk bölümünün bitiş CHS adresiyle ilgilenmediğini, boot sektörünü yüklemek için koda yalnız başlangıç CHS adresinin yeteceğini varsayarak, yalnızca diskin başındaki 8 GB içinde boot edilebilir bölümler oluşturulabilir sonucuna varılır. CHS'yle 8 GB'ın ötesindeki bölümlerden boot edilemez!

Peki 8 GB'ın ötesinde bölüm nasıl oluşturulacak? Her kayıtta 32-bit'lik bir başlangıç LBA adresi var. Eğer bootloader CHS yerine LBA'ya bakarsa; LBA, 232 * 512 byte = 2 TB desteklediğinden; diskin başındaki 2 TB alanda bulunan bölümlerden boot edilebilir. Üstelik sektör sayısı da 32 bit olduğundan 2 TB'lik bir disk bölümü bu sınırın sonuna yerleştirilerek teoride 4 TB'ye kadar olan disklerde MBR kullanılabilir. Teoride diyorum çünkü eski MBR kodlarının bu uç değerlerle sorunsuz çalışacağının garantisi yok.

Önceki yazıda bulunan debug.exe'li ekran görüntüsünü aşağıda tekrar ekledim. Buradaki byte'ları anlamaya çalışalım.


80 | 01 01 00 | 06 | 0F 3F FF | 00 00 00 C1 | EF 03 00 00
00 | 00 41 00 | 06 | 0F 7F FF | 00 F0 03 00 | 00 F0 03 00

Yukarıda iki disk bölümü var. Birinci bölüm boot edilebilir. Her iki bölüm de FAT16 olarak işaretlenmiş. Birinci bölümün CHS adresi (0, 1, 1) - (255, 15, 63), 63. sektörden başlıyor ve 257 985 sektör uzunlukta. İkinci bölümün CHS adresi (256, 0, 1) - (511, 15, 63), 258 048. sektörden başlıyor ve 258 048 sektör uzunlukta.

Bu tablo neden önemli? Kendi işletim sistemimi yazıyor olsam hiçbir önemi yok. Bunu dikkate almayıp kendim daha iyi bir tablo oluşturacağım diyebilirim ama diğer tüm işletim sistemi üreticileri bu yapı üzerinde anlaştığından kendi tablo yapımı kullanırsam diğer işletim sistemleriyle uyumlu çalışamam.

Bölümleme Tablosuna da LBA'sına da Lanet Olsun...
Evet, hala kod yazmaya geçemedim ama bu bölümde artık örneklere başlayacağım. Önce deneme için iki sanal makina oluşturdum. İlkine önceki yazıdaki örnek için VirtualBox'ta DOS6.22 kurmuştum. Bunun için 32 MB RAM ve 500 MB sabit disk yeterli. Bu makinada bir tane ağ kartı var, bunu host-only yaptım (Settings -> Network -> Enable Network Adapter, Attached to: Host-only Adapter). DOS disket imajları internetten bulunabilir. Bir de Damn Small Linux (DSL) iso'su kullandım. Ubuntu, Knoppix vb. live dağıtımlar 32 MB RAM'le çalışmıyor. DSL 50MB'lık bir iso'ya sahip. Artık geliştirilmediğinden buradan veya şuradan indirilebilir. DSL'in en yakın alternatifi TinyCore Linux, iso olarak 14MB ama en az 64 MB RAM'le çalışıyor.

Makinayı DOS disketiyle açtıktan sonra kurulum, bir disk bölümü oluşturuyor ama ben kurulumdan çıkıp fdisk'le elle bir bölüm oluşturdum. fdisk'te:

1 - 1 - N - 25% - Esc


Bunu yaptıktan sonra ekrandaki uyarı hiçbir bölümün aktif olmadığını belirtiyor. Yani şu anda hiçbir bölüm kaydının ilk byte'ı 80h değil. 2 - 1 - Esc ile bu uyarı kayboldu. Ardından tekrar bir birincil bölüm oluşturmak istediğimde hata aldım. Maalesef fdisk birden fazla birincil bölüm oluşturamıyor. Genişletilmiş bölüm oluşturulabilir ama buna yazının sonunda değineceğim. Esc'e basıp ana menüden çıktım ve disketi çıkarıp makinayı DSL ile açtım. Bu arada, disk bölümleri oluşturulduktan veya silindikten sonra, DOS yeniden başlatılmazsa yeni disk bölümlerini göremez.

DSL'in terminalinde sudo fdisk -l diyip oluşturduğum bölümü gördüm. Bir disk bölümü daha ekledim:

sudo fdisk /dev/hda
n - p - 2 - (Enter) - 512
t - 2 - 6
p
w

Bilgisayarı tekrar DOS disketiyle açınca C:'yi ve D:'yi bulup formatladı. Standart bir Microsoft kurulumu, ardarda Enter'a basarak ve arada disketleri değiştirerek tamamlandı. Bu arada, son disketi sürücüde unutup "Non-system disk error" alanlara uyarı: Eskiden MBR/boot sektör virüsü bulaşmış disketlerle bilgisayarı açınca, boot sırasında virüs kendini belleğe yazıp aktifleşir, bu hatayı gösterir ve kullanıcının bir tuşa basmasını beklerdi. Bundan korunmak için bir tuşa basmak yerine bilgisayarı yeniden başlatmak gerekirdi. Virüsü temizlemek için de fdisk /mbr komutuyla sabit diskin MBR kodu yeniden yazılırdı.

DOS kurulduktan sonra debug.exe ile MBR'ye tekrar baktım:

Makinayı bir daha DSL'le açtım. Ekranın sağında makinanın IP'si görünmeli. IP'yi, hosttan, sanal makinaya dosya kopyalarken kullanacağım. Benim hostum da linux olduğundan scp ile dosya kopyalayabiliyorum. Windows kullanıcıları sanal makinada DSL -> System -> Daemons -> ssh -> start ile sshd'yi başlatıp WinSCP kullanmak zorunda (Cygwin de iş görebilir).

Kopyalayacağım dosya Norton Utilities paketindeki disk editor. DISKEDIT.EXE adında iki farklı sürümü var. Biri Norton Utilities v8.0 (1994) paketinden diğeri de Norton Utilities 2000 paketinden. İlk sürüm CHS'yi desteklerken, ikinci sürümün dosyası daha büyük ve LBA'yı destekliyor. Disk editörle çalışmak çok pratik, bu nedenle bu dosyayı internetten bulabilenler kendilerini şanslı saymalı.

Disk editörü kopyalayıp bilgisayarı yeniden başlattım ve DOS'u açtım. Editörü başlatıp Alt+D ile sürücüleri görüntüledim. Tür (type) olarak "Physical Disk" seçip sabit diski açtığımda sektörleri HxD gibi gösterdi. Ardından "View as Partition Table" (F6) seçtiğimde yukarıda elle hesaplamak zorunda kaldığım bütün bilgiler göründü. Disk editör daha sonra boot sektörü ve FAT için de işe yarayacak. Bu yüzden diskte durmasında yarar var.

Norton Utilities'in iso'su olanların, DOS'ta CD sürücüyü tanıtmaları gerekiyor. Okurlardan DOS kullanmamış olanlar, CD sürücü tanıtmanın gerekliliğine hayret edebilir ama o zaman öyleydi. Bu işlemi ayrı bir yazıda anlatıp, buraya bağlantısını vereceğim.

Ekran görüntüsüne göre ikinci disk bölümü 511. silindirin sonunda bitiyor. Alt+I'ye basıp çıkan menüde "Drive Info"yu seçince diskin özelliklerini de gösterdi (yanda).

Drive Number: 80 Hex (Birinci fiziksel disk)
Int 13x: Yes (BIOS, LBA destekliyor)
Sides: 16 (Kafalar)
Tracks: 1015 (Silindirler)
Sectors/track: 63 (Silindir başına sektör sayısı)
Total Sectors: 1 024 000 (LBA adresi)

Bu bilgiler Int 13h/08 alt fonksiyonuyla da alınabilir:

Silindir sayısı 3F6h, sektör sayısı 3Fh, kafa sayısı 0Fh. Yazmaçlardaki değerlerin anlamları Ralf Brown'un belgelerinde var (Int13h/08). Yukarıda DL=02 oldu çünkü test için makinaya bir disk daha takmıştım.

Şimdi, diskin rastgele bir sektörüne "denizyildizi" yazacağım (tamamen rastgele bir kelime). Örn. CHS 600,0,1'e yazayım. Bunun LBA karşılığı 604 800 (CHS'den LBA'ya dönüşüm formülü bir önceki yazıda var). Disk editörü açıp Alt+P'ye bastım ve çıkan diyalogda LBA adresini yazdım. Diske yazabilmek için Tools -> Configuration altında Read-Only'nin yanındaki işareti kaldırıp OK'a bastım. Tab'la sağa geçince heksadesimal yerine metin yazılıyor. Alt+X'e ve sonra değişiklikler diske yazılsın mı diyalogunda "yaz"a bastım.

Aynısını debug.exe'yle yapmak için ufak bir kod yazdım ve Int13h/43h altfonsiyonunu kullandım. 42h altfonksiyonuyla birlikte ikisi Int13h genişlemeleri olarak geçiyor.

mov cx,000C      ; 12 karakter
mov si,0120      ; 0120h'dan
mov di,0200      ; 0200h'ya
repz movsb       ; kopyala
xor ax,ax
mov cl,FA        ; 0FAh word daha
repz stosw       ; kopyala
inc si           ; sonraki buffer DAP
push ds
pop ax           ; DS'i al DAP'daki alana
mov word ptr [si+06],ax ; yaz
mov ax,4300
mov dl,80
int 13           ; int 13h'u cagir
int 3
db 00
db 'denizyildizi',0
db 10 00 01 00 00 02 00 00 80 3A 09 00 00 00 00 00

Son satır DAP (Disk Address Packet) adlı veri yapısı. Sırasıyla, 10h DAP uzunluğu, 00 ayrılmış, 00 01 okunacak/yazılacak sektör sayısı, (0000:0200)16 kullanılacak buffer. Segmentin 0000 olması yanıltmasın, kod oraya kendi segmentini yazacak. (09 3A 80)16 da LBA adresi (qword). Bunu g komutuyla çalıştırdım ve kod belirttiğim sektöre veriyi yazdı. Yazdığım veriyi yine LBA'yla ama denetleyicinin portlarını kullanarak okuyacağım. Önceki yazıda kullandığım kodu küçük farklarla yeniden yazdım (kolaylık için yine ofsetleri ekledim):

0100    mov ax,0001
0103    mov dx,01F2
0106    out dx,al   ; Sektor sayisi = 1
0107    inc dx      ; dx = 01F3
0108    mov al,80
010A    out dx,al   ; LBA adres1 = 80h
010B    inc dx      ; dx = 01F4
010C    mov al,3A
010E    out dx,al   ; LBA adres2 = 03A
010F    inc dx      ; dx = 01F5
0110    mov al,09
0112    out dx,al   ; LBA adres3 = 09h
0113    inc dx      ; dx = 01F6
0114    mov al,E0   ; LBA modu, master disk
0116    out dx,al   ; LBA adres4 = 0h
0117    inc dx      ; dx = 1F7
0118    mov al,20
011A    out dx,al   ; 20h ATA Sektor okuma komutu
011B    in al,dx    ; Durum yazmacini oku
011C    test al,58  ; 0101 1000: Drive ready | Seek complete | Buffer ready
011E    jz 011B     ; Dataya erisene kadar bekle
0120    mov dx,01F0
0123    mov bx,0200
0126    in ax,dx    ; Datayi oku (word)
0127    mov [bx],ax ; Bellege kopyala
0129    inc bx
012A    inc bx
012B    cmp bx,0400 ; 0200h byte
012F    jnz 0126
0131    int 3       ; Breakpoint

Tekrar d 0200 komutuyla kontrol ettiğimde yazdığım veriyi doğru olarak okudum.


Mantıksal Disk Bölümleri
Bu konuya başlamadan önce VirtualBox'ta 32 MB RAM ve 1GB diskli ikinci makinayı oluşturdum (aynı şekilde host only adapter ile). FreeDOS.org'dan CD iso'sunu indirip makinaya taktım. Legacy ile CD arasında yalnız boot modunda fark var, içerikte bir fark yok. Elle disk bölümleme için "Install to harddisk" dedim, sonra dili seçip "No - Return to DOS"u seçtim ve fdisk'i açtım. FreeDOS'un fdisk'i DOS6.22'den farklı olarak FAT32 desteğiyle geliyor. Fat32, 1996'da ortaya çıktı ve Win95 OSR2 ile kullanıma girdi. Ayrıntısına sonraki yazılarda gireceğim. Bu kısımla ilgili not: fdisk, bölüm oluşturulurken diskin LBA desteğini test eder. LBA desteklemeyen disklerde bölüm türü byte'ı FAT16 ve FAT32 için sırasıyla 06h ve 0Bh olurken, destekleyenlerde 07h ve 0Ch olur.

Yukarıdaki görselde görülen FAT32 desteği açılsın mı sorusu aslında 2GB'den büyük diskler için gerekli, "N" diyip geçtim. Bir tane 340MB birincil bölümünü "1 - 1 - N - 340 - (Enter) - (Esc)" kombinasyonuyla oluşturdum ve ana menüdeki (2) seçeneğiyle, bölümü aktifleştirdim. Öncekinden farklı olarak "1 - 2 - (Enter) - (Esc)" tuş kombinasyonuyla uzatılmış veya mantıksal (extended veya logical) bir bölüm oluşturdum. fdisk, uzatılmış bölümde mantıksal sürücü oluşturmak isteyip istemediğimi soracak. Buna 340 MB verip Enter'a bastım. Geri kalan alanda son bir disk oluşturmak için tekrar Enter'a bastım. Burada bir hatadan ötürü fdisk hala 2MB alan var zannediyor ve Enter'a basılırsa bölümleme tablosu hatalı yazılıyor. Bu nedenle Esc'e bastım.


fdisk'in dördüncü seçeneğine girdiğimde diskler yukarıdaki gibiydi. fdisk'ten çıkıp bilgisayarı yeniden başlattım ve kuruluma devam ettim. "Do you want to format your drive?"a Yes dedim ve "Full installation"ı seçip kurulumu başlattım. Bitince de CD'yi çıkarıp makinayı yeniden başlattım.

Şu anda FreeDOS'ta 3 sabit disk sürücüsü (C:, D: ve E:) olmasına rağmen diskeditörle veya debug.exe'yle MBR'de iki disk bölümü görüyorum. O halde üçüncü nerede? (Diskeditör varsa buna da kopyalayın)

Diskeditör'le baktığımda diskte 520 silindir görünüyordü. (bu bölümde eski sürüm diskeditörü kullandım, bir nedeni yok). İlk bölüm 173. silindirde bitiyor. O halde birincil bölüm doğru. İkinci bölüm EXTEND olarak görünüyor. İkinci bölümün olduğu CHS (173,0,1) adresinde başka bir bölümleme tablosu daha var. EXTEND seçiliyken Enter'a basınca, o sektör bölümleme tablosu biçiminde gösterilecek.

Bu bölümleme tablosu öncekine benziyor ama aynısı değil. Sağ alt köşede (173,0,1) görüyorum yani MBR'da değilim. F2'yle Hex görünümüne geçtiğimde bu sektörde kod yok ve imza var! Bu tablodaki ilk girdi (173,1,1)'den 346. silindire kadar bir bölüm ve ikinci girdi onun bittiği yerde tekrar bir uzatılmış (EXTEND) bölüm. Bu 'Extend'i seçip Enter'a basınca (346,0,1) sektöründe kodu bulunmayan bir bölümleme tablosu daha görünüyor ve tablodaki bölüm, (346,1,1)'den diskin sonuna kadar. Anlaşılacağı üzere uzatılmış bölüm, içindeki bölümleri bağlı listede tutan bir yapı.

Bildiğim kadarıyla, uzatılmış bölümlerin boot edilebilir olmaması için teoride bir engel yok ama MBR'deki kodun, ilk tabloda aktif bir bölüm yoksa uzatılmış bölümler içinde aktif olanı araması gerek. Bu da 512 byte'la sınırlı olması gereken kodun uzaması anlamına geliyor. Standardı belirleyen mi istememiş yoksa başka bir neden mi var, emin değilim. Bu konuda Wikipedia'nın Extended Boot Record maddesinde başka örnekler bulunabilir.


MBR Kodu Nasıl Çalışır?
MBR kodunu incelemek için şimdiye kadar kurduğum iki makinayı da DSL ile açıp aşağıdaki komutla MBR'lerini kaydettim:

dd if=/dev/hda of=mbr.bin count=1 bs=512

Güncel linux dağıtımı olan bir makinada aynı komut;

dd if=/dev/sda of=mbr.bin count=1 bs=512

Sonra linux bir makinada;
objdump -M intel -D -b binary -m i8086 --adjust-vma=0x7C00 mbr.bin > mbr.asm
komutuyla bu çıktıları disassemble ettim.

Yazıyı uzatmamak için assembly kodunu satır satır incelemek yerine DOS6.22 ve FreeDOS'un çıktılarını açıklama satırlarıyla Google Drive'a yükledim. Genel olarak yapılan ikisinde de aynı: Stack ayarlamasından sonra sektör kendini DOS'ta 0:0600h ve FreeDOS'ta 1FE0h:7C00h adresine kopyalıyor. Çünkü sonradan yüklenecek boot sektör kodunun da 7C00h adresine yerleşmesi gerek. BIOS için boot sektör ve MBR kodlarının farkının olmadığını, BIOS'un görevinin boot edilecek ortamın ilk sektörünü 0:7C00h'a okuyup, sonundaki 55h AAh byte'larını kontrol etmek ve bu adrese dallanmak olduğunu yazmıştım. Eğer boot edilen ortam disketse MBR bulunmadığından 0:7C00h'a boot sektör okunur.

DOS'ta bir ve yalnız bir 80h girdisi olup olmadığı kontrol edilirken, FreeDOS'ta en yüksek değerli biti 1 olan ilk girdiye ait boot sektör okunuyor. Sektör okunamadıysa "read error" veriyor. Bundan başka herhangi bir kontrol yok.

Disk okuma konusunda DOS, bölümleme tablosundan CHS değerlerini aldığı sektörü, DI'de tuttuğu bir sayacı kullanarak beş kere okumaya çalışıyor. Beşinci sefer okunamazsa "Error loading OS" hatasını veriyor. DOS'ta LBA desteği bulunmazken, FreeDOS önce LBA desteğini kontrol ediyor. Destekleniyorsa 7CCDh'da tuttuğu DAP'la sektörü okuyor. Desteklenmiyorsa DOS'taki kodun hemen hemen aynısı çalışıyor. Sektör okunduktan sonra imza kontrolü yapılıyor (FreeDOS'ta bu, fonksiyonun dönüşünde) ve program 0:7C00h adresine atlıyor.

Aslında bu konunun devamında GRUB, GPT ve UEFI'yi ele almak gerekiyor ama UEFI benim de çok ayrıntılı bilmediğim bir teknoloji olduğundan bunları sonraki bir yazıda toparlayacağım. Bu yazıda anlattıklarım her ne kadar 1980'lerin teknolojisi olsa da, UEFI kullanılmadığında bugün de çalıştırılan kod, ufak farklar dışında aynı. Buraya kadar MBR'i bitirmiş olduk. Sonraki yazıda MBR'den sonraki adım olan boot sektörünü ele alacağım.

6 Mayıs 2019 Pazartesi

Disk Sistemi Nedir, Ne Değildir? #1: Diskler


Merhaba. Bir süredir disk sistemi ve dosya yerleşim tablosuyla (FAT) ilgili yazmayı düşünüyordum ama boot sektörüne değinmeden FAT'ten bahsetmek, MBR'ye ve daha da önemlisi donanıma değinmeden de boot sektöründen konuşmak anlamlı gelmediği için konuyu en baştan ele almaya karar verdim. En baştan demişken BIOS'un nerede bitip MBR'nin nerede başladığından da bahsedeceğim ama BIOS'u derinlemesine ele almayacağım (belki başka yazıda).


Diski Adreslemek: CHS, LBA ve Int 13h
Diskte Silindir Kafalar
ve Sektörler
CHS, Cylinder, Head ve Sector kelimelerinin baş harflerinden oluşan bir kısaltma. Çok eski (1970'lerden) disklerle birlikte standartlaşmış ve yapı farklılaşsa da geriye doğru uyumluluktan ötürü kullanılmaya devam edilmiş. Yanda dönen bir diskin iç yapısı görülüyor. Bu disk 4 plaktan (platter) oluşmuş ve erişim için 8 kafa (head) var. Her plakta iç içe halkalar var. Bunlar silindirler. Silindire "track" de deniyor ama ben silindiri kullanacağım. Son olarak, her silindir belli açıda dilimlere bölündüğünde, dilimlerin silindiri kestiği alanlara sektör deniyor. Bu sektörler 512 byte büyüklükte. Günümüzde 4KB'lik sektörler kullanılıyor ama disk denetleyici uyumluluk yüzünden işletim sistemine 512 byte raporluyor. Şimdilik 1980'lerde olduğumuzu varsayalım (keşke). CHS adreslemede 3 tane sayı gerekiyor. Dikkat edilmesi gereken silindir ve kafa numaraları sıfırdan başlarken sektör numaraları her zaman birden başlamalıdır.

Aşağıda daha açıklayıcı bulduğum bir görsel var:

https://superuser.com/questions/974581/chs-to-lba-mapping-disk-storage
Cluster (küme) birkaç sektörden oluşan bir yapı. Dosya sisteminin en küçük birimine deniyor. Bu yazıda değinmeyeceğim çünkü henüz donanımı tanımlıyorum, dosya sistemine gelemedim. Bazı kaynaklarda sektör yerine kullanıldığını gördüm ancak kesinlikle yanlış. Bir küme bir sektöre eşit olabilir (örn. disketlerde) ama zorunda değildir. 

5 MB'lık IBM disk
forklift'le taşınıyor.
Kaynak: thenextweb.com
CHS'nin basit ama kullanışsız bir mantığı var. Dikkatli okuyucu dış silindirlerdeki sektörlerin içtekilere göre büyük olacağını fark etmiştir. CHS, dış sektörlerin verimsiz kullanılmasına yol açıyor. 1970'lerde kullanılan bir kaç on MB'lık ve bir kaç onbin dolar fiyatlı disklerde yapı böyleymiş ve bunlara CAV (constant angular velocity - sabit açısal hız) diskler deniyor. Açısal hız sabit olduğundan kafa her sektör üzerinden aynı sürede geçiyor. Yeri gelmişken, CD ve DVD'lerin CLA (constant linear velocity - sabit doğrusal hız) olduklarını belirteyim. CD'yi okumaya başladığında yavaş döner sona doğru dış sektörlerde hızlanırdı. 1990'larda (hatta 80'lerin sonunda) disklerde zone bit recording teknolojisi geliştiriliyor. ZBR'de geniş dış silindirler fazla sayıda sektöre bölünmüşken iç silindirlerde daha az sektör var. Geriye doğru uyumluluk için denetleyici işletim sisteminden CHS adreslerini alıyor ve arka planda CHS'yi fiziksel sektöre dönüştüren bir formül uyguluyor.

Bu arada hızlıca bir flashback'le günümüze gelelim. SSD'lerde artık ne CHS ne de CAV/ZBR'den söz edilebilir. Dolayısıyla CHS'nin modasının ne kadar geçtiği anlaşılabilir.

LBA adreslemede yalnız doğrusal sektör numarası veriliyor. Örn. 1.44MB'lik High Density (HD) disketlerde 18 silindir, 2 kafa ve 80 sektör vardı (Double Density'lerde (DD) 9 silindir). Son sektörün CHS adresi (17, 1, 80) iken LBA adresi 2879'dur (18 * 2 * 80 = 2880, LBA'de ilk sektör sıfır).

c, silindir; h, kafa ve s, sektör numarası olsun. Nhead, diskteki kafa sayısı ve NSPC bir silindirdeki sektör sayısı olmak üzere CHS'den LBA'ya dönüşüm aşağıdaki formülle yapılır:

LBA(c, h, s) = (c * Nhead + h) * NSPC + (s - 1)    [ 1 ]

LBA'nın bir avantajı aşağıdaki gibi karmaşık yapıda sektörleri olan disklere izin vermesi ama tek avantajı bu değil.

Kaynak: https://venam.nixers.net/blog/unix/2017/11/05/unix-filesystem.html

Int 13h, BIOS'un disk erişimi için sunduğu bir kesme servis rutini (ISR). Bu ISR, 80'lerde CHS kullanıyordu ve büyüyen diskler karşısında zamanla yetersiz kalmaya başlamıştı. Int 13h'ün bir çok fonksiyonu var ama bu yazıda ben 02 ve 03'e yoğunlaşacağım. 02, diskten belleğe okumaya, 03 bellekten diske yazmaya yarıyor. Kesme çağırılmadan önce giriş değerleri aşağıdaki gibi olmalı:

AH = 02h/03h (okuma/yazma)
AL = Okunacak/yazılacak sektör sayısı ( >0 )
CH = Silindir numarasının düşük 8-biti
CL = Sektör numarası 1-63 (bit 0 .. 5) +
Silindir numarasının yüksek 2-biti (bit 6 .. 7)
DH = Kafa numarası
DL = Sürücü numarası: Sabit diskler için 7. bit 1 olmalı.
Örn. Birinci disket sürücü: 00h, ikinci disket sürücü: 01h
Birinci sabit disk: 80h, ikinci sabit disk: 81h
ES:BX: Okunacak yazılacak veri için gösterici


Int 13h için bir örnek hazırladım. debug.exe, Windows XP'de var ama o bile bu kadar düşük seviye bir işleme izin vermiyor. Bunun için bir sanal makinaya DOS6.22 kurdum (sanırım zamanında İTÜ yazılım sunucusundan indirmiştim). Kurulumu sonraki yazıda ele alacağım. Şimdilik yalnız ekran görüntülerini ekledim:

debug.exe ile disk okumak
Yukarıda diskin ilk sektörünü okudum. Okunan byte'ların anlamına sonraki yazıda değineceğim. Bu arada yukarıda, sektörün başı ve sonu olmak üzere iki ekran görüntüsü var.

Int 13h'ü kullanmak istemeseydim, erişimi IDE denetleyicisinin portlarına erişerek yapmam gerekecekti. Yazının devamında bununla ilgili bir örnek vereceğim.

Şimdi bir hesap yapalım. CHS adresleme için toplam 3 byte ayrılmış: CH/CL/DH. 8+6+10=24 bit. 512 byte * 224 = 8 GB. Üstelik sektör numarası 0 olamadığından aslında bundan daha az: 218 * 63 * 512 byte = 7.875 GB. Yani CHS, 8 GB'dan büyük diskleri adreslemede yetersiz! LBA'nın adresleme konusunda da avantajı var ama bunlara girmeden önce ATA standartlarına bakmamız gerek.


(Paralel) ATA Standardı
Yukarıda disk erişimine BIOS tarafından baktık. O tarafta işlerin karışık olması yetmezmiş gibi donanım tarafı da karışık. 1986'da Western Digital ilk IDE/ATA standardını duyurduğunda adresleme için 22-bit ayrılmıştı: yanlış bilmiyorsam 10-bit silindir, 4-bit kafa ve 8-bit sektör. 1994'te EIDE/ATA-2 standardında silindir 16-bit oldu ve adresleme 28-bit'e çıktı. Üstelik CHS'nin yanısıra 28-bit LBA adresler de destekleniyordu. Ancak IBM, BIOS kesmeleri ve diğer (MBR) standartları oluşturmuştu ve buna göre (örn. int 13h) silindir 10-bit, kafa 8-bit ve sektör 6-bit'di. Ortak bölenler alındığında CHS, 10+4+6 = 20-bit ve sektör başına 512 byte'la 512 MB'ye kadar olan diskleri destekleyebiliyordu. Aslında sektör numaraları da birden başladığından bundan daha da az.*

Standartları değiştirmek geriye doğru uyumluluk nedeniyle imkansızdı. Neyse ki o yıllarda LBA'nın temelleri atılıyordu ve IBM'in standardındaki bir saçmalık bu soruna geçici bir çözüm sağladı: IBM disk kafalarına 8-bit ayırmıştı ama 3.5 inç form faktörlü 1 inç yüksekliğindeki sabit diske 128 plak veya 256 kafanın nasıl sığdırılabileceğini düşünmemişti. Bunun pratikte imkansız olması sayesinde 512 MB'den büyük disklerin silindir sayısını 10-bit'le sınırlamak için işletim sistemine diskte varolandan fazla sayıda kafa raporlanıyordu. Int 13h kodunda bu sayılar [1] formülünde yerine koyulup LBA'ya dönüştürülerek erişiliyordu, elbette BIOS'ta da LBA'nın aktifleştirilmesi koşuluyla.**

*: Bu paragraf için ayrıntılı bilgi:
https://en.wikipedia.org/wiki/Logical_block_addressing#Enhanced_BIOS
https://en.wikipedia.org/wiki/Parallel_ATA

**: Bu paragraf için ayrıntılı bilgi:
https://en.wikipedia.org/wiki/Logical_block_addressing#LBA-assisted_translation


Örnek:
2 GB'lik bir diskin fiziksel yapısında 8 kafa, 8320 silindir ve her silindirde 63 sektör olduğunu varsayalım. İşletim sistemi, int 13h'le disk parametrelerini sorguladığında dönecek değer 128 kafa, 520 silindir ve 63 sektör olacak. Çarpım değişmedi. Aynı işletim sistemi, diske GÇ portları üzerinden doğrudan erişmek isteseydi 16'dan büyük kafa numaralarını porta nasıl göndereceğini bilemeyecekti.

Bu diskte CHS 100,17,17 adresine erişilmek istendiğini düşünelim. [1]'deki formülde değerler yerine konulduğunda:

LBA = (100 * 128 + 17) * 63 + (17 - 1) = 807 487 sektörüne erişilecek.

1990'larda ATA-2 standardı 28-bit'le 128GB'a kadar diskleri destekleyebiliyor ama IBM'in mevcut arabirimi LBA dönüşümüyle 8 GB'a kadar diskleri destekleyebiliyordu (IBM, MBR'de LBA için 32-bit ayırmıştı. Gelecek yazıda değineceğim). 1990'ların ortasında IBM ve Microsoft, 42h ve 43h gibi fonksiyonlarla int 13h'ün yeteneklerini genişlettiler. Diğer yandan MBR'deki LBA kaydı 32-bit olduğundan ATA standardının da ilerlemek için alanı vardı. 2003'te ATA-6 standardıyla disklerde fiziksel adresleme 48-bit'e çıktı ve aynı zamanda CHS adresleme tarihe karıştı.


BIOS Diske Nasıl Erişir? Disk Denetleyicisi
IDE/ATA disk denetleyicisinin portlarıyla ilgili dökümantasyon Ralf Brown Interrupt List'in D bölümünde ports.b dosyasında bulunabilir. Eskiden anakartlarda iki EIDE disk denetleyicisi oluyordu. Bunlara da master/slave olmak üzere ikişer disk bağlanabiliyordu. Denetleyicilerin taban (base) adresi sırayla 01F0h ve 0170h'ydi. Portlar şu şekilde:

Port | GÇ | Aciklama
-----+----+----------------------------------------------------
01F0 | RW | Veri yazmacı
01F1 | R- | Hata yazmacı++
01F1 | -W | (Write Precompensation Cylinder divided by 4)+
01F2 | RW | Sektör Sayısı
01F3 | RW | Sektör Numarası (CHS)
     |    | 0-7. adres bitleri (LBA)
01F4 | RW | Silindir numarası düşük anlamlı byte (CHS)
     |    | 15-8. adres bitleri (LBA)
01F5 | RW | Silindir numarası yüksek anlamlı byte (CHS)
     |    | 23-16. adres bitleri (LBA)
01F6 | RW | Sürücü ve kafa numarası
     |    | 27-24. adres bitleri (LBA)
01F7 | R- | Durum yazmacı++
01F7 | -W | Komut yazmacı++


+: Bu değerin yeni disklerde anlamı yok. Sıfır olabilir. Ne olduğunu açıklayacağım.
++: Bu yazmaçlardaki bitlerin anlamları için Ralf Brown Interrupt Listesi'ndeki tabloya bakınız.

01F6h göründüğünden biraz daha karışık. Geriye doğru uyumluluk nedeniyle 5 ve 7. bitler her zaman 1 olmak zorunda (bunlar sektörü 512 byte olmayan MFM disklerde kullanılıyordu). 6. bit sıfırsa adres CHS, birse LBA olarak işlem görür. 4. bit sıfırsa işlem master, birse slave diske aittir. 3-0. bitler adres bitleridir. Buraya kadarki bilgi aslında kodu yazmak için yeterli ama henüz elimizde kodu yazabileceğimiz bir yer yok. İsteyen okur bir DOS veya FreeDOS sanal makina kurup kendi deneyebilir.

Okumak kolay olsun diye satırın başına offset adreslerini ve solda açıklamaları yazdım. Kodu debug.exe'ye a (Assemble) komutuyla girmek gerekiyor.

0100    mov ax,0001
0103    mov dx,01F2
0106    out dx,al    ; Sektor sayisi = 1
0107    inc dx       ; dx = 01F3
0108    out dx,al    ; Sektor numarasi = 1
0109    inc dx       ; dx = 01F4
010A    dec ax       ; ax = 0000
010B    out dx,al    ; Silindir D = 0
010C    inc dx       ; dx = 01F5
010D    out dx,al    ; Silindir Y = 0
010E    inc dx       ; dx = 01F6
010F    mov al,A0    ; CHS modu, master disk
0111    out dx,al
0112    inc dx       ; dx = 1F7
0113    mov al,20
0115    out dx,al    ; 20h ATA Sektor okuma komutu
0116    in al,dx     ; Durum yazmacini oku
0117    test al,58   ; 0101 1000: Drive ready | Seek complete | Buffer ready
0119    jz 0116      ; Dataya erisene kadar bekle
011B    mov dx,01F0
011E    mov bx,0200
0121    in ax,dx     ; Datayi oku (word)
0122    mov [bx],ax  ; Bellege kopyala
0124    inc bx
0125    inc bx
0126    cmp bx,0400  ; 0200h byte
012A    jnz 0121
012C    int 3        ; Breakpoint


Kodu çalıştırmak için g (Go) komutunu verip, d (Dump) ile belleği kontrol ettiğimde sorunsuz (int 13h çıktısıyla aynı) okunduğunu görüyorum:


Bu kod debugger'da çalıştırmak için uygun. Sondaki int 3, int 20 ile değiştirildikten sonra aşağıdaki komutlarla kaydedilebilir:

rbx
0000
rcx
002E
nreadmbr.com
w

Kodun ekrana çıktı vermediği unutulmamalıdır. Yüklemek için dosya adı, ya debug.exe'ye parametre olarak verilir veya n komutuyla girilir ve l (Load) komutuyla yüklenir. LBA kodunu bir sonraki yazıya bırakıyorum.


Biraz Daha Teori
Write Precompensation Cylinder (WPC): Yazının başlarında ZBR olmayan CHS disklerde dış sektörlerin daha büyük alanı olduğunu yazdım. Bu disklerde üretici bir silindir belirler ve bundan dıştaki silindirlere yazarken yazma kodlamasını biraz değiştirmesi gerekir. Bu kodlama değişimine "write precompensation" adı verilir. Bu silindiri BIOS mu raporlar yoksa kullanıcı mı girer bilmiyorum. Böyle bir diskim olmadı.

Çok eski disklerde kafalar bir step motora bağlıydı. Step motorların sıcaklık hassasiyeti düşük olduğundan "voice coil motor" adında bir teknoloji kullanılmaya başlandı. Aşağıdaki video nasıl çalıştığına dair bir fikir verecektir:

Bu motorlar, uygulanan akım oranında kafayı sürüklüyor, akım kesildiğinde de bir yayla, kafa otomatik olarak boş bir alana taşınıyordu.

Landing Zone (LZ): Step motorlu disklerde bilgisayar kapatılmadan önce kafaların LZ adı verilen, içinde data olmayan bir silindirin üstüne taşınması yani "park" edilmesi gerekirdi. Kafaları park edilmemiş diskler hareket ettirilirse, kafa veri silindirleri üzerinde hareket ederek diski çizebilirdi. Int 13h'ün 19h altrutini bu park etme işlemini yapar. Günümüzde park etme gereksiz, dolayısıyla LZ de anlamsızdır.

Voice coil motorlar, step motorlardan daha az hassas oldukları için üreticiler şöyle bir çözüm buldular. Örn. 3 plakalı bir diskin bir yüzüne kafaların daha hassas yer belirlemesi için belli işaretler koydular. En üstteki kafa bu işaretlere göre yerini belirleyip gerekiyorsa geri beslemeyle konumunu düzeltiyordu. Böylelikle ilginç bir şekilde tek sayıda kafası olan diskler ortaya çıktı.

Kaynak: https://www.brainbell.com/tutors/A+/Hardware/_Actuator_Arms.htm

Donanımla şimdilik işim bitti. Sonraki yazıda artık MBR'den söz edeceğim.

10 Mart 2019 Pazar

Streaming SIMD Extensions (SSE) ve glibc


Merhaba. Bu yazı, önceki kayar noktalı sayılarla ilgili yazıların kamera arkası gibi olacak. Kayar noktalı sayılarla ilgili yazıyı hazırlarken ilginç bir sorun yaşadım. Oracle Blog'daki kodu derleyip assembly çıktısına baktığımda gcc'nin herhangi bir parametre verilmediğinde kayar nokta işlemlerini SSE komutlarıyla derlediğini farkettim. (Platform: Linux Mint 17.3, Centos 6.x ve 7.x)

...
40054d:  f2 0f 10 45 e8        movsd xmm0,QWORD PTR [rbp-0x18]
400552:  bf 18 06 40 00        mov edi,0x400618
400557:  b8 01 00 00 00        mov eax,0x1
40055c:  e8 af fe ff ff        call 400410 <printf@plt>
400561:  f2 0f 10 45 f8        movsd xmm0,QWORD PTR [rbp-0x8]
400566:  f2 0f 10 0d b2 00 00  movsd xmm1,QWORD PTR [rip+0xb2]
                               # 400620 <_IO_stdin_used+0x10>
40056d:  00
40056e:  f2 0f 5e c1           divsd xmm0,xmm1

Yukarıdaki kod parçası printf ve bölme komutlarını içeriyor.

Bu kodu FPU'da çalışmaya zorlamak için önce -mno-sse derleyici parametresini denediğimde aşağıdaki kodu üretti:

...
40054d:  bf 08 06 40 00      mov    edi,0x400608
400552:  b8 00 00 00 00      mov    eax,0x0
400557:  e8 b4 fe ff ff      call    400410 <printf@plt>
40055c:  dd 45 f8            fld QWORD PTR [rbp-0x8]
40055f:  dd 05 ab 00 00 00   fld QWORD PTR [rip+0xab]
                             # 400610 <_IO_stdin_used+0x10>
400565:  de f9               fdivrp st(1),st

Bu aynı kodun assembly çıktısı ilginç biçimde çıktı olarak 4.940656e-324 dışında birşey üretmiyor. Bu arada çıktıyı kısaltmak için double d = 1.0; ifadesini double d = pow(2.0, -1000); olarak değiştirdim. 1075 satır çıktı üreten kod 75 satır çıktı üretmeye başladı.

-mno-sse'li veya -mno-sse'siz çıktının satır sayısı değişmiyor ve uygulama sonlanıyor. Yani sorun hesaplamada değil ama ekrana yanlış yazılıyor. İki kod parçası karşılaştırıldığında, ilkinde sonuç xmm0'a yazılıyor. edi'de "%e\n"in göstericisi, eax'te printf'e giren ekstra argüman sayısı var. İkinci çıktıyla karşılaştırdığımda, -mno-sse yüzünden xmm0 yazmacına erişilemediği için bu komut yok bu nedenle de eax'te 0 var. Bunun C karşılığı printf("%e\n"); ve başka argüman olmadığından muhtemelen xmm0'de kalmış çöp değer ekrana yazılıyor. Bu arada gdb ile kontrol ettiğimde divsd komutu doğru değerlerle çalışıyor. Bu da hatanın printf'te olduğunu ispatlıyor.

Yukarıdaki kod parçalarını Mint'den aldım (glibc v2.19) ama CentOS'da da fark yok. Denemeleri CentOS sanal makinalarda yaptım. Sanırım xmm yazmaçları 64-bit olduklarından, 32-bit kodlarda böyle bir sorun yok. İstenirse SSE komutları 32-bit kodlarda kullanılmak üzere zorlanabiliyor: https://stackoverflow.com/questions/24386541/gcc-wont-use-sse-in-32bit

CentOS 6.x, glibc 2.12 kullanıyor, CentOS 5.6 ve 5.9'la da denediğimde (glibc v2.5) aynı durum tekrarlandı. 64-bitlik ilk CentOS 3.3, glibc v2.3.2 kullanıyor. Bununla denediğimde sanırım gcc (v3.2.3) sürümündeki bir hatadan ötürü -mno-sse'nin bir etkisi olmadı. Kod çıktısında sonuç yine xmm0'da printf'e gidiyor gibiydi.

Öyle anlaşılıyor ki, glibc derlenirken printf işlevinde SSE kullanılmış ve ondalıklı sayıların printf fonksiyonuna mutlaka xmm0'da girmesi gerekiyor. Bu nedenle -mno-sse parametresi kesinlikle kullanılmamalı. İşlemleri FPU'ya zorlamak için -mno-sse yerine -mfpmath=387 kullanıldığında istenen sonuç elde ediliyor.

Öte yandan önceki yazıda Oracle Blog'da söz edilen -fns parametresinin gcc'de bulunmadığından söz etmiştim. gcc'de aynı işi yapan -ffast-math parametresi bulunuyor. Bu parametre altı ayrı flag'i içeriyor: -fno-math-errno, -funsafe-math-optimizations, -ffinite-math-only, -fno-rounding-math, -fno-signaling-nans ve -fcx-limited-range (man gcc). Bunlardan subnormal sayılar üzerinde etkisi olan -funsafe-math-optimizations parametresi ve bununla derlenen kodda fazladan aşağıdaki kod parçası var:

0000000000400440 <set_fast_math>:
  400440:  0f ae 5c 24 fc        stmxcsr DWORD PTR [rsp-0x4]
  400445:  81 4c 24 fc 40 80 00  or DWORD PTR [rsp-0x4],0x8040
  40044c:  00
  40044d:  0f ae 54 24 fc        ldmxcsr DWORD PTR [rsp-0x4]
  400452:  c3                    ret

MXCSR, SSE komutlarının çalışma şeklini kontrol etmek için kullanılan, FPU Control Word (FCW) benzeri bir yazmaç*. Bu yazmacın 15. ve 6. bitleri yani "Flush to Zero (FZ)" ve "Denormals are Zero (DAZ)" bayraklarına bir veriliyor. Bu parametreyle derlenen kod çalıştırıldığında ekrandaki son çıktı normal double'ların en küçüğü 2.225074e-308 oluyor. MXCSR bir SSE yazmacı olduğundan -ffast-math'in 80387 komutlarına etkisi yok.

80387'de subnormaller için aynı mekanizma bulunmuyor. Önceki yazıda FCW'de subnormallerle ilgili DE biti olduğunu yazmıştım. Bu kullanılarak subnormallerle yapılan işlemler kontrol edilebilir. Kodda FCW'ye erişmek için şu işlevi ekledim:

void setfst()   {
    short FPUCW;
    asm("fstcw %0": "=m" (FPUCW));
    FPUCW = FPUCW & ~0x02;
    asm("fldcw %0":: "m" (FPUCW));
}

Veya diğer seçenek:

void setfst2()   {
    short FPUCW;
    asm("fstcw %0\n\t"
        "and  %1,0xFFFD\n\t"
        "fstcw %1"
        : "=m" (FPUCW): "m" (FPUCW));
}

Bu fonksiyon while döngüsünden hemen önce çağırılmalı ve kod -mfpmath=387 -masm=intel parametreleriyle derlenmeli. Kod çalıştırıldığında en son 2.225074e-308 yazıyor ve ardından "Floating point exception"la sonlanıyor. DE bayrağının elle sıfırlanması subnormal sayılarla yapılan işlemlerin exception oluşturmasına neden oluyor. Bu exception için handler yazılıp; subnormal sayıda yapılan işlem, exception'a neden olduğunda sonuç sıfıra yuvarlanırsa, SSE'deki DAZ bayrağının görevi FPU'ya uyarlanmış olur.

Ek Kaynaklar:
* http://softpixel.com/~cwright/programming/simd/sse.php
* https://software.intel.com/en-us/articles/x87-and-sse-floating-point-assists-in-ia-32-flush-to-zero...

18 Ocak 2019 Cuma

Kayar Noktalı Sayılar ve Kesme Hataları #2


Merhaba. Bir önceki yazıda ondalıklı sayıların bilgisayarda nasıl tutulduğuna değindim ve bilgisayar mimarisine ait kısıtların ilginç davranışlara neden olduğundan bahsettim. Bu yazıda ilginç davranışlara birkaç örnek vereceğim.

Bu ilginçliklerin ilki, bir önceki yazıda söz ettiğim nedenlerden ötürü özellikle sıfıra yakın sonuçları sıfırla karşılaştırmak yerine sıfıra kabul edilebilir uzaklıkta olup olmadığına bakma gereksinimidir. Bununla ilgili bir örneğe bazı ara konulara değindikten sonra yanında yıldız imiyle (*) yer vereceğim.

Önceki yazıda verdiğim örnekler çoğunlukla tek değerlikli (single) sayılarlaydı. Buradan, çift değerlikli (double) sayılarla hesaplamada sorunlar olmadığı anlamı çıkmamalı. Double'da katsayı daha çok bitle temsil edildiğinden (önceki yazıda) hesaplamadaki hata onyedinci ondalıkta ortaya çıkıyordu. Single için hata dokuzuncu ondalıktaydı. Önceki yazının ilk örneğinde Excel'deki sonuçta onaltıncı ondalıkta hata var (hata on kat büyüdü). Ben örneklerde tek duyarlıklı sayıları, hesaplamalar ve hataları göstermek kolay olsun diye seçtim.

Hatalar yalnız sıfıra yakın sayılarda ortaya çıkmaz. Katsayı bitlerinin sınırlı olması büyük sayılarda da hataya neden olur. Katsayı için 23 bit ayrılmış olması 23'ten fazla sayıda kesirle ifade edilebilen sayılarda sorun çıkarır. Hexpert'te bir deney yaptım: 00H ofsetine gidip FP32 hücresine 16 777 215, 04H ofsetine 16 777 216 vb. 16 777 220'ye kadar olan sayıları girdim.


İlk sayıda tüm katsayı bitlerini tükettim. 16 777 215, 23 kesrin toplamı olarak yazılıyor (yukarıda). Ardışığı, 1*224 olarak ifade ediliyor. Ama üs o kadar büyüdü ki 224+1'in 1'ini yazacak bit kalmadı. Dikkat edilirse bir fazlası olsa bile 04H'daki sayıyla 08H'daki sayı ve 0CH'daki sayıyla 10H'daki sayılar aynı. 224 ile 225 arasındaki sayılar bilgisayarda tek değerlikli olarak saklanırken tek sayılar yazılamıyor. Benzer şekilde 225 ile 226 arasındaki sayılarda dördün katı olmayanlar yazılamıyor. Yeterince büyük sayılardaki küçük artımların anlamsız olduğu durumda sorun yok ama problemine göre 224 çok büyük bir sayı olmayabilir. Bu arada 224'ü 32-bit'lik tamsayıyla (integer) hala kesin bir şekilde gösterebilirim.

Sonuçta bitlerin sonlu olmasından ötürü sayılar belli bir bitte kesilmektedir. Bu konudaki hata örnekleri çoğaltılabilir. Örn. π ve e gibi rasyonel sayılar kümesinde olmayan, sonlu sayıda ondalıkla gösterilemeyen sayılar hiçbir zaman bilgisayarda tam olarak ifade edilemez. Üstelik sayılamaz sonsuzlukta olan gerçel sayılar kümesinin, sonlu sayıda bitle gösterilmesi teoride zaten olanaksızdır. Bunun pratiğe dökülebilmesinin tek nedeni mühendislik hesaplarındaki sonuçların yuvarlandığında "yeterince" tutarlı olmasıdır. Bu tür hatalara kesme veya yuvarlama hatası denir. Sayısal analiz'de bu iki hata aynı değilse de mikroişlemcinin davranışı bu tür sayılarda yuvarlama yapmak olduğundan bu anlamda birbirlerinin yerine kullanılabilirler.

Kayar noktalı sayıları karşılaştırırken sıfır değil sıfıra yeterince yakın olmasıyla ilgili olarak bir örnek ele alalım*. sin(30) = sin(π / 6) = 0.5 olduğunu biliyoruz, doğal olarak sin(π / 6) - 0.5 = 0 olur. printf("%e\n", sin( M_PI / 6.0 )); ekrana 5.000000e-01 yazar. Yalnız yukarıda yazdığım gibi pi sayısında bir yaklaşıklık var. Sinüs fonksiyonunda da MacLaurin serisinin kesilmesinden dolayı bir yaklaşıklık olmalı. Bunun sonucunda hata birikmesinden ötürü gördüğümüz 5.000000e-01'in aslında 2-1 olmadığı tahmin edilebilir. printf("%e\n", sin( M_PI / 6.0 ) - 0.5); ifadesi ekrana -5.551115e-17 yazar. Farkın sıfır olması değil sıfıra "yeterince" yakın olması dikkate alınmalıdır. Örneğin sonucun sıfıra olan uzaklığının, makina epsilonu gibi yeterince küçük bir sayı kadar olup olmadığına bakılabilir. Yani:

if(fabs(sin(M_PI / 6.0) - 0.5) < DBL_EPSILON)

doğru ifadedir. Makina epsilonunun 1 komşuluğunda anlamlı olduğunu yazmıştım. Bu nedenle aslında bu yaklaşım da yeterince doğru değildir. Yapılmak istenen işlemin hassaslığı yanında makina epsilonu büyük kalabilir. Çift duyarlıklı sayılarda en küçük 2.225 * 10-308 (aslında 4.94 * 10-324 ama bu da ileride açıklamak üzere kenarda dursun**) olduğu halde epsilon 10-16 mertebesindedir. Doğru "yeterince küçük" değeri seçmek, bu değerin sayı doğrusunun her yerinde farklı olması nedeniyle zor bir sorundur. Örneğin, 224 ile 225 arasındaki sayılarda 3 yeterince küçükken 225 ile 226 arası sayılarda değildir.

Üssün 00h ve 0FFh olmasının özel anlamları vardır. Üs 0FFh ve katsayı sıfırsa işaret bitine göre sayı, pozitif veya negatif sonsuz değerini alır. Çok büyük sayıların çok küçük sayılara bölünmesiyle çıkacak sonuç, yazmaca sığmıyorsa sonsuz değeri döner. Üs 0FFh ve katsayı sıfırdan farklıysa bu NaN yani "Not a Number" değeridir. Bu da matematikteki belirsiz işlemlerin sonucunda (Örn. 0/0, 00, ∞ - ∞) veya FPU'da yapılmak istenen işlem tanımsızsa ortaya çıkar ve NaN'la yapılan bütün işlemlerin sonucu da NaN'dır.

Üssün sıfır olması ikinci özel durumdur. Üs sıfır ve katsayı da sıfırsa (yani 00 00 00 00) sayı sıfırdır ama üssün sıfır olması durumu bundan daha karışıktır. Şöyle ki, daha önce her katsayıya 1 eklendiğini söylemiştim. Katsayı her zaman 1'e eşit veya 1'den büyükse hiçbir zaman sıfır elde edilemez. O nedenle üs sıfır olduğunda kural bozulur ve üsten 127 çıkarılmaz. Bunun yerine üs -126 alınıp katsayıya 1 eklenmesi kuralı bozulur yani d0 = 0 olur (d0'ın ne olduğu bir önceki yazıda bulunabilir). Üssün -126 olduğu sayılarla çıkartma yapılırken aradaki fark 2-127 mertebesinde olacaktır:

1.5 * 2-126 - 1.375 * 2-126 = 0.125 * 2-126 = 1.25 * 2-127

Yukarıdaki işlemin taşmaya (aslında doğru terim overflow değil underflow) neden olmaması, aradaki farkın bilgisayarda gösterilebilmesi için küçük sayılara ihtiyaç vardır. Bu nedenle [0, 1] aralığı ±2-126 için ondalıklı sayı standardına eklenmiştir. Bu tür sayılara subnormal sayılar denir (normalize olmayan). Subnormal sayılar üzerinde işlem yapılırken FPU, DE bayrağına 1 verir. Bunun anlamı artık DEnormal (subnormal) sayılar üzerinde işlem yapıldığı ve en küçük değerli ondalıkların kaybolabileceğidir. Başka bir deyişle işlemci, subnormal sayıların fark ve bölüm işlemlerinde sonucun kesinliğini garanti edemez. Yukarıda ** olarak işaretlediğim cümlede, çift duyarlıklı sayılarda iki farklı en küçük değer yazmıştım. Büyük olan normal sayıların en küçüğü, diğeri de subnormal sayıların en küçüğüdür.

Örnek Kod:
Subnormal sayılarda işlemler için şu kodu yazdım:

#include<stdio.h>
#include<stdint.h>
union uSayi {
    unsigned char bayt[8];
    uint64_t q;
    double s;
};

int main()      {
    union uSayi d1, d2, d3;
    double t1, t2, t3;

    d1.q = 0x0018000000000000;
    d2.q = 0x0016000000000000;
    d3.q = 0x0014000000000000;
    //printf("%e %e %e\n", d1.s, d2.s, d3.s);

    // 1.5 * 2^-1022 - 1.25 * 2^-1022 = 0.25 * 2^-1022
    t1 = d1.s - d3.s;
    // 1.5 * 2^-1022 - 1.375 * 2^-1022 = 0.125 * 2^-1022
    t2 = d1.s - d2.s;
    // 0.25 * 2^-1022 - 0.125 * 2^-1022 = 0.125 * 2^-1022
    t3 = t1 - t2;

    return 0;
}

Bir union'la, double değere bir işaretsiz tamsayı veya 8 byte olarak eriştim. d1, d2 ve d3 olarak union uSayi türünde üç değişken tanımlayıp normal ondalıklı sayı sınırının en altına yakın değerler verdim ve birbirlerinden çıkarttım. Ara sonuçları t1, t2 ve t3 değişkenlerinde tuttum. Kodu debugger'la inceleyeceğim için aşağıdaki gibi derledim:

gcc -mfpmath=387 -g kod2.c
gdb -tui a.out

gcc, ondalıklı sayıları normalde SSE komutlarıyla işler. Ben FPU'yu görmek istediğim için 387 komutlarına zorladım.

Programı gdb'nin TUI'siyle açtım. assembly'de intel yazımını tercih ettiğim için şu iki komutu verdim:

(gdb) set disassembly-flavor intel
(gdb) layout asm


Yukarıdaki ekran görüntüsünde d1, d2 ve d3'e yapılan atamalar 0x400574 adresinden itibaren açıkça görülebiliyor. İlk çıkarma işlemi 0x4005a4'te, ikincisi 0x4005af'de, son çıkarma da 0x4005b7 adresindeki fsub ile yapılıyor. break *main+71 komutuyla ilk çıkarmaya bir breakpoint koyup run komutuyla kodu çalıştırdım. Programın durduğu yerde info float komutuyla FPU stack'e baktım. Elle girdiğim sayılar FPU'da işleme hazır :


nexti komutunu verip çıkarmayı gerçekleştirdim. Tekrar info float yazıp stack'te çıkartmanın sonucuna baktım. Diğer çıkartma da aynı şekilde gerçekleşecek. Bu nedenle until *main+90 komutuyla son çıkarmaya ilerledim. Bu adımda info float yazınca FPU Status Word'de önceden 0x3800 olan değer 0x3802 oldu. DE yani Denormalized bayrağı bir olarak ayarlandı. nexti komutuyla son çıkartmayı da yaptım. Sonuç +2,781342323134001729e-309 bulundu. Son olarak cont komutuyla programı tamamladım. Dökümanlarda ve Wikipedia'da, komutların denormalized değerlerle, normal değerlerle olduğundan daha yavaş çalıştığı belirtiliyor. Bu nedenle hızın, sonuçların kesinliğinden daha önemli olduğu uygulamalarda denormalize değerlerin kullanımın kapatılması öneriliyor.

https://blogs.oracle.com/d/subnormal-numbers adresinde derleyicinin -fns parametresinin, çalışma sırasında oluşacak subnormal sonuçları sıfıra yuvarlamasını sağlayacağından söz edilmiş ancak ben gcc'de böyle bir parametre bulamadım.


5 Ocak 2019 Cumartesi

Kayar Noktalı Sayılar ve Kesme Hataları


Merhaba. Bu konuda uzun zamandır yazmak istiyordum ama zaman bulamıyordum. Konu, geniş olduğundan kafamda toparlamak ve yazmak uzun zaman aldı. Aynı nedenden yazıyı iki parça olarak böldüm. Bu yazılarda tek değerlikli (single precision) sayılar üzerinden bilgisayarda ondalıklı (floating point) sayıların nasıl tutulduğunu ve bunun ne tür hatalara yol açtığını anlatacağım. Öncesinde iki ilginç ekran görüntüsü ekledim. İlki Excel 2007'den ama daha güncel sürümlerde de benzer sonuçlar elde edilebilir:


A sütununda formüllerin sonuçları var. B sütununda, A sütunundaki formülleri metin olarak yazdım. Yani A1'de =1-0.2-0.2-0.2-0.2-0.2, A2'de =1-0.1-0.1...-0.1 (10 tane 0.1) var. A2 ve A3'teki sonuçlar ilginç. Sıfır olması gereken iki sonucun da sıfırdan saptığı görülüyor. Bu neden oldu? İkinci sorulması gereken aynı ilginçlik A4 veya A5'te neden tekrarlanmıyor? Aşağıda görüleceği gibi Octave/Matlab'le de aynı sonuçlar alınıyor.



Bu davranışla ilgili bir ipucu vereceğim. Octave'da varsayılan veri türü çift değerlikli (double) sayı. Tamsayı olduğu halde 1 bile çift değerlikli olarak işlem görüyor (yandaki ekran görüntüsüne bakınız). Sayıları 0.2 yerine single(0.2) verip (tek duyarlıklı sayıya dönüştürüp), bunları ardarda 1'den çıkardığımda hata artıyor. Örn. 0.2 için hata 10-17 mertebesinden 10-8 mertebesine yükseldi ama single(0.0625) için hata yine 0.

Sayıların Bilimsel Gösterimi
Hatanın nedenlerine inmeden önce okul yıllarına dönüp normalleştirilmiş sayı ve sayıların bilimsel gösterimini hatırlamak gerekiyor: Çok büyük veya çok ondalıklı sayılar yazılırken sayının anlamlı basamakları yazılıp yanına 10n gibi bir çarpan eklenir. Sıfıra yakın sayılar için n, negatif ve büyük sayılar için n pozitif olur.

Örn.
6 720 000 000 = 6.72 * 109
0.000 000 007 51 = 7.51 * 10-9

m, bir gerçel sayı ve n bir tamsayı olmak üzere, onluk tabandaki bilimsel gösterim, m * 10n olarak genellenir. m'e katsayı, n'e üs diyelim. m'i gerçel sayı seçtim ama bunun üzerine bazı kısıtlamalar daha koymak gerekir. di'ler rakamlar {0, 1, ..., 9} ve d0, sıfır olmamak üzere m aşağıdaki gibi yazılabilir:


d0'ın sıfır olduğunu düşünelim: Örn. 0.672 * 1010 = 6.72 * 109
d0'ın 10dan büyük bir değer olduğunu düşünelim: 67.2 * 108 = 6.72 * 109

Dolayısıyla her sayı yukarıdaki gibi tek bir şekilde yazılabilir. Yukarıda onluk tabanın altını çizdim çünkü herhangi bir sayı sistemine genellersem yukarıdaki ifade şöyle olur:


İkilik sayı sistemi için b = 2 seçilir ve kısıtlar gözönüne alınırsa ikilik sayı sisteminde d0'ın yalnızca 1 olabileceği görülür.

İkilik Sayı Sisteminde Ondalıkların Gösterimi
Bu konu lise matematiğinin dışında ama mantığı basit. Onluk sistemde basamak değerleri şöyle:
İkilik sayı sistemine genellendiğinde basamak değerleri 20, 21, 22... olarak gidiyorsa noktadan sonraki basamak değerleri de benzer biçimde 2-1, 2-2, 2-3... şeklinde devam eder.
Yukarıdaki sayı 23 + 21 + 20 + 2-2 + 2-3 = 11.375'dir. Kısacası ikilik sayı sisteminde de sayılar bilimsel gösterimle tekil olarak yazılabilir. Katsayının tamsayı kısmının her zaman 1 olacağını yazmıştım. Bu gösterimde tamsayılar aşağıdaki gibidir:

1 = 1 * 20
2 = 1 * 21
3 = 1.5 * 21
4 = 1 * 22
5 = 1.25 * 22
6 = 1.5 * 22
7 = 1.75 * 22
8 = 1 * 23
...

Ondalıklı sayıları bilgisayarda göstermek için, Hexpert adında bir Hex düzenleyici (editör) kullanacağım. Bu düzenleyiciyi bilgisayarla tanıştığım zamandan beri kullanıyorum. En sevdiğim düzenleyici ama artık geliştirilmiyor. İkinci sevdiğim düzenleyici HxD ve aktif olarak geliştiriliyor. Bu yazıyı hazırlarken baktığımda HxD'nin yeni sürümünün çıktığını farkettim. Son sürümde dosya düzenleme daha yetenekli duruma gelmiş ve bu düzenleyici de kullanılabilir. Ben ekran görüntülerini Hexpert'den aldım. Aşağıda küçük bir dosyanın ekran görüntüsü var. İçinde boşluklar olan bir metin dosyası oluşturup bunu açmak yeterli.

Yanda dosyanın byte'ları, alttaki kutularda imlecin olduğu yerdeki 8, 16 ve 32 bitlik değerler var. S'ler işaretli U'lar işaretsiz değerler. İşaretli sayılarda sayının ilk biti işaret (birse negatif, sıfırsa pozitif) biti. İşaretsiz sayılarda bu bit sayıya katılıyor. Alttaki kutular değerleri elle girmeye olanak veriyor. Örn. 30h ofsetindeki 8 bit (00h) ve 16 bit (00h 00h) değer sıfır. Gösterim Little Endian olduğundan 32 bitlik değerin byte'ları tersine okunduğunda (3fh 80h 00h 00h) 63 * 2563 + 128 * 2562 + 0 * 256 + 0 = 1 065 353 216 değerini alıyor. Asıl ilgilendiğim FP32 hücresi. Ondalıklı sayılar bilgisayarda her zaman işaretli olarak ele alındıklarından bunların işaretsizi yok. FP32, 32 bit floating point yani tek değerlikli anlamına geliyor. FP64 ise 64-bit floating point (çift değerlikli). Buna şimdilik değinmeyeceğim. Yalnızca 40h offsetinde FP64 1.0 değeri var.

Bu gösterim IEEE-754 standardıyla belirleniyor. 1985'teki standartta 'single' ve 'double' ifadeleri geçerken 2008 revizyonunda bunlar binary32 ve binary64 olarak anılıyor. Standarda göre 32 bitlik ondalıklı sayının ilk biti işaret, sonraki 8 bit [30-23] üs ve geri kalan 22 bit [22-0] katsayı. 1.0'ı buna göre ayrıştıralım:

3F 80 00 00 = 0|011 1111 1|000 0000 0000 0000 0000 0000

[ Hexpert'te Alt+B ile ikilik taban görünümüne geçilebilir. ]

Tamsayı kısmı her zaman 1 olduğundan bu saklanmaz, her hesaplamada kendiliğinden eklenir. Dolayısıyla burada katsayı 0 + 1 = 1. Üs 0111 1111b = 127 ve standart gereği üsten 127 çıkarılır. Yani gerçek üs 127 - 127 = 0. İşaret biti 0 olduğundan pozitif. Hesaplandığında 1 * 2(127-127) = 1 bulunur.

2 = 1 * 21 olarak yazılır. Katsayıdan bir çıkarılırsa 1 - 1 = 0 ve üsse 127 eklenirse 1 + 127 = 128. O halde 2 şöyle ifade edilir: 0|100 0000 0|000 0000 0000 0000 0000 0000 = (40 00 00 00)16. Tabii Little endian olduğundan byte'ların tersine yazılması gerekir.

Katsayı bitlerinin ondalıklı ikilik sayılar olduğuna dikkat edilmelidir. Örn. 200 = 128 + 64 + 8 = 27 + 26 + 23. Üs olarak en büyük üssün değeri seçilmeli. O halde 200 = 27 * (1 + 2-1 + 2-4) = 1.1001b * 27 = 1.5625 * 27. Katsayıdan 1'i atınca kalan .1001 ve üs 7 + 127 = 134 = 1000 0110b. Hepsini toparlayınca sayı: 0|100 0011 0|100 1000 0000 0000 0000 0000 = (43 48 00 00)16.

Ondalıklı sayıları ifade etmek için hiçbir engel kalmadı:

Örn. 0.25 = 1 * 2-2. Üs -2 + 127 = 125 olur : 0|011 1110 1|000 0000 0000 0000 0000 0000

Şimdi periyot seyreden sayıları hatırlayalım. Bölmeleri kesirler yerine ondalıkla ifade etmeye kalktığımızda bölünen bölene tam bölünmüyorsa bölümde ondalıklar birbirini tekrar eder. Örn: 7 / 90 = 0.0707... Ondalık sayının bilgisayarda gösteriminde sürekli ikiye bölme ve ikilik kesirlerle ifade etme var. Peki ondalıklı sayılarda böyle bir periyot durumu olur mu? 0.1'i ele alalım. Bunun karşılığı: 0|011 1101 1|100 1100 1100 1100 1100 1101. Üs 123 - 127 = -4. Katsayıları ayırıp toplarsak aşağıdaki gibi oluyor:


Eğer en düşük değerli 2-23 olmasaydı 0.599 999 904 632 568 359 15. Tam 0.6 (aslında 1.6) hiçbir zaman olmuyor. Sayıya 1 ekleyip 2-4 ile çarpınca elde edilen sayı 0.100 000 001 490 116 119 370 703 125 ama Hexpert, Excel, Matlab ve Octave bu sayının ilk 6-7 basamağını gösterdiklerinde 0.1 görüyoruz (elbette şimdilik Matlab'de sayıların double olmasını görmezden geldim). Bu küçük fark her -0.1 işleminde birikerek artıyor ve sonuç sıfıra yaklaşıkça önem kazanıyor.

Not: Bu durum ikilik sistemin eksikliği olarak görülmemeli. Bilgisayarlar 10'luk sistemi kullanabilseydi ama insanlık 12'lik sayı sistemini kullanıyor olsaydı aynı durum yine yaşanacaktı.

Yukarıdaki sayıda dokuzuncu ondalıktan sonra sıfırdan farklı değerler görüldü ama 10 kere çıkarma yapıldığında aslında yapılan işlem "1-1.000 000 014 901 161 193 707 031 25" olup hata birikerek sekizinci ondalığa taşar. Octave'da tek değerlikli sayılarla aynı işlem tekrarlandığında hatanın 10-8 mertebesinde olduğunu yazmıştım.

>> X=single(0.1)
X =  0.10000
>> 1-X-X-X-X-X-X-X-X-X-X
ans =  -7.4506e-008
>>

Bilgisayardaki katsayının son bitinin bir olması ve olmaması arasındaki fark, bilgisayarın iki ondalıklı sayı arasındaki anlayabileceği en küçük farktır. Eğer bu 1 ile kendisinden büyük ilk sayı arasındaysa bilimsel yazında makina epsilonu (machine epsilon) denir. Matlab'de eps işlevi bu değeri döndürür:

>> eps
ans =   2.2204e-016
>> eps('single')
ans =   1.1921e-007

Wikipedia'daki örneklerde aynı değerler karşılaştırılabilir:
Single-precision floating-point format
0|011 1111 1|000 0000 0000 0000 0000 00012 = 3f80 000116 = 1 + 2-23 ≈ 1.0000001192
(smallest number larger than one)

0|011 1111 1111 | 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 00012 ≙ 1 + 2-52 ≈ 1.0000000000000002, the smallest number > 1


Peki neden 0.0625'te hata olmadı? Çünkü 0.0625 aslında 0.00012 olduğundan işlemcide tam değeriyle ifade edilebiliyor. Haliyle hata olmuyor.

Yazının ikinci bölümünde ortaya çıkabilecek hataları detaylandırıp başka örnekler vereceğim.