main() fonksiyonu çalışmadan önceki yolculuk
(amit.prasad.me)- Program çalıştırılmadan önce, çekirdeğin
execvesistem ç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
_startgiriş noktasına devreder; dile özgü runtime başlatıldıktan sonra ancak o zaman kullanıcı tanımlımainfonksiyonu ç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
execvesistem çağrısı ile başlarexecve(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::Commandyapısı içerideexecveçağırır - Kabuktaki PATH aramasına benzer şekilde, komut adını tam yola dönüştürme işlemi yapılır
- Örnek: Rust'ın
- 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
- Örnek:
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 alanlar:
- Ö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
0x10358adresi 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:
libciçindekiprintf,mallocgibi fonksiyonlar - ELF içindeki
PT_INTERPbölümü, dinamik bağlayıcıyı(yorumlayıcıyı) belirtir
- Örnek:
- Ç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
- Örnek girdiler:
- Bunun büyük kısmı standart kütüphane ve runtime başlatma kodundan kaynaklanır
- Çünkü
muslveyaglibcgibilibcuygulamaları bağlanmıştır
- Çünkü
- Ç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ı,
_startfonksiyonunun adresi olarak belirlenir_start, çekirdeğin denetimi devrettiği ilk kullanıcı alanı kodudur
- Çoğu dil
_startiçinde önce runtime başlatmayı yapar, ardındanmainçağrılır- Örnek: Rust'ta
std::rt::lang_start, C'de__libc_start_main
- Örnek: Rust'ta
- Rust örneğinde
#![no_std],#![no_main]öznitelikleri kullanılarak runtime olmadan doğrudan_starttanımlanabilir_startiçinde yığındanargc,argv,envpokunur vemainiş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
1 yorum
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
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
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
Ş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
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
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
Ş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
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>/mapsiçinde aşağı kaydırdıkça adresler büyüyorYani “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
Ancak görselleştirmenin yatay yapılmasının daha doğal olacağını öneriyor
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ı
strace ile çalıştırılırsa hatanın hangi syscall'da oluştuğunun hemen görülebileceğini tavsiye ediyor
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