1 puan yazan GN⁺ 2024-07-16 | 1 yorum | WhatsApp'ta paylaş
  • 512 baytlık önyükleme sektöründen başlayıp x86_64 CPU’yu 16 bit real mode’dan 64 bit long mode’a çıkaran en küçük önyükleyici akışı adım adım kuruluyor
  • Önce BIOS kesmeleriyle çıktının çalıştığı doğrulanıyor, ardından nasm, ld, objcopy, QEMU birleştirilerek önyükleme imajının gerçekten çalışıp çalışmadığı sınanıyor
  • Önyükleme sektörünün kapasite sınırı nedeniyle yapı stage 1/stage 2 olarak ayrılıyor ve protected mode’a girmeden önce BIOS int 0x13 ile sonraki kod diskten okunuyor
  • 32 bit protected mode’dan itibaren BIOS rutinleri kullanılamadığı için GDT, flat segmentation, VGA tamponuna doğrudan yazma gibi donanım başlatmaları gerekiyor
  • 64 bit long mode’a geçiş için sayfa tabloları, PAE, EFER.LME, cr0.PG ve 64 bit için GDT’nin hepsinin doğru ayarlanması gerekiyor; sonrasında freestanding C kodu bir çekirdek gibi çağrılabiliyor

BIOS’tan başlayan 16 bit ortam

  • x86 CPU, reset sonrasında real mode durumundadır ve varsayılan işlenen boyutu 16 bittir
  • real mode, segmentation ile 20 bitlik adres alanı oluşturur ve en fazla 1MB belleği kullanabilir
  • BIOS’un ilk çalıştırdığı kod diskin önyükleme sektöründe bulunur
    • BIOS, ilk sektörü 0xaa55 magic number ile biten diski arar
    • Bu sektörü 0x7c00 bellek adresine yükler
  • BIOS’un verdiği alan yalnızca 512 bayt olduğu için bu kod, önyükleyicinin geri kalanını yükleyen bootstrap görevine odaklanır
  • BIOS rutinleri yalnızca real mode’da kalındığı sürece kullanılabilir

Gerekenler ve derleme ortamı

Önyükleme sektörü oluşturup çalıştığını doğrulamak

  • İlk önyükleme sektörü, BIOS rutinleriyle "Hello, world!" yazdırdıktan sonra hlt ve bir döngüyle duruyor
  • Metin yazdırmak için BIOS video servis çağrısı int 0x10 ve ah = 0x0e kullanılıyor
  • Makefile, nasm ile object dosyası oluşturuyor, linker script ile bağlıyor ve objcopy -O binary ile ham önyükleme imajı üretiyor
  • make boot, imajı QEMU ile çalıştırıyor
    • qemu-system-x86_64 -no-reboot -drive file=$<,format=raw,index=0,media=disk
  • linker script, önyükleme sektörünün 0x7c00 tabanına yerleştirilmesini sağlayacak şekilde ayarlanıyor
    • MEMORY içindeki boot_sector origin değeri 0x7c00, uzunluğu 512
    • .bootsign bölümü 0x7c00 + 510 konumuna 0x55, 0xaa ekliyor
  • Önyükleme sektörü assembly’si içinde offset ve magic number doğrudan da işlenebilir, ancak burada bu görevi linker script üstleniyor

stage 1’in stage 2’yi diskten okuması

  • stage 1, BIOS’un yüklediği önyükleme sektörü kodudur ve hedefi stage 2’yi belleğe almaktır
  • stage 2, 16 bit real mode’dan 32 bit protected mode’a geçiş kodunu içerir
  • protected mode’a girdikten sonra BIOS rutinleri kullanılamadığı için disk sektörü okuma işlemi geçişten önce tamamlanmalıdır
  • Disk erişimi için BIOS disk servisleri int 0x13 kullanılır
    • ah = 0x42, BIOS extended read işlevidir
    • dl = 0x80, sürücü numarasıdır
    • disk address packet içinde okunacak sektör sayısı, hedef adres ve başlangıç sektörü bulunur
  • Örnek, READ_SECTORS_NUM equ 64 ile 64 sektör okur
    • Önyükleme sektörü sector 0 olduğundan stage 2, sector 1’den itibaren okunur
    • Hedef adres BOOT_LOAD_ADDR + SECTOR_SIZE, yani 0x7c00 + 512
  • Kod içinde, istenenden daha az sektör okunması durumunu kabul eden geçici bir işlem hâlâ bulunuyor
  • stage 2 başlangıçta real mode için print_string fonksiyonunu kopyalayıp "Hello from stage 2" yazdırarak stage 1’den sıçramanın düzgün devam ettiğini doğruluyor

