1 puan yazan GN⁺ 2023-07-10 | 1 yorum | WhatsApp'ta paylaş
  • .NET Framework 4.x’te yalnızca "Hello World" çıktısı veren bir çalıştırılabilir dosya için, normal C# derlemesinin 4,6KB olan sonucu elle oluşturulmuş PE yapısıyla 834 bayta kadar küçültüldü
  • Deney; yönetilen giriş noktası, Windows’ta çalıştırılabilir tek dosya, yalnızca BCL veya standart Windows kurulum kütüphanelerinin kullanımı ve dosya sonundaki 0 baytların hariç tutulması koşullarıyla yapıldı
  • Boyut küçültme; nullable referans tipi anotasyonlarını ve gereksiz metaverileri kaldırarak, 64 bit PE’ye geçip imports ve base relocations bölümlerini ortadan kaldırarak ilerledi
  • Console.WriteLine yerine P/Invoke ile ucrtbase!puts çağrılarak UTF-16 #US akışı kaldırıldı; ASCII dizesi ise PE başlığı ile ilk bölüm arasındaki padding alanına taşındı
  • Son çalıştırılabilir dosya, trailing zero’lar hariç 834B(0x342), ZIP ile sıkıştırıldığında 476B boyutuna inerek .NET PE ve CLR metaverisinin asgari yapı sınırlarını gösteriyor

Deney koşulları ve başlangıç noktası

  • Amaç, CLR’ın standart çıktıya "Hello World" yazdırmasını sağlamak için bir .NET çalıştırılabilir dosyasına yalnızca gerekli asgari baytları koymak
  • Deney kuralları şöyleydi
    • Uygulama, C# veya CIL ile uygulanmış bir yönetilen giriş noktası çalıştırmalı
    • Hedef çalışma zamanı .NET Framework 4.x.x olmalı
    • Üçüncü taraf bağımlılıklar yasak; yalnızca BCL olan mscorlib veya standart Windows kurulum kütüphaneleri kullanılabilir
    • PE dosyası ve CLR’ın hizalama kısıtları nedeniyle Windows 10 ve üzeri sürümlerde çalışabilen teorik en küçük .NET PE dosyası 1KB’den küçük olamaz; ancak dosya sonundaki trailing zero bytes boyut hesabına katılmaz
  • Sıradan bir C# Hello World programı net472, C# 10 ve nullable enabled ayarıyla derlendiğinde çalıştırılabilir dosya boyutu 4,6KB olur
  • Nihai kaynak kod Full Source Code içinde yer alıyor

C# derleme sonucunda küçültülebilecek kısımlar

  • C# 8.0’daki nullable referans tipleri, çalıştırılabilir dosyaya custom attribute biçiminde statik olarak eklenir ve bu attribute’lar boyutu ciddi ölçüde artırır
  • .csproj içinde <Nullable>disable</Nullable> ile nullable referans tipi denetimi kapatılınca attribute’lar kaldırılır; ancak PE dosyası hizalaması nedeniyle toplam dosya boyutu hâlâ 4,6KB kalır
  • Decompile sonucu, C# derleyicisinin nullable kapalı olsa bile gereksiz metaveri üretmeye devam ettiğini gösterdi
    • Assembly dosya sürümü, telif hakkı bilgisi gibi assembly attributes
    • Program sınıfı dışındaki gizli <Module> tipi
  • Aşırı küçültme için daha fazla C# derleyici ayarı aramak yerine, AsmResolver ile .NET çalıştırılabilir dosyasını doğrudan oluşturmaya karar verildi
  • Elle oluşturulan modül, main fonksiyonunu <Module> tipine koyuyor ve yalnızca Ldstr "Hello World", Console.WriteLine, Ret içeren bir CIL gövdesi kuruyor
  • Bu yöntemle dosya boyutu, öncekinin yaklaşık yarısına indi

64 bit PE ile imports ve base relocations kaldırma

  • Oluşturulan çalıştırılabilir dosyada .text ve .reloc adlı iki bölüm var; Imports ve Base Relocations veri dizinleri büyük yer kaplıyor
  • AnyCPU veya 32 bit .NET çalıştırılabilir dosyalarında yönetilmeyen giriş noktasının mscoree!_CorExeMain çağırması gerektiğinden imports dizini gerekir
  • Varsayılan .NET çalıştırılabilir dosyası yeniden konumlandırılabilir (relocatable) olduğundan, bu imported function çağrısı için base relocation da gerekir
  • Base relocation varsayılan olarak ayrı bir bölüme konur ve her bölüm en az 0x200 bayt birimlerle hizalandığı için dosya en az 1KB’nin üzerine şişer
  • 64 bit .NET çalıştırılabilir dosyalarında yönetilmeyen giriş noktası gerekmediği için aşağıdaki ayarlarla iki dizin ve .reloc bölümü kaldırılabilir
    • module.PEKind = OptionalHeaderMagic.PE64
    • module.MachineType = MachineType.Amd64
  • Bu aşamada geçerli bir .NET PE dosyasının teorik asgari boyutu olan 1KB’ye ulaşıldı

Metaveri adlarını, GUID’i ve public key token’ı kaldırma

  • Bundan sonraki boyut ölçümü, Windows Explorer’daki dosya boyutuna göre değil, hex editor içinde son non-zero byte’tan sonraki konuma göre yapıldı
  • 1KB’lik PE dosyası, bu kritere göre zaten 991 bayta(0x3DF) kadar küçülmüş durumdaydı
  • #Strings heap’i, .NET ikili dosyalarında ikinci en büyük metaveri akışıdır ve tablo akışı #~ tarafından kullanılan tip ve metot adlarını saklar
  • Çalışma zamanı için önemli olmayan adlar null yapılınca program normal çalışmaya devam eder; bu işlemle 32 bayt tasarruf edildi
  • #GUID akışı, Module tablosundaki Mvid tarafından referans verilen GUID’i içerir; ancak deneyde sürüm kimliğine ihtiyaç olmadığı için kaldırıldı
    • Mvid için 16 bayt kaldırıldı
    • Boş #GUID akışının tamamı ve 16 baytlık başlığı da kaldırıldı
    • Toplam 32 bayt azaltıldı
  • Console::WriteLine çağrısı için referans verilen mscorlib genelde 8 baytlık public key token taşır
  • Public key token olmadığında CLR bu assembly token’ının gerçekliğini doğrulamaz; deneyde bu da kaldırıldı
  • Bu aşamanın ardından dosya boyutu 918 bayt oldu

Console.WriteLine kaldırma ve ASCII dizesi kullanma

  • "Hello World", .NET’in #US metaveri akışında saklanır ve uzunluk prefix’i olan 16 bit karakter dizisi ile ek bir 0 bayt olarak temsil edilir
  • Yazdırılacak dizenin tüm karakterlerinin code point değeri 0xFF’ten küçük olduğundan 1 baytlık ASCII ile temsil edilebilir; ancak CLR’a #US akışındaki dizeleri ASCII olarak yorumlatmanın bir yolu yoktur
  • Console::WriteLine yerine yönetilmeyen ucrtbase!puts fonksiyonu P/Invoke ile çağrıldı
    • C# bildirim biçimi: [DllImport("ucrtbase")] static extern int puts(nint str);
    • puts, argüman olarak zero-terminated ASCII dize işaretçisi alır
  • Derleme zamanında dizenin runtime adresini bilmek için PE optional header’daki DllCharacteristics içinde DynamicBase bayrağı kapatıldı
    • Sabit image base ayarlandı
    • ASCII dizesi .text bölümü içine yerleştirildi
    • Runtime adresi module_base_address + rva_ascii_string olarak hesaplandı
  • CLR’ın bu bayrağa uyması için .NET data directory’deki ILOnly bayrağı da kapatıldı
  • Bu değişiklikle wide-character dize, #US akışı ve System.Console::WriteLine referansı kaldırıldı; yerine yalnızca puts tanımı ve P/Invoke metaverisi eklendi
  • Sonuç boyutu 889 bayt(0x379) oldu

