Çekirdek İçerisinde
Artık ELF dosyasının yapısını, segmentleri, section'ları ve execution zincirinin temel mantığını öğrendik. Bundan sonraki aşamada ise çekirdeğin (kernel) içerisinde execve() sistem çağrısının nasıl işlendiğini inceleyeceğiz.Aslında Linux'un yaptığı işlem düşündüğünüz kadar karmaşık değildir. Kernel'in temel amacı şudur:
Bütün execution zinciri bunun etrafında şekillenir."Bu dosya gerçekten çalıştırılabilir mi? Eğer çalıştırılabiliyorsa, onu belleğe doğru şekilde yükleyip CPU'ya teslim et."
1. Kullanıcıdan Sistem Çağrısı Gelir
Her şey kullanıcı alanında (User Space) başlar.Program:
Kod:
execve("/bin/ls", argv, envp);
çağrısını yaptığında kullanıcı modundan kernel moduna geçilir.
x86-64 mimarisinde bu çağrı, sistem çağrısı tablosundaki __x64_sys_execve fonksiyonuna ulaşır.
Bu fonksiyon da çekirdeğin asıl execution mekanizmasını başlatır.
2.
Kernel artık çalıştırılacak dosyanın bilgilerini almaya başlar.Burada;
- dosya yolu (filename)
- komut satırı argümanları (argv)
- ortam değişkenleri (envp)
Kernel ilk olarak bu bilgilerin geçerli olup olmadığını kontrol eder.
Örneğin;
- Dosya gerçekten var mı?
- Kullanıcının çalıştırma yetkisi var mı?
- Argümanlar okunabiliyor mu?
3. Binary Formatı Tanınır
Dosya artık açılmıştır.Fakat kernel henüz bunun nasıl bir dosya olduğunu bilmez.
Bunun için search_binary_handler() fonksiyonu çalışır.
Kernel içerisinde farklı dosya formatlarını tanıyan birçok Binary Format Handler bulunur.
Örneğin;
- ELF
- Script (#!/bin/bash)
- a.out
- binfmt_misc ile eklenmiş özel formatlar
Kernel sırayla her işleyiciye dosyayı gösterir.
İşleyici ilk birkaç baytı (Magic Bytes) kontrol eder.
Eğer eşleşme sağlanırsa ilgili yükleyici çalıştırılır.
Hiçbiri dosyayı tanıyamazsa:
Kod:
Exec format error
hatası döndürülür.
4. ELF Yükleyicisi Devreye Girer
Dosyanın ELF olduğu anlaşıldığında kernel:
Kod:
load_elf_binary()
fonksiyonuna geçer.
Burası ELF yükleme mekanizmasının merkezidir.
Bu fonksiyon;
- ELF Header'ı okur.
- Magic Bytes kontrolü yapar.
- 32 bit mi 64 bit mi kontrol eder.
- Mimari uyumluluğunu doğrular.
- Header alanlarının geçerli olup olmadığını denetler.
5. Program Header Okunur
Artık kernel Program Header Table'ı okumaya başlar.Buradaki en önemli segmentler şunlardır:
- PT_LOAD
- PT_DYNAMIC
- PT_INTERP
- PT_GNU_STACK
- PT_GNU_RELRO
6. PT_INTERP Kontrolü
Kernel daha sonra PT_INTERP segmentini kontrol eder.Eğer bu segment varsa program dinamik olarak derlenmiştir.
Örneğin:
/lib64/ld-linux-x86-64.so.2
Bu durumda kernel önce dinamik bağlayıcıyı (Dynamic Linker) belleğe yükler.
Daha sonra çalıştırılacak program da belleğe yerleştirilir.
Statik derlenmiş programlarda (Static Binary) ise bu segment bulunmaz.
Kernel doğrudan programa geçebilir.
7. PT_LOAD Segmentleri Belleğe Yerleştirilir
Execution zincirinin en önemli aşamalarından biri burasıdır.Kernel Program Header'daki her bir PT_LOAD segmentini tek tek işler.
Bunun için elf_map() fonksiyonu kullanılır.
Bu fonksiyon dosya içerisindeki segmentleri sanal belleğe (Virtual Memory) eşler.
Kernel her segment için şu bilgileri kullanır:
- p_offset → Dosyada okunacak başlangıç noktası.
- p_vaddr → Bellekte yerleştirileceği sanal adres.
- p_filesz → Dosyadan okunacak veri miktarı.
- p_memsz → RAM'de ayrılacak toplam alan.
- p_flags → Segmentin izinleri (R, W, X).
- .text
- .rodata
- .data
- .bss
8.
.bss dosyada fiziksel olarak bulunmaz.Bu nedenle kernel:
- p_memsz
- p_filesz
Ayırdığı bu alanın tamamını sıfır (0) ile doldurur.
Böylece başlatılmamış global değişkenler hazır hale gelir.
9. Dinamik Bilgiler Hazırlanır
Kernel daha sonra PT_DYNAMIC segmentini inceler.Bu segment;
- kullanılacak paylaşımlı kütüphaneleri,
- relocation bilgilerini,
- GOT ve PLT tablolarını
Bu bilgiler daha sonra ld-linux tarafından işlenir.
10. Stack Hazırlanır
Program başlamadan önce kernel yeni proses için kullanıcı stack'ini oluşturur.Bu stack içerisine;
- argc
- argv
- envp
Bunun yanında Auxiliary Vector (auxv) adı verilen bazı özel bilgiler de eklenir.
Önemli auxv girişleri şunlardır:
- AT_ENTRY → Programın giriş noktası.
- AT_PHDR → Program Header Table'ın bellekteki adresi.
- AT_PHNUM → Program Header sayısı.
- AT_RANDOM → ASLR için rastgele üretilmiş 16 bayt.
- AT_EXECFN → Çalıştırılan dosyanın adı.
11. CPU Yeni Programa Geçer
Artık her şey hazırdır.Kernel son olarak CPU yazmaçlarını (Registers) ayarlar.
- Eğer program statik derlenmişse;
- Eğer program dinamik derlenmişse;
Dinamik bağlayıcı;
- gerekli kütüphaneleri yükler,
- sembolleri çözer,
- relocation işlemlerini tamamlar.
_start, gerekli başlangıç hazırlıklarını yaptıktan sonra en sonunda main() fonksiyonunu çağırır.
Artık kullanıcı programı çalışmaktadır.
Execution Zincirinin Özeti
Kod:
Bütün bu uzun süreci aslında birkaç adımda özetleyebiliriz:
execve()
↓
Kernel sistem çağrısını alır.
↓
Dosya açılır.
↓
Magic Bytes kontrol edilir.
↓
ELF yükleyicisi seçilir.
↓
ELF Header okunur.
↓
Program Header okunur.
↓
PT_LOAD segmentleri belleğe eşlenir.
↓
PT_INTERP varsa dinamik bağlayıcı yüklenir.
↓
.bss hazırlanır.
↓
Stack ve Auxiliary Vector oluşturulur.
↓
CPU yazmaçları ayarlanır.
↓
Kontrol _start fonksiyonuna geçer.
↓
main()
↓
Program çalışır.
Konunun Temel Felsefesi
Bütün bu süreç ilk bakışta oldukça karmaşık görünebilir. Ancak aslında kernel'in yaptığı işlem tek bir amaca hizmet eder:- Dosyanın gerçekten çalıştırılabilir olup olmadığını doğrulamak.
- Programın ihtiyaç duyduğu tüm bellek alanlarını hazırlamak.
- Gerekli kütüphaneleri yüklemek.
- Stack ve başlangıç bilgilerini oluşturmak.
- CPU'yu doğru giriş noktasına yönlendirmek.
Son düzenleme: