6 puan yazan GN⁺ 2024-02-03 | 1 yorum | WhatsApp'ta paylaş
  • Linux'ta küçük bir C hello programı bile bir ELF çalıştırılabilir dosyası olur ve iç yapısı readelf, nm, objdump ile doğrudan incelenebilir
  • Çalıştırılabilir dosyaları anlamanın üç temel ekseni semboller, bölümler ve segmentlerdir; bunlar sırasıyla işlev bağlama, kod/veri ayrımı ve çalıştırma anındaki bellek yerleşiminden sorumludur
  • objdump ve readelf ile .text, .rodata, .data, .bss, .interp gibi bölümlerin baytları ve özellikleri görülebilir
  • Program doğrudan main içinde başlamaz; önce _start noktasına girer, çeşitli başlangıç işlemlerinden geçer ve ardından main çağrılır
  • Çalıştırılabilir dosya “okunamaz bir yığın” değil, tanımlı bir dosya biçimidir; bu yüzden araçlarla kod, dizge ve linking bilgileri adım adım izlenebilir

Çalıştırılabilir dosya okunabilir bir dosya biçimidir

  • Derlenmiş bir çalıştırılabilir dosya ilk bakışta okunamayan “büyülü bir ikili” gibi görünse de aslında anlaşılabilir bir dosya biçimidir
  • Örnek Linux'taki ELF ikili dosyası üzerinden anlatılır ve ikili dosyalar platforma bağımlı olduğu için açıklama da platforma bağlıdır
  • Kullanılan örnek şu C programıdır
#include <stdio.h>

int main() {
    printf("Penguin!\n");
}
  • gcc -o hello hello.c ile derlenip hello çalıştırılabilir dosyası oluşturulduktan sonra içi incelenir
  • Genel akış üç kavram etrafında ilerler
    • Semboller (symbols): printf gibi başka yerde tanımlı işlevleri çağırırken konumu bulmak için kullanılır
    • Bölümler (sections): kod ve veriyi ayıran birimlerdir; .text, .data, .rodata gibi örnekleri vardır
    • Segmentler (segments): bölümleri çalıştırma anındaki bellek yerleşimi birimleri olarak gruplar

Metin olarak açıldığında da ipuçları görünür

  • cat hello gibi bir komutla çalıştırılabilir dosya doğrudan açılırsa çoğu çıktı bozuk karakterler gibi görünür
  • Yine de çıktının içinde Penguin! ve ELF gibi dizgeler bulunabilir
  • ELF, bu ikili dosyanın dosya biçiminin adıdır
  • Çıktının büyük bölümünün insan tarafından okunmasının zor olmasının nedeni, çalıştırılabilir dosyanın ikili veri içermesidir

Sembol tablosu ile işlev adları ve bağlantılar görülebilir

  • readelf --symbols hello, çalıştırılabilir dosyanın sembol tablosunu gösterir
  • Örnek çıktıda önemli semboller yer alır
    • main: yazılan main() işlevinin adresi
    • puts@@GLIBC_2.2.5: kodda çağrılan printf ile ilişkili bir başvuru gibi görünür; derleyicinin optimizasyonla bunu putsa çevirdiği düşünülür
    • _start: program başlangıcıyla ilgili önemli bir sembol
  • Program doğrudan main içinde başlamaz; gerçekte _start noktasına girer
  • _start birçok önemli işi yapar ve bunlardan biri de main çağrısıdır

Semboller bağlamayı mümkün kılar

  • Programa hello adında bir işlev yazılırsa derlenmiş ikili dosyada bu işlevin koduna hello adlı bir sembol eklenir
  • printf gibi bir kütüphane işlevini çağırmak için o işlevin kod konumunu bulmanın bir yolu gerekir
  • İşlev konumunu bulma süreci linking olarak adlandırılır
    • Derlemeden hemen sonra olursa statik linking
    • Program çalışma anında olursa dinamik linking
  • libc, C standart kütüphanesi işlevlerini içerir
  • nm libc için “no symbols” yazsa bile objdump -tT /lib/x86_64-linux-gnu/libc-2.15.so ile semboller görülebilir
  • libc'nin sembol tablosunda sprintf, strlen, fork, exec gibi işlevler bulunabilir
  • hello programının puts çağırıp libc'nin sembol tablosundan puts konumunu bulduğu akış üzerinden dinamik linkingin nasıl çalıştığı hayal edilebilir

Bölümler kodu ve veriyi ayırır

  • objdump -s hello, çalıştırılabilir dosyanın her bölümündeki baytları onaltılık ve ASCII olarak gösterir
  • Başlıca bölümler şunlardır
    • .text: programın gerçek kodunu, yani assembly'yi içerir; _start ve main burada bulunur
    • .rodata: salt okunur veriyi içerir; örnekte "Penguin!" dizgesi burada yer alır
    • .interp: dinamik bağlayıcı dosyasının adını içerir
  • Bölümler ile segmentler farklı aşamalarda kullanılır
    • Bölümler, linking aşamasında ld tarafından kullanılır
    • Segmentler, çalıştırma anında kullanılır
  • readelf --sections hello ile bölümlerin meta verisi daha ayrıntılı görülebilir
  • Örnekteki bayraklar her bölümün niteliğini gösterir
    • .text: çalıştırılabilir ve salt okunur
    • .rodata: salt okunur
    • .data: okunabilir/yazılabilir
    • .bss: yazılabilir veri alanı

Disassembler ile makine kodu assembly olarak görülebilir

  • .text bölümü, CPU'nun kod olarak yorumlayıp çalıştırdığı baytları içerir
  • Örnekte .text bölümünün başlangıç baytı olan 31 ed, anlamı doğrudan anlaşılmadığı için bir disassembler gerektirir
  • objdump -d ./hello, .text bölümünü disassemble ederek assembly komutları olarak gösterir
  • Örnek çıktıda 31 ed, xor %ebp,%ebp olarak gösterilir
  • Bu yöntemle ikili dosya içindeki kod baytlarının hangi assembly komutlarına karşılık geldiği görülebilir

Segmentler çalıştırma anındaki bellek yerleşimini belirler

  • Çalıştırılabilir dosya segmentler ya da program başlıkları (program headers) ile de tanımlanır
  • readelf --segments hello, programın segmentlerini ve bölüm-segment eşlemesini gösterir
  • Segmentler, programın her parçasının bellekte nasıl ayrılıp yerleştirileceğini belirlemek için kullanılır
  • Örnekte iki ana LOAD segmenti vardır
    • İlk LOAD: R E ile işaretlenir; okunabilir ve çalıştırılabilir durumdadır
    • İkinci LOAD: RW ile işaretlenir; okunabilir ve yazılabilir durumdadır
  • .text okunup çalıştırılmalıdır ama yazılmamalıdır; bu yüzden ilk segmente girer
  • .data ve .bss yazılabilir olmalıdır ama çalıştırılmaları gerekmez; bu yüzden ikinci segmente girer

Daha fazla inceleme için araçlar ve kaynaklar