Son mikro optimizasyonlar

  • puts’un standart tanımı, yazdırdığı karakter sayısını int32 olarak döndürür; ancak program bu değeri kullanmaz
  • 64 bit PE’de Microsoft’un x64 calling convention’ına göre dönüş değeri .NET değerlendirme yığınına değil, RAX register’ına konur
  • P/Invoke tanımı void dönüşlü yapılınca main metodunda dönüş değerini kaldırmak için gereken pop komutu ortadan kalkar
  • PE header boyutu 0x200’den küçük olduğu için header ile ilk bölüm arasında padding data oluşur
  • Windows PE Loader, PE header’ı okunabilir belleğe map eder ve bu aralığa padding data da dahildir
  • ASCII dizesi bu padding alanına taşınınca dosya 13 bayt daha küçültülebildi
  • Artık Console::WriteLine referans edilmediği için mscorlib referansı da kaldırıldı
    • Tables stream #~ içindeki bir tablo azaldı
    • #Strings akışından mscorlib adı da kaldırıldı
  • .NET metadata directory’deki VersionString varsayılan olarak "v4.0.30319" değerindedir ve 4 bayt katına padding uygulanarak toplam 12 bayt yer kaplar
  • Bu dize v4.0. değerine kadar kısaltılsa da CLR 4.0 boot eder ve program çalışır; padding sonrasında toplam 4 bayt daha azaltılır
  • Sondaki . gerekli göründü; daha fazla kısaltılınca program düzgün boot etmiyor

Nihai sonuç

  • Nihai ikili dosya boyutu, trailing zero’lar hariç 834 bayt(0x342)
  • ZIP ile sıkıştırıldığında 476 bayt oluyor; 1KB sınırına ulaştıktan sonra ek optimizasyon yapılmadığındaki 582 bayttan daha küçük
  • Deney, C# derleyicisinin ürettiği 4,6KB Hello World’den başlayıp, doğrudan oluşturulmuş 834B PE dosyasına kadar indi
  • Nihai program hâlâ normal şekilde "Hello World" çıktısı veriyor
  • Bu çalışma, pratik faydadan çok .NET modül dosya formatı ve CLR metaverisinin asgari yapı sınırlarını araştıran bir deney niteliğinde

