En küçük .NET Hello World ikili dosyası ne kadar küçülebilir?
(blog.washi.dev)- .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.WriteLineyerine P/Invoke ileucrtbase!putsçağrılarak UTF-16#USakışı 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
mscorlibveya 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
.csprojiç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
Programsı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,
mainfonksiyonunu<Module>tipine koyuyor ve yalnızcaLdstr "Hello World",Console.WriteLine,Retiç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
.textve.relocadlı 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
0x200bayt 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
.relocbölümü kaldırılabilirmodule.PEKind = OptionalHeaderMagic.PE64module.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ı
#Stringsheap’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
nullyapılınca program normal çalışmaya devam eder; bu işlemle 32 bayt tasarruf edildi #GUIDakışı, Module tablosundakiMvidtarafından referans verilen GUID’i içerir; ancak deneyde sürüm kimliğine ihtiyaç olmadığı için kaldırıldıMvidiçin 16 bayt kaldırıldı- Boş
#GUIDakışı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 verilenmscorlibgenelde 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#USmetaveri 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#USakışındaki dizeleri ASCII olarak yorumlatmanın bir yolu yoktur Console::WriteLineyerine yönetilmeyenucrtbase!putsfonksiyonu 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
- C# bildirim biçimi:
- Derleme zamanında dizenin runtime adresini bilmek için PE optional header’daki
DllCharacteristicsiçinde DynamicBase bayrağı kapatıldı- Sabit image base ayarlandı
- ASCII dizesi
.textbölümü içine yerleştirildi - Runtime adresi
module_base_address + rva_ascii_stringolarak 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,
#USakışı veSystem.Console::WriteLinereferansı kaldırıldı; yerine yalnızcaputstanı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ıint32olarak 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,
RAXregister’ına konur - P/Invoke tanımı
voiddönüşlü yapılıncamainmetodunda dönüş değerini kaldırmak için gerekenpopkomutu 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::WriteLinereferans edilmediği içinmscorlibreferansı da kaldırıldı- Tables stream
#~içindeki bir tablo azaldı #Stringsakışındanmscorlibadı da kaldırıldı
- Tables stream
- .NET metadata directory’deki
VersionStringvarsayı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
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”
https://learn.microsoft.com/en-us/dotnet/core/extensions/cha...
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
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
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...
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
Şöyle çağırmak yeterli:
echo "hello world"Özellikle .NET Framework 4.7.2, desteklenen Windows sürümlerinde varsayılan olarak bulunuyor olmalı
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
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
Orada başka ne yapabilirsin ki?
.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
"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...
En çok küçültülmüş AOT derleme build'leri yakında 1MB eşiğini kıracak gibi görünüyor:
https://github.com/AustinWise/SmallestDotnetHelloWorlds
Tebrikler. Unicode desteğini bırakıp
putsiçin bir C çağrısı oluşturmuş oldunuzDoğrudan bir C derleyicisiyle derlemek daha kolay olmaz mıydı?
thats_just_c_with_extra_steps.jpgOberon+ IDE ile az önce denedim; bu modülden 2048 baytlık Hello.dll çıktı
module Hellobeginprintln("hello world")end Hellohttps://github.com/rochus-keller/Pelib assembly üreticisini kullanıyor
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
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
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