1 yorum

 
GN⁺ 2024-02-03
Hacker News görüşleri
  • Başka bir başlıkta da söylediğim gibi https://news.ycombinator.com/item?id=38847750#38862450, ELF’yi bir kez elle yazmayı şiddetle tavsiye ederim
    Çalıştırılabilir dosyaların temel bileşenlerini anlamak için iyi bir alıştırma; ayrıca bu yazının tersine yukarıdan aşağı değil, aşağıdan yukarı yaklaşmak istediğinizde de yardımcı olur
    O diğer HN yazısındaki çeşitli başlıklarda da çok iyi tartışmalar var

    • Yakın zamanda bir ELF dosyasını doğrudan yazmayı denedim: https://github.com/avik-das/garlic/blob/master/recursive/elf...
      Biçimi kendime ve başkalarına açıklamak için dosyanın baytlarını gösteren etkileşimli bir görselleştirme de yaptım
      Baytlara tıklayınca açıklama çıkıyor ve dosyadaki ilgili baytlar vurgulanarak anlamayı kolaylaştırıyor: https://scratchpad.avikdas.com/elf-explanation/elf-explanati...
    • Benzer şekilde basit bir ELF yükleyici yazmayı da tavsiye ederim
      Dinamik bağlama tarafında epey uygulama karmaşıklığı var, ama yalnızca statik ELF desteği veriyorsanız oldukça anlaşılır
    • Mevcut bir ELF’yi değiştirmek de son derece öğretici ve eğlenceli
      Başta çalışmadığında hata ayıklamak fiilen imkânsız olduğu için sinir bozucu, ama sonunda çalışmaya başlayınca gerçekten harika oluyor
      ELF ilginç şekillerde yamalanabiliyor ve yardımcı vektör sayesinde çalışma sırasında kendini gözlemlemek de mümkün
      Linux program başlık tablosunun adresini verdiği için oradan her yere ulaşabilirsiniz; LOAD segmentini tüm ikili dosyayı kapsayacak şekilde genişletmek yeterli
      Örneğin Lisp yorumlayıcımın çalıştırılabilir dosyasının içine Lisp modülleri ve kodunu doğrudan yerleştiren bir araç yaptım
      Eklenen segment ELF tarafından otomatik olarak yükleniyor, yorumlayıcı da onu bulup çalıştırıyor
      Bu küçük özelliği o kadar sevdim ki hakkında bir yazı da yazdım: https://www.matheusmoreira.com/articles/self-contained-lone-...
      Ana akım dillerin de bu yöntemi benimsemesini isterdim
    • Kendiniz yaptığınızda bu aslında neredeyse elle assembly yazmak gibi
      Belgeleri okuyup, işlemci veri sayfasından gerekli baytları seçip, bunları çeşitli bölümlere sırayla yerleştirip ELF alanlarını dolduruyorsunuz; sonuçta iş hepsini tek tek girmeye varıyor
      ELF öncesi dönemin 8 bit Apple II gibi ortamlarında, makine dili monitörü program baytlarını doğrudan girmenize izin verirdi ve o baytlar çalıştırılırdı
      Diske kaydetmek yalnızca biraz daha karmaşık; burada da yine fırsatlar var
      Bir disk sektör düzenleyiciyle dosya oluşturabilirsiniz, süreç böyle devam eder
    • Chris Wellons’ın A Magnetized Needle and a Steady Hand yazısı, sıfırdan ELF çalıştırılabilir dosyası yapmayı anlatıyor: https://nullprogram.com/blog/2016/11/17/
  • Benim anladığım kadarıyla main sembolü C’ye özgü
    _start sembolü dilden bağımsız ikili giriş noktasıdır ve bu durumda maini çağırır
    Giriş noktasına _start denmesi ve buraya mainin argc/argv değerlerinin geçirilmesi gibi bir teamül olsaydı, biçimin esnekliği çok daha düşük olurdu

    • Kesin konuşmak gerekirse _start adı da özel değil
      İkili dosya başlığında giriş noktası adresini yazar, işletim sistemi de çalıştırmayı o adresten başlatır
      O sembole _start demek yalnızca C ve diğer dillerin bir teamülüdür; bağlayıcı ELF başlığını yazarken giriş noktasını ayarlamak için bunu kullanır
      Kendi bağlayıcı betiğinizi yazarsanız giriş noktasına istediğiniz adı verebilirsiniz
    • main sembolü, doğru biçimde, yalnızca hosted C’de sağlanır
      freestanding C’de istediğiniz giriş noktasına sahip olabilirsiniz
      _start da yalnızca bağlayıcının varsayılanıdır; -Wl,--entry="${symbol}" ile daha iyi bir sembol belirtebilirsiniz ve GCC çirkin görünen -Wl olmadan bunu doğrudan ayarlamayı da destekler
      Ayrıca giriş noktası aslında bir sembol değil, işaretçidir
      Bağlayıcı yalnızca belirtilen sembolün adresini alıp ELF giriş noktası olarak ayarlar
      Yığında argüman sayısı ve argüman vektörünün yanı sıra ortam vektörü ve yardımcı vektör de bulunur
      Süreç başlangıç kodu bu değerleri yığından çıkarıp uygun yazmaçlara koymak ve istediği C fonksiyonunu çağırmak kadar basit olabilir
      Giriş noktasının kendisi bir fonksiyon değildir; bu yüzden dönecek bir yer yoktur
      Giriş noktası kodu, main bir durum kodu döndürdüğünde sürecin temiz biçimde sonlanması için exit sistem çağrısıyla bitmelidir
      En azından Linux’ta bu şekilde çalışır
    • Dil çalışma zamanına bağlı olarak değişir, ama yaygın işlerden biri sıfır olmayan global statik değerleri ilklendirmektir
      Rust/C/C++ gibi dillerde, bağlayıcı bayrakları aracılığıyla ilklendirilecek değişkenleri enjekte etmek de mümkündür
      Program dinamik olarak bağlanmışsa, bildiğim kadarıyla _starttan önce bağlayıcı çalışma zamanı çalışır, bağlantıları çözer ve sonra denetimi _starta devreder
      Sonuçta bunlar, genişletilebilirlik sağlamak için organik olarak üst üste eklenmiş hack üstüne hackler; toplumsal olarak yeterince kabul gördükleri ve yeterince iyi çalıştıkları için kullanmaya devam ediyoruz
  • 2012’de akademik rotamı matematikten bilgisayar bilimine çevirirken blog yazmaya başlamıştım; bu konu da kelimenin tam anlamıyla ilk çalıştığım şeydi: https://heinrichhartmann.com/archive/Dissecting-Hello-World....
    Bu derin tavşan deliğine girdiğime hiç pişman olmadım
    Yanlış hatırlamıyorsam Julia’nın da matematik geçmişi var
    Belki de matematik kökenli insanların böyle deneylere çekilmesinin nedeni, en temelden akıl yürütme isteğidir
    Onun bu konuyu daha fazla kişinin erişebileceği hale getirmesi sevindirici

  • Julia’nın yazıları her zaman mükemmel
    Derlenmiş kodun sır saklayamayacağını anlatırken strings demosu yapmak her zaman çok etkili olmuştur

    • Bunu Alman hâkimlere de böyle açıklamak gerek
      Zavallı bir kişi, bir binary üzerinde strings çalıştırmaya benzer bir yolla parola bulduğu için para cezası aldı
      Hâkimler onun yazılımın güvenlik önlemlerini “aştığını” düşündü: https://www.theregister.com/2024/01/19/germany_fine_security...
  • Ne eleştiri ne de kusur bulma; sadece aklıma gelen bir düşünce
    “Binary’ler neredeyse platforma özgü olmanın tanımı olduğu için, buradaki her şey de platforma özgü” cümlesini görünce, Actually Portable Executable’ın aynı binary’nin birden çok platformda çalışabileceğini gösterdiği anı hatırladım
    O gerçeküstü andan hâlâ zihinsel olarak tamamen toparlanamadım
    On yıllar boyunca Java, çapraz platform kütüphaneleri ve benzeri türlü fraktal yöntemlerle çapraz platform sorununu çözmeye çalıştık; meğer çözüm başından beri burnumuzun dibindeymiş

    • Kişisel olarak taşınabilir binary’lerin net etkisinin iyi olduğundan emin değilim
      Hızlı bilgisayarlar çağında, kaynak dağıtımı ve yerel derlemenin binary dağıtımından daha iyi olduğunu düşünüyorum
      Ne yazık ki bağımlı olduğumuz yazılımların önemli bir kısmı çok büyük, derleyiciler de görece yavaş; bu yüzden binary dağıtımı bir tür zorunlu kötülük haline geliyor
      Taşınabilir binary’ler yerine, çabaların doğal olarak hızlı derlenen daha basit yazılım bileşenlerine ve daha hızlı derleyicilere yönelmesini isterdim
    • Yanlış anlamış olabilirim ama APE’nin kendi başına bir binary biçimi olduğunu sanmıyorum
      Her sistemde çalışabilen bir betik ve bu betik bir binary’yi yükleyebiliyor
      Yanlış hatırlamıyorsam ilk sürümün yüklemeden önce base64’ten decode edilmesi gerekiyordu
      Yani daha çok çalıştırılabilir bir binary loader gibi
  • 1990’ların başında yürütülebilir dosya biçimlerine kapıldım; birkaç hafta boyunca Modula 2 ile DOS ve Windows yürütülebilir dosya görüntüleyicisi yazdım, adını VEXE koydum ve 1991’de shareware olarak dağıttım
    Bu araç cracker’lar arasında niş bir popülerlik kazandı; +ORC tutorial’larında anılacak kadar: https://gist.github.com/callowaysutton/48bdf0245e17e72d41a15...
    Muhtemelen programların tersine mühendisliğini engellemek için kullanılan çeşitli şifreleme ve sıkıştırma yöntemlerini tespit edebildiği içindi

  • ELF binary dosyalarının ne kadar küçülebileceğini merak ediyorsanız şu eğlenceli yazı hoşunuza gidebilir: https://www.muppetlabs.com/~breadbox/software/tiny/teensy.ht...

  • ELF’yi SQL ile keşfetmenizi sağlayan aracımı da kontrol edebilirsiniz: https://github.com/fzakaria/sqlelf

  • Python geçmişi güçlü olan biri için pratik bir düşük seviyeli programlama başlangıç kaynağı ya da kitap önerebilir misiniz
    Yakın zamanda Rust öğrenmeye başladım ve yetişmem gereken çok şey olduğunu fark ettim
    Hiç derleyici dersi almadığım için birçok bilgiyi kaçırıyor olabilirim
    Örneğin binary’lerde symbol diye bir şey olduğunu bile bilmiyordum; ELF ile Mach-O arasındaki farkı da bilmiyordum

  • Bir binary’yi terminale catlemek üzüntüye giden kestirme yoldur
    Ben | hd kullanmayı seviyorum; fiilen hexdump -C ve çıplak gözle bakınca o da aynı derecede anlaşılmaz