- 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
- Ö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
- Ç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
- Ç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
execve çağrılır → çekirdek ELF dosyasını yükler
- ELF yorumlanır → kod/veri bölümleri eşlenir, yorumlayıcı belirlenir
- Yığın kurulur → argümanlar, ortam değişkenleri ve yardımcı vektör kaydedilir
- Giriş noktası
_start çalıştırılır
- 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
Henüz yorum yok.