32 bit protected mode’a geçmek

  • protected mode’a girmek için önce Global Descriptor Table(GDT) tanımlanmalıdır
  • protected mode’da segmentation, temelde bellek koruması için kullanılır
  • 64 bit long mode’da paging gereklidir, ancak ondan önceki protected mode aşamasına geçmek için önce segmentation ayarı gerekir
  • Örnek GDT, Intel kılavuzundaki flat modeli izler
    • bir code segment ve bir data segment içerir
    • bu iki segment tüm linear address space’e eşlenir
    • long mode’a geçiş için ara aşama olduğundan en basit model kullanılır
  • GDT, bellekte bulunan bitişik bir yapı dizisidir
    • başlangıçta geçersiz çevirileri yakalamak için null descriptor bulunur
    • ardından code segment descriptor ve data segment descriptor gelir
  • Geçiş şu sırayla yapılır
    • cli ile interrupt’lar devre dışı bırakılır
    • lgdt [gdt32_pseudo_descriptor] ile GDT adresi ve uzunluğu GDTR’ye yüklenir
    • cr0.PE, yani cr0 içindeki bit 0 ayarlanarak protected mode açılır
    • instruction pipeline’ı boşaltmak ve cs değerini yeni code segment ile güncellemek için far jump yapılır
  • protected mode’a girdikten sonra eski segment değerleri artık anlam taşımadığından ds, ss, es, fs, gs yeni data segment selector değeriyle ayarlanır
  • Tüm ayarlardan sonra interrupt’ları yeniden açmak için ek işlemler gerekir

BIOS olmadan ekrana yazmak

  • protected mode’da artık BIOS rutinleri çağrılamaz
  • Metin yazdırma yöntemi, VGA buffer’a doğrudan yazmaya dönüşür
  • print_string32, 0xb8000 adresine karakter ve renk baytlarını yazar
    • renk değeri 0xf
    • her karakter hücresi 2 bayt kullanır
  • Bu çıktı fonksiyonu çok basittir; mesaj her zaman ekranın sol üst köşesinde görünür

long mode’a giriş için sayfa tabloları

  • Intel belgelerindeki IA-32e mode, AMD64 kılavuzundaki long mode’a karşılık gelir
  • long mode’a geçmek için CPU’nun protected mode’da olması ve paging’in de etkinleştirilmiş olması gerekir
  • paging kavramı için Introduction to Paging ve OSTEP kaynaklarına başvuruluyor
  • PAE etkin long mode’da 4 seviyeli sayfa tabloları kullanılır
  • build_page_table, belirtilen adreste 4 seviyeli sayfa tablolarını oluşturur
    • page boyutu 0x1000
    • her page table boyutu 0x1000
    • entry sayısı 512
    • önce dört tablo 0 ile başlatılarak tüm entry’ler not present durumuna getirilir
    • PML4 → PDP → PD → page table zincirindeki ilk entry’ler birbirine bağlanır
    • en alt page table katmanında 512 entry ayarlanır

64 bit için GDT ve long mode’a geçiş sırası

  • paging, virtual address space ve yetki yönetimini üstlense de long mode’da da GDT gerekir
  • 64 bit için GDT de flat model’i izler ve protected mode için kullanılan GDT ile neredeyse aynıdır
  • Fark, long mode ile ilgili bit ayarlarındadır
    • code segment için 64-bit code segment flag’i ayarlanır
    • bu flag ayarlandığında default operation size bit’i 0 olmalıdır
  • long mode’a geçiş şu akışla ilerler
    • 0x1000 adresinde 4 seviyeli page table oluşturulur
    • cr3 içine PML4 table adresi yazılır
    • cr4 içindeki bit 5 ayarlanarak PAE etkinleştirilir
    • 0xc0000080 MSR okunur ve EFER.LME, yani bit 8 ayarlanır
    • cr0 içindeki bit 31 olan PG flag’i ayarlanarak paging etkinleştirilir
    • 64 bit için GDT lgdt ile yüklenir
    • 64 bit code segment’e far jump yapılarak 64 bit moda girilir
  • paging açıldıktan hemen sonra durum IA-32e compatibility mode’dur; 64 bit segment flag’i ayarlanmış GDT’ye sıçranınca 64 bit moda geçilir
  • Başarının doğrulama mesajı, VGA buffer üzerinden ekranın sol üst köşesinde gösterilir