1 yorum

 
GN⁺ 2023-07-10
Hacker News yorumları
  • En küçük çalıştırılabilir program türündeki deneyler gerçekten harika
    Michal Strehovský, bugün .NET olarak bilinen framework’ü kullanarak .NET runtime bağımlılığı olmadan 8 KB’tan küçük bir Snake oyunu yapmıştı
    Medium pek iyi değil ve Twitter da şu an pek kullanışlı değil ama kabaca “hello world”ün Windows 3.11’i bile desteklediğini hatırlıyorum
    https://medium.com/@MStrehovsky/building-a-self-contained-ga... [https://web.archive.org/web/20200103110836/https://medium.co... | https://archive.ph/b6qXE]
    https://news.ycombinator.com/item?id=22010159 (2020)
    https://news.ycombinator.com/item?id=22104734
    Güncel çalışmalar ve sponsorluk https://flattened.net üzerinde görülebilir: “bflat - C# as you know it but with Go-inspired tooling”

    • “Go-inspired” C# konusundan devam edersek, .NET’te System.Threading.Channels var
      https://learn.microsoft.com/en-us/dotnet/core/extensions/cha...
    • C# uygulamaları, kullanıcıların indirme süresini anlık hissedeceği kadar küçülebilirse, bunun C# WebAssembly’yi daha gerçekçi kılmak için kullanılıp kullanılamayacağını merak ediyorum
      Hatırladığım kadarıyla indirme boyutunun önemli bir kısmı .NET kütüphanesinin kendisiydi
      Tarayıcıyı hedefleyen, C# kullanan ve küçük ikililer üreten bir 2D oyun framework’ü/motoru olup olmadığını da merak ediyorum
    • Michal ve CoreRT ekibi sayesinde C#’a yeniden baktım; bugün NativeAOT olmasaydı, özellikle Go varken C#’ı kesinlikle düşünmezdim
  • Bu yazı kafa karıştırıcı olabilir. Söz konusu çalıştırılabilir dosya sistemde kurulu runtime’a ihtiyaç duyuyor ve aslında 2023’te .NET Framework 4.7.2 kullanıyor
    Bu yöntemle adil bir karşılaştırma yapmak için .jar veya .py dosyaları muhtemelen daha uygun olur
    Gerçek çalıştırılabilir dosya ölçütü olarak Windows, Linux ve macOS’ta çalışabilen Native AOT ikilileri daha uygun
    Ancak GC, ThreadPool, Console gibi yardımcı kodları da içermesi gerektiğinden çok daha büyük olması doğal

    • Mutlaka öyle değil. Gerçek çıktı exe, main çağrısı dışında .NET Framework’ü fiilen kullanmıyor
      Konsola bayt yazma mantığı, ucrt.dll’in sunduğu yönetilmeyen koda yapılan pinvoke çağrısıyla işleniyor
      Başka bir yorumda paylaşılan Snake küçültme yazısı, Windows’la birlikte gelmeyen yeni .NET, yani .NET Core temelinde aynı şeyi yapıyor ve gerçekten .NET olmayan bir ikili üretiyor
      [0]: https://medium.com/@MStrehovsky/building-a-self-contained-ga...
    • Tamamen statik linklenmiş yerel bir ikiliyle, kütüphanelerin bir kısmını veya tamamını dinamik olarak yükleyen bir ikiliyi karşılaştırmak da oldukça adaletsiz
      Konteyner imajlarını karşılaştırmak iyi bir başlangıç gibi görünüyor. Mümkün olan en küçük temel imajı kullanmak ya da mümkünse “FROM scratch” kullanmak, ama yalnızca aynı mimariler arasında karşılaştırmak gerekir
      64 bitte 32 bit çalıştırma veya pointer sıkıştırma gibi değişkenleri devre dışı bırakmak için 32 biti tercih ederim
      Ardından sıkıştırılmamış export tar boyutunu karşılaştırmak iyi olur. Tamamen adil olmasa da bariz değişkenleri azaltabilir
      Biraz daha düşününce, konteyner içi sıkıştırmayı da yansıtmak için azami sıkıştırılmış imaj boyutunu karşılaştırmak daha adil olabilir. Elbette aynı algoritma ve parametreler kullanılmalı ya da sıkıştırma benchmark’larında olduğu gibi açıcı programın boyutu nihai sonuca eklenmeli
    • Benim hello world’üm “bash” framework’üne ihtiyaç duyuyor ve boyutu 0 bayt
      Şöyle çağırmak yeterli: echo "hello world"
    • .NET Framework Windows’a gömülü geliyor ve en azından Vista’dan beri zorunlu bir bileşendi
      Özellikle .NET Framework 4.7.2, desteklenen Windows sürümlerinde varsayılan olarak bulunuyor olmalı
    • Önemsememiz gereken şeyin “adillik” mi “gerçeklik” mi olduğundan emin değilim
      Güncel bir Windows kurulumunda .NET’in olmaması mümkün değil; Linux ve macOS’ta ise varsayılan olarak yok. Bunu adillik ölçütü yapmak garip görünüyor
  • Bu arada, yazarın konumunun Antarktika McMurdo Station olduğunu fark eden var mı merak ediyorum
    O üste böyle bir teknoloji yığınına sahip birinin olacağını düşünmezdim: https://usscar.org/directory?combine=&field_usscar_nsf_progr...
    Epey ilginç bir hikâye olabilir

    • Bir araştırma gezisi için McMurdo'ya gitmiştim
      Dışarıdan gelenleri şaşırtan şey, oradaki insanların çoğunun destek personeli olması; bilim insanları küçük bir oran, muhtemelen yalnızca %20 civarında
      Destek personeli; itfaiyeci, marangoz, aşçı, tamirci vb. modern bir şehri işletmek için gereken her tür meslekten geliyor
      Seçim süreci yüzünden mi bilmem, neredeyse hepsi çok yetenekli ve kendi alanlarının en üst düzeyinde. Geniş beceri setine sahip İsviçre çakısı tipi insanlar şaşırtıcı derecede çoktu
      Orada yaşamanın ve çalışmanın zorlukları, benzersiz ve motivasyonu yüksek insanları ışığa üşüşen böcekler gibi kendine çekiyor; pek çok kişi her yıl geri dönüyor. Harika bir topluluk
    • Bu durumda "cumartesiyi aptalca harcadım" cümlesini anlamıyorum
      Orada başka ne yapabilirsin ki?
    • https://brr.fyi/'ye bakmak iyi olur
    • Bunu nasıl bulduğunu merak ediyorum
      .NET metadata'sında ya da about kısmında görünmüyor
  • Tüm bölümleri mümkün olan en küçük bölüm hizalama değeri olan 0x200 bayta hizalamak gerektiği için dosyanın en az o kadar şiştiğini söylemişti; ama 0x200, 512 bayt, yani 0,5KB
    Bu düzeyde PE boyutu optimizasyonunu uzun zaman önce denemiştim; hatırladığım kadarıyla Windows 9x için 512 minimumdu, NT ise 1'e kadar küçük hizalamaları kabul edebiliyordu
    Eskiden en güncel teknik sayılan DOS MZ ve PE başlıklarını üst üste bindirme hilesini de göremedim: http://www.phreedom.org/research/tinype/
    Bir de demoscene'de bunun 1KB ikili dosya içinde yapıldığını fark ediyorsunuz:
    https://www.pouet.net/prodlist.php?type%5B%5D=1k&platform%5B...

  • Bu kesinlikle zamanı aptalca harcamak değil
    Bir ikili dosyanın ayrıntılarına daldığınızda, nasıl oluşturulduğuna dair derin bir içgörü kazanırsınız; orada öğrendiğiniz kavramlar da bilginizi zenginleştirir
    İlginizi çekiyorsa ve bir şey öğreniyorsanız, harcanan zamana ve emeğe her zaman değer

  • Şu çok daha ilgili ve ilginç: https://twitter.com/MStrehovsky/status/1669502394827419648
    İçeriği şu: "GC dahil her şeyi içeren, tamamen self-contained native olarak derlenmiş bir C# Hello World yaklaşık 440KB'a kadar küçülebilir"
    .NET classic framework, Windows'un artık önerilmeyen legacy tarafı; yeni uygulamalar onun üzerine inşa edilmemeli

    • Ortaya çıkan exe, yalnızca yönetilmeyen pinvoke çağrılarıyla açığa çıkarılan alt seviye ucrt.dll kitaplığının uygulama ayrıntılarına dayanıyor; framework'ü fiilen "kullanmıyor"
    • Office/COM API'lerine bağımlı bir uygulama yapıyorsanız bu bir istisna olabilir
  • "Gerçek" bir ikili dosyadan, yani .NET AOT gibi native derlenmiş bir .NET çalıştırılabilirinden bahsedileceğini ummuştum
    https://learn.microsoft.com/en-us/dotnet/core/deploying/nati...

  • Tebrikler. Unicode desteğini bırakıp puts için bir C çağrısı oluşturmuş oldunuz
    Doğrudan bir C derleyicisiyle derlemek daha kolay olmaz mıydı?
    thats_just_c_with_extra_steps.jpg

    • Derlemeyi fiilen atlamak biraz heves kırıcı
  • Oberon+ IDE ile az önce denedim; bu modülden 2048 baytlık Hello.dll çıktı
    module Hello
    begin
    println("hello world")
    end Hello
    https://github.com/rochus-keller/Pelib assembly üreticisini kullanıyor

    • Nicklaus Wirth haklıydı
      Lazarus/FreePascal'ı ilk kullandığımda böyle hissetmiştim; yazdıklarını daha çok okudukça bunun daha da doğru olduğunu düşündüm
      Son zamanlarda bu düşünce biçiminin "reenkarnasyonundan" çıkış yolunun WASM olabileceğini düşünüyorum, ama donanımdan bu kadar vazgeçemem
      Masamın üstünde çekiçle parçalayamayacağım ruhani bir makineyi kabul etmek için fazla ilkelim
      Başkalarının donanımında binlerce dolarlık hesaplama zamanı harcayarak çalışan ve RPi'den daha yavaş olan Ethereum gibi yok edilemez küresel bilgisayarlar, temel bir düzeyde bana tabu gibi geliyor
      Hâlâ Wirth'ün yüzüyle birlikte "Wirth was right" yazısını bir tişörte koymak istiyorum
    • Oberon+ ise self-hosting olması gerekmez mi diye düşünüyorum
      C++ ekosisteminin tortusu çok fazla
      GitHub ve GitLab'e alternatif diye tanıtılan açık kaynak yazılım barındırma forge projelerini akla getiriyor. Kodu clone etmek ya da bug bildirmek için gidince hepsi GitHub'da çıkıyor
    • Dosyanın sonunun 0 baytlarla doldurulup doldurulmadığını merak ediyorum
      Bunu devre dışı bırakacak bir seçenek olmalı
  • Bir .exe dosyasının 1 KB’ın altına kadar küçültülebileceğini görmek ufuk açıcıydı
    Birisi Flutter’ın Hello World .exe’sini 1 MB’a kadar küçültebilirse gerçekten harika olurdu

    • İlk adım, Microsoft’u Windows’a Flutter’ı dahil etmeye ikna etmek olurdu gibi
    • Dart ile yalnızca "Hello world" derlemenin kabul edilip edilmeyeceğini merak ediyorum