1 puan yazan GN⁺ 2025-10-26 | 1 yorum | WhatsApp'ta paylaş
  • Program çalıştırılmadan önce, çekirdeğin execve sistem çağrısı üzerinden süreci oluşturup başlatma sürecini inceleyen teknik bir analiz
  • Bu çağrı çalıştırılabilir dosya yolunu, argümanları ve ortam değişkenlerini iletir; çekirdek de buna dayanarak ELF biçimindeki çalıştırılabilir dosyayı yükler
  • ELF dosyası kod, veri, semboller ve dinamik bağlama bilgileri gibi unsurları içerir; çekirdek bunları yorumlayarak bellek eşlemeyi ve yığın başlatmayı gerçekleştirir
  • Ardından çekirdek denetimi _start giriş noktasına devreder; dile özgü runtime başlatıldıktan sonra ancak o zaman kullanıcı tanımlı main fonksiyonu çağrılır
  • Bu süreç, işletim sistemi, derleyici ve runtime arasındaki iş birliği yapısını gösterir ve sistem düzeyinde program yürütmenin nasıl gerçekleştiğini anlamak açısından önemlidir

Program çalıştırmanın başlangıç noktası: execve çağrısı

  • Linux'ta program çalıştırma execve sistem çağrısı ile başlar
    • execve(const char *filename, char *const argv[], char *const envp[]) biçiminde çalıştırılabilir dosya adı, argüman listesi ve ortam değişkenleri listesi iletilir
    • Çekirdek bunun üzerinden hangi programın hangi ortamda çalıştırılacağını belirler
  • Yüksek seviyeli dillerde bu çağrı genellikle standart kütüphanenin süreç çalıştırma API'si ile sarmalanır
    • Örnek: Rust'ın std::process::Command yapısı içeride execve çağırır
    • Kabuktaki PATH aramasına benzer şekilde, komut adını tam yola dönüştürme işlemi yapılır
  • Shebang(#!) içeren betiklerde çekirdek, belirtilen yorumlayıcıyı kullanarak programı çalıştırır
    • Örnek: #!/usr/bin/python3 → Python yorumlayıcısıyla çalıştırılır

ELF: çalıştırılabilir dosyanın yapısı

  • Linux'taki çalıştırılabilir dosyalar ELF(Executable and Linkable Format) biçimini izler
    • ELF; kod, veri, semboller ve yeniden konumlandırma bilgileri gibi unsurları içeren standart bir çalıştırılabilir dosya biçimidir
    • Diğer işletim sistemleri ise Mach-O(macOS), PE(Windows) gibi farklı biçimler kullanır
  • ELF başlığı dosyanın yapısı ve bellek yerleşimine dair bilgileri içerir
    • Örnek alanlar: ELF Magic, Class, Entry point address, Program headers, Section headers
    • Entry point address, programın ilk çalıştırılacağı komutun adresidir
    Reklam
  • Örnek ELF başlığında bunun RISC-V mimarisi için bir ELF32 çalıştırılabilir dosyası olduğu ve giriş noktasının 0x10358 adresi olarak belirlendiği görülür

ELF iç yapısı

  • ELF dosyası birden fazla bölümden(section) oluşur
    • .text: çalıştırılabilir kod
    • .data: başlatılmış global değişkenler
    • .bss: başlatılmamış global değişkenler
    • .plt: paylaşımlı kütüphane çağrıları için tablo
    • .symtab, .strtab: sembol ve dizge tabloları
  • PLT(Procedure Linkage Table), paylaşımlı kütüphane fonksiyon çağrılarını destekler
    • Örnek: libc içindeki printf, malloc gibi fonksiyonlar
    • ELF içindeki PT_INTERP bölümü, dinamik bağlayıcıyı(yorumlayıcıyı) belirtir
  • Çekirdek, ELF'yi okuyarak yüklenebilir bölümleri belleğe yerleştirir ve gerektiğinde ASLR, NX bit gibi güvenlik özelliklerini uygular

Sembol tablosu ve runtime bağlama

  • ELF'nin sembol tablosu(symtab), fonksiyon ve değişkenlerin adres bilgilerini içerir
    • Örnek girdiler: _start, main, __libc_start_main
    • Basit bir “Hello, World!” programı bile 2300'den fazla sembol içerebilir
  • Bunun büyük kısmı standart kütüphane ve runtime başlatma kodundan kaynaklanır
    • Çünkü musl veya glibc gibi libc uygulamaları bağlanmıştır
    Reklam
  • Çekirdek, ELF'nin her bölümünü yükledikten sonra denetimi yorumlayıcıya(dinamik bağlayıcıya) devreder
    • Yorumlayıcı yeniden konumlandırma(relocation), adres rastgeleleştirme(ASLR), çalıştırma izni ayarı(NX bit) gibi işlemleri yürütür

Yığın başlatma süreci

  • Çekirdek, program çalıştırılmadan önce yığını(stack) doğrudan kurmak zorundadır
    • Yığın; yerel değişkenler, fonksiyon çağrı çerçeveleri ve argüman aktarımı için kullanılır
  • execve çağrısında iletilen argv, envp yığına yerleştirilir
    • Program bu sayede komut satırı argümanlarına ve ortam değişkenlerine erişir
  • Çekirdek ayrıca ELF yardımcı vektörünü(auxv) de yığına ekler
    • Sayfa boyutu, ELF meta verileri ve sistem bilgileri dahil yaklaşık 30 öğe içerir
    • Örnek: AT_PAGESZ, bellek sayfası boyutunu belirtir (ör. 4KiB)
  • RISC-V öykünücü örneğinde yığın işaretçisi(sp), yüksek adreslerden başlatılır; argümanlar, ortam değişkenleri ve yardımcı vektör ters sırada yığına eklenir

Giriş noktası ve _start fonksiyonu

  • ELF'nin giriş noktası, _start fonksiyonunun adresi olarak belirlenir
    • _start, çekirdeğin denetimi devrettiği ilk kullanıcı alanı kodudur
    Reklam
  • Çoğu dil _start içinde önce runtime başlatmayı yapar, ardından main çağrılır
    • Örnek: Rust'ta std::rt::lang_start, C'de __libc_start_main
  • Rust örneğinde #![no_std], #![no_main] öznitelikleri kullanılarak runtime olmadan doğrudan _start tanımlanabilir
    • _start içinde yığından argc, argv, envp okunur ve main işaretçisi çağrılır
  • Dile özgü runtime; global kurucular, iş parçacığı yerel depolama ve istisna işleme gibi dile özel başlatma işlemlerini yürütür

main() çağrısına kadar olan genel akış

  • Tüm süreç şu şekilde özetlenebilir
    1. execve çağrılır → çekirdek ELF dosyasını yükler
    2. ELF yorumlanır → kod/veri bölümleri eşlenir, yorumlayıcı belirlenir
    3. Yığın kurulur → argümanlar, ortam değişkenleri ve yardımcı vektör kaydedilir
    4. Giriş noktası _start çalıştırılır
    5. Runtime başlatıldıktan sonra main() çağrılır
  • Bu adımlar dizisi, işletim sistemi çekirdeği, ELF biçimi ve dil runtime'ı arasındaki iş birliğini gösterir
  • Gerçek Linux çekirdeği adres alanı, süreç tablosu ve grup yönetimi gibi ek iç mantıklar da içerir; ancak bu yazı ondan önceki temel akışı açıklar

Sonuç ve düzeltme

  • main() öncesindeki yürütme süreci, çekirdek düzeyindeki başlatma ile runtime yapılandırmasının birleşimidir
  • Basit bir “Hello, World!” programı bile karmaşık ELF yapısı ve runtime başlatma adımlarından geçerek çalışır
  • Yazının ilk sürümünde bazı bölüm yükleme mantıkları çekirdeğe atfedilmişti; ancak gerçekte bunun ELF yorumlayıcısının görevi olduğu şeklinde düzeltme yapıldı
  • Bu analiz, sistem programlama, derleyiciler ve işletim sistemi mimarisini anlama açısından yararlı bir temel kaynak işlevi görür

1 yorum

 
GN⁺ 2025-10-26
Hacker News yorumları
  • ELF dosyalarının dinamik bağlama sürecini açıklıyor
    Çekirdek, ELF'nin PT_LOAD segmentlerini eşler, PT_INTERP ile belirtilen dinamik bağlayıcıyı (ld.so) yükler ve ardından denetimi ona devreder
    Sonrasında dinamik bağlayıcı kendisini yeniden konumlandırır (relocation) ve gerekli paylaşılan nesneleri mmap/mprotect ile yükler
    Bu yapı, betiklerdeki shebang(#!) mekanizmasına benzetiliyor

    • Çekirdeğin bölüm bilgileriyle hiç ilgilenmediği, yalnızca PT_LOAD segmentlerini işlediği belirtiliyor
      Geçmişte objcopy ile ELF'ye rastgele bir dosya gömmeye çalışırken çekirdeğin bunu yüklememesi nedeniyle yaşadığı kafa karışıklığını paylaşıyor
      Sonunda doğrudan program header tablosu yama aracı yaptığını ve bu özelliğin mold bağlayıcısına da eklendiğini söylüyor
      İlgili yazı: Self-contained Lone Lisp Applications
    • Yazar, daha önce içeriği yanlış düzenleyip yayımladığını kabul ediyor ve düzelteceğini söylüyor
    • Linux'ta yükleyicinin kullanıcı alanında çalıştığını, buna rağmen neden daha çeşitli yükleyiciler olmadığını hep merak ettiğini söylüyor
  • Tüm kodu main() öncesine ya da main() olmadan paketleme deneyi yaptığını söylüyor
    İlgili yazı: Packing a codebase into a single function

    • Okuyunca beklenmedik ölçüde basit ve dayanıksız görünmediği için ilginç bulduğunu söylüyor
      Şaka yollu olarak, tüm fonksiyonları main(100+n, ...) biçimine çevirmenin yeterli olacağını söylüyor
  • Bu konuyla ilgileniliyorsa kendi hazırladığı cpu.land'e bakılmasını öneriyor
    Bellek yerleşiminden çok çoklu görev ve kod yükleme sürecini ele alıyor

    • cpu.land'i gerçekten çok sevdiğini söyleyip teşekkür ediyor
  • Standart kütüphaneden kaçınıp doğrudan yalnızca Linux syscall çağıran C projelerinin ne kadar yaygın olduğunu merak ettiğini söylüyor
    Kod yazmanın bu şekilde çok daha eğlenceli geldiğini belirtiyor

    • Syscall'ları doğrudan kullanmanın aksine verimsiz olduğunu savunuyor
      ALSA, DRM gibi işlevlere çekirdek syscall'ları yerine sistem kütüphaneleri üzerinden erişmenin çok sayıda avantajı olduğunu söylüyor
      Bunun taşınabilirlik ve bakım açısından Windows tarzı yaklaşımdan daha iyi olduğunu açıklıyor
    • Windows'ta yalnızca Win32 API kullanılırsa C çalışma zamanını bağlamaya gerek olmadığını ekliyor
    • Kendisinin de geçmişte liblinux projesini yapıp yalnızca syscall'larla program yazdığını söylüyor
      Şimdi Linux'un nolibc başlıkları iyi durumda olduğu için bunu bıraktığını,
      şu anda syscall tabanlı bir Lisp yorumlayıcı dili geliştirdiğini belirtiyor
      Sistem çağrılarıyla doğrudan Linux kullanıcı alanı kurma deneyi olduğu için bunun çok ilginç bir yolculuk olduğunu söylüyor
    • Taşınabilirliği korumaya çalıştığını ama dosya tanımlayıcılarının çok kullanışlı olduğu için vazgeçmenin zor olduğunu söylüyor
    • Birçok sürücü kodunun da gerçekte yalnızca syscall kullandığını ekliyor
  • ELF yorumlayıcısının (ld.so), ilk ELF segmentleri eşlendikten sonra tüm yükleme işini üstlendiğini açıklıyor
    execve, PT_LOAD segmentlerini eşler ve aux vector'ü yığına doldurduktan sonra
    ELF yorumlayıcısının giriş noktasına sıçrar
    Çekirdeğin PLT/GOT hakkında hiçbir şey bilmediğini söylüyor

  • Üniversitede bu konuyu öğreten biri olarak, öğrencilerin bellek diyagramları yüzünden kafa karışıklığı yaşadığını söylüyor
    Ders kitapları adresleri yükseldikçe yukarı çizerken, gerçek Linux süreçlerinde
    düşük adresler yukarıda, yüksek adresler aşağıda gösteriliyor
    /proc/<pid>/maps içinde aşağı kaydırdıkça adresler büyüyor
    Yani “heap yukarı büyür, stack aşağı büyür” ifadesi yalnızca sayısal yönü anlatıyor;
    görsel olarak ise durum tam tersi
    IDE'lerdeki gibi aşağı indikçe adreslerin büyüdüğü şekilde çizilirse bunun çok daha sezgisel olacağını öneriyor

    • Stack sonuçta stack pointer azalırken büyüdüğü için “aşağı büyür” ifadesinin yine de doğru olduğunu söylüyor
      Ancak görselleştirmenin yatay yapılmasının daha doğal olacağını öneriyor
    • Kendisinin de geçmişte aynı kafa karışıklığını yaşadığını ve little-endian adres gösteriminin de kafa karıştırıcı olduğunu hatırlıyor
    • Gerçek nesnelerin yığılma yönü düşünülünce “stack aşağı büyür” ifadesinin sezgisel olmadığını söyleyerek itiraz ediyor
  • Eski PIC16 mikrodenetleyicilerle bu tür deneyler yapmayı sevdiğini söylüyor
    Stack pointer, zamanlayıcı ve değişken ayarlarını doğrudan ellemenin eğlenceli olduğunu düşünüyor

  • shebang(#!) ile ilgili bir deneyimini paylaşıyor
    Bir Java uygulaması çalıştırma betiğini bulamadığına dair hata veriyordu,
    ama gerçek sorun betiğin shebang yolunun yanlış olmasıydı
    Yerelde sorunsuz çalışırken uzak sunucuda yorumlayıcı yolu farklı olduğu için bu sorun ortaya çıkmıştı

    • Bunun yalnızca Java'ya özgü olmadığını, ENOENT hatası veren tüm programlarda görülebileceğini söylüyor
      strace ile çalıştırılırsa hatanın hangi syscall'da oluştuğunun hemen görülebileceğini tavsiye ediyor
    • Shebang yapısını inceleyen şu yazıyı paylaşıyor: What the #! means
    • Çekirdekte shebang desteği için CONFIG_BINFMT_SCRIPT=y ayarının gerektiğini ekliyor
  • Hata ayıklama sırasında ana ikili dosyanın yeniden konumlandırma sırasının tam olarak ne zaman uygulandığını hep karıştırdığını söylüyor
    Bağlayıcının kendi sembollerini çözmeden önce mi sonra mı olduğunun adeta kara büyü gibi göründüğünü ifade ediyor

  • Markdown içindeki “lang_start function (defined here)” kısmındaki bağlantının bozuk olduğunu belirtiyor