freestanding C kodu çağırmak

  • 64 bit long mode’a kadar gelindikten sonra freestanding C kodu çağrılabilir
  • kernel.c, VGA buffer 0xb8000 alanını temizleyip "Hello from C" yazdırır
  • Assembly tarafındaki start_long_mode, 64 bit için metin yazdırdıktan sonra extern _start_kernel bildirir ve _start_kernel fonksiyonunu çağırır
  • linker script bellek alanını üç bölüme ayırır
    • boot_sector: 0x7c00, length 512
    • stage2: 0x7e00, length 512
    • kernel: 0x8000, length 0x10000
  • .text, .data, .rodata, .bss bölümleri kernel alanına yerleştirilir
  • Makefile, hem assembly hem de C derlenecek şekilde değiştirilir
    • C compiler gcc’dir
    • başlıca CFLAGS değerleri -std=c99 -ffreestanding -m64 -mno-red-zone -fno-builtin -nostdinc -Wall -Wextra
  • Tüm örnek kod indirme bağlantısı üzerinden sunuluyor

1 yorum

 
GN⁺ 2024-07-16
Hacker News yorumları
  • Koruma modundan geçmeden, çok daha az kodla doğrudan long mode'a geçmek mümkün: https://wiki.osdev.org/Entering_Long_Mode_Directly
    Bu yöntemle yapılmış, küçük bir 64 bit çekirdek için bir bootloader vardı; diskteki çekirdeği okuyup VESA modunu ayarlayan kod da dahil olmasına rağmen boot sector'a rahatça sığıyordu. 2. aşama yükleyiciye de gerek yoktu

    • Bunu 512 bayt içine nasıl sığdırdıklarını merak ediyorum. Çekirdeği diskte herhangi bir yere normal bir dosya gibi koymayı sağlayan gerçek bir dosya sistemi yok gibi görünüyor; yalnızca dosya parçalanmasını ele almak bile muhtemelen 512 baytı rahatça aşar
    • Sadece https://limine-bootloader.org/ kullanmak çok daha basit hale getiriyor. Gerçek mod ile uğraşmak gerekmiyor, SMP için de aynı durum geçerli; ayrıca çekirdeği higher-half mapping ile otomatik yüklüyor ve aarch64 ile riscv64 üzerinde de çalışıyor
    • Doğru, ama bölüm tablosunu da ekleyip modern AHCI denetleyicileri ve SATA desteği sağlamak istersen bootloader alanı daha da daralıyor ve optimizasyon gerekiyor. Bu durumda yükleyici için 510 baytın tamamını kullanamazsın, çok daha azı kalır; geçerli bölüm girdilerini de doldurman gerektiğinden tablonun içindeki baytları da kullanamazsın, bu da işi daha zor hale getirir
      Gerçek anlamda modern bir sabit disk kullanacaksan MBR yerine GPT'ye bakmak daha iyi olur. Bölüm tablosu sınırlarına takılmadan 2 TB'dan büyük disklerle de çalışabilirsin. UEFI bu sorunları ortadan kaldırır ve ciddi bir zorluk olmadan düzgün bir disk yerleşimi kullanmanı sağlar
      64 bit moda geçmek için koruma modu gerekmez. Ama BIOS kullanmamak daha iyidir. Dağınıktır ve işleri sadece daha zahmetli hale getirir
      UEFI'yi EDK2 ya da GnuEFI ile kullanmak daha iyidir; ikisini de uygulamak oldukça kolay ve rahattır. UEFI'nin ilk kavramlarına alışmak biraz zaman alır ama GitHub'daki örnek projelere bakınca yapıyı kolayca kavrayabilirsin. EDK'deki .dec ve .inf dosyaları biraz can sıkıcı, GnuEFI'de ise işlev bulmak için başlık dosyalarını okumak gerekiyor; ama bunların ikisi de belirtimi belirsiz BIOS arayüzlerinden çok daha iyidir. Gerçek donanımda int 0x10, int 0x15 vb. şeylerin düzgün şekilde var olduğunu varsayamazsın bile
      UEFI sistemlerde kararlı bir asgari temeli varsayabilirsin ve donanım ya da platform özelliklerini de makul biçimde listeleyebilirsin. Ayrıca UEFI platformu zaten büyük ölçüde ayarlamış olduğundan, OS yükleyici bileşeninde çok fazla ilklendirme yapmadan doğrudan OS, sürücüler ve çekirdek tasarımına uygun bileşenleri yükleyebilirsin. Bellek haritasını alır, EFI dosya sistemine erişir ve gerekenleri oradan okursun
    • Bunun mümkün olduğunu bilmiyordum. Amaç sadece long mode'a geçmekse neden baştan koruma modundan geçmek gerektiğini merak ediyorum
  • 80286'da 16 bitlik bir register olan Machine Status Word (MSW) vardı, 80386 bunu 32 bitlik CR0 register'ına genişletti. Daha sonra 64 bit long mode, EFER MSR'ını ekledi ve CR0'ı 64 bite genişletti, ama bugün bile CR0'da yalnızca 11 bit kullanılıyor ve EFER'de de yalnızca 8 etkin bit var
    Intel/AMD'nin mevcut register'lardaki boş bitleri kullanmak yerine neden iki kez yeni bir tercih yaptığını merak ediyorum: https://wiki.osdev.org/CPU_Registers_x86-64#CR0

    • Muhtemelen geriye dönük uyumluluğu daha sağlam tutmak içindir. Yazılım, ayrılmış bitlerin değerini varsayabilir ya da oralara değer yazabiliyor olabilir. Bu tür donanım register'larında bit yerleşimleri oldukça keyfidir ve üst bitleri kullanmanın da pek bir maliyeti yoktur
    • Tek kelimelik cevap muhtemelen bürokrasi olurdu. Büyük organizasyonlar genel olarak özellikle iyi kararlar almakta zorlanır ve çok sayıda anlamsız seçim ortaya çıkar
      CR1 ile CR5~CR7 hâlâ ayrılmış durumdayken CR8'in ortaya çıkmasının nedeni de benzer görünüyor
  • Bu yazıdaki gereksiz yere en karmaşık görünen şey Makefile ve linker script. NASM düz ikili çıktı destekliyor, ama görünüşe göre bunu kullanmak fazla “hacky” bulunmuş

    • Bana göre linker script, düz NASM'den çok daha okunabilir ve akıl yürütmesi daha kolay. Özellikle birden fazla kaynak dosyası olduğunda
    • Tamamen doğru. Sonradan Makefile ve linker script önemli bir baş ağrısına dönüşür, ama düz ikili üreteceksen düz ikili üretirsin. Bunu gereksiz yere şişirmeye gerek yok
      Eskiden OS'imde bununla dalga geçmek için make.sh diye bir dosya vardı. Şimdi “dosya biçimi” gibi havalı şeyler kullanıyorum; bu yüzden -fbin ve --oformat=binary artık geçici birer hatıra. Veri C dosyaları ile kod C dosyalarını ayırıp bunları ikili olarak dökerek oradan bir canavar toplamaya uzun süre çalıştım, ama bağlama ve yükleme çok zorlaşıyor. Sadece ELF ya da PE kullanmak daha iyi; zaten görünüşe göre bu biçimler tam da bunun için var
  • Harika ve iyi bir egzersiz gibi görünüyor, ama işe yarayıp yaramadığından emin değilim. Çalışırken ayarları kontrol etmeyi ya da değiştirmeyi sağlayan, Fisher-Price oyuncağı gibi bir UX bile var mı merak ediyorum
    Boot süreci, minimize edilmiş moddan tek kullanıcılı moda, oradan da recovery mode'da havalanan bir duruma giden bir süreçtir
    Xenix/DOS döneminden beri, Microsoft ürünleriyle birlikte Unix de kullanıyorum; sanırım yaklaşık 40 yıl oldu
    Linux'u da İsveç versiyonundan, yani ilk sürümünden beri kullandım; GNU 0.1'i de denedim
    Xenix'e Unix dediğim için özür dilerim. Xenix, çıktığı andan çöküşüne kadar zaten geçmişte kalmak isteyen, taklitçi karakterde karmakarışık bir üründü
    Microsoft bana, ürün çıkarmaktan çok müşterilerinin üstüne kedi kumu kutusunu boşaltan bir şirket gibi geliyor. Buna son örnekler Copilot ve 22H2
    F1 arabasının, kurşun kalemin ve cep hesap makinesinin nasıl geliştiğine bakınca, kullanılabilir bir ideale ne kadar yaklaştığımızı merak ediyorum
    Neden bootloader'ın statik çekirdek modunda olmadığını da merak ediyorum. Eskiden öyleydi; yakın zamanda biri yeniden öyle olması gerektiğini önerdi, ben de katıldım

  • https://wiki.osdev.org/A20_Line

  • CPU’yu doğru moda geçirmek için gereken adımların tamamının gereksiz görünmesi şaşırtıcı. Bunların çoğu geriye dönük uyumluluk yüzünden gerekli prosedürlermiş gibi duruyor
    Intel’in en baştan doğru modda başlatan bir bayrak ya da komut sağlayıp sağlayamayacağını, ya da geriye dönük uyumluluğu kaldırıp kaldıramayacağını merak ediyorum
    ARM64’te de kısmen benzer sorunlar olduğunu hatırlıyor gibiyim. Başından beri 64 bit olarak tasarlanmış, geriye dönük uyumluluğa ihtiyaç duymayan ve varsayılan olarak istenen durumda açılan bir CPU olup olmadığını merak ediyorum. Itanium’un hedefi ya da tasarımı böyle bir şey miydi diye düşündüm

    • Intel’in önerdiği X86S bunun amacı için var

      X86S is a legacy-reduced-OS ISA that removes outdated execution modes and operating system ISA.
      The presence of the X86S ISA is enumerated by a single, main CPUID feature LEGACY_REDUCED_ISA in CPUID 7.1.ECX[2] which implies all the ISA removals described in this document. A new, 64-bit “start-up” interprocessor interrupt (SIPI) has a separate CPUID feature flag.
      [0] https://cdrdv2.intel.com/v1/dl/getContent/776648 [PDF uyarısı]

    • Intel bunu 80376 ile denedi ama pek başarılı olmadı: https://en.wikipedia.org/wiki/Intel_80376
      Itanium, yani Itanic için de durum aynıydı
      Geriye dönük uyumluluk; ARM, MIPS, RISC-V vb. yerine x86’ı seçmenin asıl nedenidir. Ne yazık ki Intel ve AMD’deki bazı insanlar bunu pek anlamıyor gibi görünüyor
    • UEFI zaten var. Bir bölüme klasör içinde Windows benzeri bir ikili dosya koyarsanız, 64 bit moddaki bir barındırma ortamında çalıştırılır. Elbette bu işi sizin yerinize yapan sayısız önyükleyici de var
    • arm64’te tam olarak sorunun ne olduğunu bilmiyorum
  • Harika bir proje. Burada UEFI taraftarları neden ille de yeni bir önyükleyici yöntemi yapıldığını sorguluyorsa, insanların böyle şeyleri neden yaptığını kaçırıyor gibiler
    Yazarın sonunda yazdığı “buraya kadar geldiysen harikasın” sözü tam yerinde; gerçekten harika

  • UEFI çıkalı ne kadar oldu diye düşünüyorum. Keşke long mode ile birlikte BIOS da kaldırılmış olsaydı

    • BIOS zaten kaldırılma sürecinde. Yeni anakartlarda bu işlev varsayılan olarak UEFI üzerinden emüle ediliyor ve artık genişletilmiyor da
      Kaldırılma sürecinde demek silinmiş demek değil; kaldırılması hedeflendiği için artık güncellenmediği veya geliştirilmediği anlamına geliyor
  • Bu önyükleme sürecinin EFI/UEFI üzerinde de çalışıp çalışmadığını merak ediyorum. Çalışıyorsa, UEFI denetleyicisinin real mode, protected mode ve long mode geçişlerini emüle edip etmediğini, yoksa bunların gerçek donanım üzerinde mi yapıldığını da merak ediyorum

    • Hayır. UEFI aygıt yazılımı, UEFI önyükleyicisine legacy BIOS ortamından, yani real-address mode’dan tamamen farklı bir ortam sunar. Modern sistemlerdeki UEFI aygıt yazılımı doğrudan 64 bit long mode’a geçer ve düz bellek modeli GDT ile identity-mapped paging’i de ayarlar
      Hobi amaçlı bir OS için UEFI önyükleyicisi oluşturma sürecini burada yazmıştım: https://0xc0ffee.netlify.app/osdev/05-bootloader-p1.html
  • Bunun ARM tarafında daha basit olup olmadığını merak ediyorum

    • Kart üreticilerinin her şeyi kendi kafasına göre yapabilmesi anlamında daha basit. Kart üreticisi için basit ama geri kalan herkes için korkunç derecede karmaşık hale geliyor
    • Evet. Önyükleyici hâlâ karmaşık, ama gereken legacy ayarları daha az. Yine de BIOS yerine UEFI’yi hedeflerseniz, x86’ta da çok daha basit oluyor
    • Emin değilim ve çok da beklemiyorum. Şu sıralar RISC-V’yi derinlemesine inceliyorum; orada umut var gibi görünüyor