Seberapa kecil file biner .NET Hello World minimal bisa dibuat?
(blog.washi.dev)-.Untuk file eksekusi yang hanya mencetak "Hello World" di .NET Framework 4.x, hasil kompilasi C# biasa diperkecil dari 4.6KB menjadi 834 byte dengan menyusun PE secara manual
- Eksperimen ini memakai syarat: harus menjalankan managed entry point, berupa satu file executable Windows, hanya memakai BCL atau library yang umum terpasang di Windows, dan 0 byte di akhir file tidak dihitung
- Pengurangan ukuran dilakukan dengan membuang anotasi nullable reference type dan metadata yang tidak perlu, lalu beralih ke PE 64-bit untuk menghapus imports dan base relocations
- Alih-alih
Console.WriteLine, dipakai P/Invoke keucrtbase!putsagar stream UTF-16#USbisa dihapus, dan string ASCII dipindahkan ke area padding di antara header PE dan section pertama - File eksekusi akhir menjadi 834B (0x342) tanpa trailing zero, dan hanya 476B saat dikompresi ZIP, menunjukkan batas minimal susunan PE .NET dan metadata CLR
Kondisi eksperimen dan baseline
- Tujuannya adalah memasukkan byte sesedikit mungkin ke dalam file executable .NET agar CLR tetap bisa mencetak
"Hello World"ke standard output - Aturan eksperimennya sebagai berikut
- Aplikasi harus menjalankan managed entry point yang diimplementasikan dengan C# atau CIL
- Runtime target adalah .NET Framework 4.x.x
- Dependensi pihak ketiga dilarang; hanya
mscorlibsebagai BCL atau library yang umum terpasang di Windows yang diperbolehkan - Karena batas alignment pada file PE dan CLR, file .NET PE minimum secara teoretis yang bisa dijalankan di Windows 10 ke atas tidak bisa lebih kecil dari 1KB, tetapi trailing zero bytes di akhir file tidak dihitung dalam ukuran
- Jika program Hello World C# biasa dibangun dengan
net472, C# 10, dan nullable enabled, ukuran file executable menjadi 4.6KB - Kode sumber akhirnya tersedia di Full Source Code
Bagian yang bisa dipangkas dari hasil kompilasi C#
- Nullable reference type di C# 8.0 disertakan secara statis dalam executable dalam bentuk custom attribute, dan atribut ini sangat menambah ukuran
- Jika pemeriksaan nullable reference type dimatikan lewat
<Nullable>disable</Nullable>di.csproj, atributnya memang hilang, tetapi karena alignment file PE ukuran total tetap 4.6KB - Hasil dekompilasi menunjukkan compiler C# masih terus menghasilkan metadata yang tidak perlu meski nullable dimatikan
- Assembly attributes seperti versi file assembly dan informasi hak cipta
- Tipe tersembunyi
<Module>selain kelasProgram
- Daripada terus mencari pengaturan compiler C# untuk pemangkasan ekstrem, diputuskan untuk membuat file executable .NET secara langsung dengan AsmResolver
- Modul buatan tangan ini menaruh fungsi
maindi tipe<Module>dan hanya berisi body CIL denganLdstr "Hello World",Console.WriteLine, danRet - Dengan cara ini ukuran file turun menjadi sekitar setengah dari sebelumnya
Menghapus imports dan base relocations dengan PE 64-bit
- File executable yang dihasilkan memiliki dua section,
.textdan.reloc, dan data directory Imports serta Base Relocations memakan ruang besar - File executable .NET AnyCPU atau 32-bit membutuhkan non-managed entry point yang memanggil
mscoree!_CorExeMain, sehingga imports directory diperlukan - Karena file executable .NET default bersifat relocatable, base relocation untuk memanggil imported function ini juga ikut diperlukan
- Secara default base relocation ditempatkan di section terpisah, dan setiap section harus di-align minimal
0x200byte, sehingga ukuran file membengkak menjadi minimal 1KB - File executable .NET 64-bit tidak memerlukan non-managed entry point, sehingga dua directory itu dan section
.relocbisa dihapus dengan pengaturan berikutmodule.PEKind = OptionalHeaderMagic.PE64module.MachineType = MachineType.Amd64
- Pada tahap ini tercapai ukuran minimum teoretis file .NET PE yang valid, yaitu 1KB
Menghapus nama metadata, GUID, dan public key token
- Setelah ini pengukuran ukuran tidak lagi memakai ukuran file dari Windows Explorer, tetapi posisi setelah non-zero byte terakhir di hex editor
- Dengan ukuran berdasarkan kriteria ini, file PE 1KB tadi sebenarnya sudah menyusut menjadi 991 byte (0x3DF)
- Heap
#Stringsadalah stream metadata terbesar kedua dalam biner .NET, dan menyimpan nama tipe serta metode yang dipakai table stream#~ - Nama-nama yang tidak penting saat runtime bisa diatur menjadi
nulldan program tetap berjalan normal; langkah ini menghemat 32 byte - Stream
#GUIDberisi GUID yang dirujukMvidpada Module table, tetapi pada eksperimen ini identifikasi versi tidak dibutuhkan sehingga dihapusMvidsepanjang 16 byte dihapus- Seluruh stream
#GUIDyang kosong beserta header 16 byte juga dihapus - Total penghematan 32 byte
mscorlibyang dirujuk untuk memanggilConsole::WriteLinebiasanya menyertakan public key token 8 byte- Jika public key token dihapus, CLR tidak memverifikasi keaslian assembly token tersebut, dan dalam eksperimen ini token itu dibuang
- Setelah tahap ini ukuran file menjadi 918 byte
Menghapus Console.WriteLine dan memakai string ASCII
"Hello World"disimpan di stream metadata#USmilik .NET, direpresentasikan sebagai array karakter 16-bit dengan prefix panjang dan tambahan 0 byte- Semua karakter dalam string yang dicetak memiliki code point di bawah
0xFFsehingga bisa direpresentasikan sebagai ASCII 1 byte, tetapi tidak ada cara untuk membuat CLR menafsirkan string dari stream#USsebagai ASCII - Sebagai pengganti
Console::WriteLine, dipakai pemanggilan P/Invoke ke fungsi unmanageducrtbase!puts- Bentuk deklarasi C#-nya adalah
[DllImport("ucrtbase")] static extern int puts(nint str); putsmenerima pointer ke string ASCII yang diakhiri zero sebagai argumen
- Bentuk deklarasi C#-nya adalah
- Agar alamat runtime string diketahui saat kompilasi, flag DynamicBase pada
DllCharacteristicsdi PE optional header dimatikan- Image base dibuat tetap
- String ASCII ditempatkan di dalam section
.text - Alamat runtime dihitung sebagai
module_base_address + rva_ascii_string
- Agar CLR menghormati flag ini, flag ILOnly pada .NET data directory juga dimatikan
- Perubahan ini menghapus string wide-character, stream
#US, dan referensiSystem.Console::WriteLine, lalu menggantinya dengan definisiputsdan metadata P/Invoke - Hasilnya ukuran turun menjadi 889 byte (0x379)
Mikro-optimasi terakhir
- Definisi standar
putsmengembalikan jumlah karakter yang dicetak sebagaiint32, tetapi program tidak memakai nilai itu - Pada PE 64-bit, sesuai x64 calling convention Microsoft, nilai balik masuk ke register
RAX, bukan ke evaluation stack .NET - Jika definisi P/Invoke diubah menjadi return
void, instruksipopuntuk membuang nilai balik di metodemainbisa dihapus - Karena ukuran header PE lebih kecil dari
0x200, muncul padding data di antara header dan section pertama - Windows PE Loader memetakan header PE ke memori yang bisa dibaca, dan area ini juga mencakup padding data tersebut
- Dengan memindahkan string ASCII ke area padding ini, ukuran file bisa dipangkas lagi 13 byte
- Karena
Console::WriteLinetidak lagi dirujuk, referensimscorlibjuga dihapus- Satu tabel di tables stream
#~berkurang - Nama
mscorlibjuga hilang dari stream#Strings
- Satu tabel di tables stream
VersionStringpada .NET metadata directory secara default adalah"v4.0.30319", dipadding ke kelipatan 4 byte sehingga totalnya 12 byte- String ini bisa dipendekkan menjadi
v4.0.dan CLR 4.0 tetap bisa boot serta menjalankan program, sehingga setelah padding ada penghematan tambahan 4 byte - Titik
.di akhir ternyata diperlukan; jika dipangkas lebih jauh program tidak bisa boot dengan benar
Hasil akhir
- Ukuran biner final adalah 834 byte (0x342) tanpa trailing zero
- Hasil kompresi ZIP menjadi 476 byte, lebih kecil daripada 582 byte saat optimasi tambahan setelah batas 1KB belum dilakukan
- Eksperimen ini dimulai dari Hello World 4.6KB hasil compiler C#, lalu diperkecil hingga file PE rakitan manual sebesar 834B
- Program akhirnya tetap bisa mencetak
"Hello World"dengan benar - Pekerjaan ini lebih merupakan eksperimen untuk mengeksplorasi batas minimum susunan format file modul .NET dan metadata CLR daripada sesuatu yang praktis
1 komentar
Komentar Hacker News
Saya sangat suka eksperimen jenis program minimal yang bisa dijalankan seperti ini
Michal Strehovský pernah membuat game Snake berukuran kurang dari 8KB tanpa dependensi runtime .NET, dengan menggunakan framework yang sekarang dikenal sebagai .NET
Medium kurang bagus dan Twitter sekarang juga tidak terlalu berguna, tetapi kalau tidak salah saya ingat kira-kira "hello world"-nya bahkan mendukung Windows 3.11
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
Pekerjaan terbaru dan dukungan bisa dilihat di https://flattened.net: "bflat - C# as you know it but with Go-inspired tooling"
https://learn.microsoft.com/en-us/dotnet/core/extensions/cha...
Seingat saya, porsi besar dari ukuran unduhan adalah library .NET itu sendiri
Saya juga penasaran apakah ada framework/engine game 2D yang menargetkan browser, memakai C#, dan bisa menghasilkan biner kecil
Artikel ini bisa menyesatkan. File executable tersebut membutuhkan runtime yang terpasang di sistem, dan sebenarnya memakai .NET Framework 4.7.2 pada 2023
Untuk perbandingan yang adil dengan pendekatan seperti ini, mungkin lebih tepat membandingkannya dengan file .jar atau .py
Sebagai tolok ukur executable sungguhan, biner Native AOT yang dapat berjalan di Windows, Linux, dan macOS lebih cocok
Namun wajar jika ukurannya jauh lebih besar karena harus menyertakan juga kode pendukung seperti GC, ThreadPool, dan Console
Logika untuk menulis byte ke konsol ditangani melalui pemanggilan pinvoke ke kode unmanaged yang diekspos oleh ucrt.dll
Artikel minimisasi Snake yang dibagikan di komentar lain melakukan hal yang sama di atas .NET Core, yaitu .NET baru yang tidak dibundel dengan Windows, dan benar-benar menghasilkan biner non-.NET
[0]: https://medium.com/@MStrehovsky/building-a-self-contained-ga...
Membandingkan image container tampaknya titik awal yang bagus. Gunakan base image sekecil mungkin, atau kalau bisa pakai "FROM scratch", tetapi hanya bandingkan antararsitektur yang sama
Untuk menghilangkan variabel seperti menjalankan 32-bit di atas 64-bit atau kompresi pointer, saya lebih memilih 32-bit
Setelah itu, akan bagus jika membandingkan ukuran export tar yang tidak dikompresi. Itu memang belum sepenuhnya adil, tetapi setidaknya bisa mengurangi variabel yang terlalu jelas
Kalau dipikir-pikir lagi, mungkin lebih adil membandingkan ukuran image yang dikompresi semaksimal mungkin untuk ikut mencerminkan kompresi di dalam container. Tentu saja harus memakai algoritma dan parameter yang sama, atau seperti pada benchmark kompresi, ukuran dekompresornya harus ditambahkan ke hasil akhir
Cukup panggil seperti ini:
echo "hello world"Secara spesifik, .NET Framework 4.7.2 kemungkinan sudah disertakan secara default pada versi Windows yang didukung
Instalasi Windows modern praktis tidak mungkin tidak memiliki .NET, sedangkan Linux dan macOS memang tidak menyediakannya secara default, jadi terasa aneh jika itu dijadikan standar keadilan
Tambahan lagi, penasaran apakah ada yang memperhatikan bahwa lokasi penulis adalah McMurdo Station, Antarktika
Tidak menyangka ada orang dengan stack teknologi seperti ini di pangkalan itu: https://usscar.org/directory?combine=&field_usscar_nsf_progr...
Ini bisa jadi cerita yang cukup menarik
Hal yang biasanya mengejutkan orang luar adalah bahwa sebagian besar orang di sana adalah personel pendukung, dan ilmuwan hanya sebagian kecil, mungkin sekitar 20%
Personel pendukung datang dari segala macam profesi yang dibutuhkan untuk menjalankan kota modern, seperti pemadam kebakaran, tukang kayu, juru masak, mekanik, dan lain-lain
Mungkin karena proses seleksinya, hampir semuanya sangat hebat dan termasuk yang terbaik di bidangnya. Ada sangat banyak talenta tipe pisau Swiss Army dengan keterampilan luas, dalam jumlah yang mengejutkan
Kesulitan unik dalam hidup dan bekerja di sana menarik orang-orang yang unik dan sangat termotivasi seperti serangga ke cahaya, dan banyak yang kembali setiap tahun. Komunitas yang luar biasa
Memangnya ada hal lain yang bisa dilakukan di sana?
Tidak terlihat di metadata .NET maupun about
Disebutkan bahwa semua section harus disejajarkan ke nilai alignment section minimum yang memungkinkan, yaitu 0x200 byte, jadi file membengkak setidaknya sebesar itu, tetapi 0x200 adalah 512 byte, yaitu 0,5KB
Saya pernah melakukan optimasi ukuran PE pada tingkat seperti ini sejak lama, dan setahu saya Windows 9x menetapkan minimum 512, sedangkan NT bisa menerima alignment sekecil 1
Saya juga tidak melihat trik lama yang dulu merupakan teknik mutakhir, yaitu menumpangtindihkan header DOS MZ dan PE: http://www.phreedom.org/research/tinype/
Dan ini membuat saya sadar bahwa di demoscene, orang-orang berhasil melakukan ini di dalam biner 1KB:
https://www.pouet.net/prodlist.php?type%5B%5D=1k&platform%5B...
Ini sama sekali bukan pemborosan waktu
Jika menggali detail biner, Anda akan mendapat wawasan mendalam tentang bagaimana sesuatu dibangun, dan konsep yang dipelajari dari sana akan memperkaya pengetahuan Anda
Jika itu menarik dan Anda belajar sesuatu, selalu layak menginvestasikan waktu dan usaha
Yang ini jauh lebih relevan dan menarik: https://twitter.com/MStrehovsky/status/1669502394827419648
Isinya menyebut bahwa "C# Hello World hasil kompilasi native yang sepenuhnya self-contained, termasuk GC dan semuanya, bisa diperkecil hingga sekitar 440KB"
.NET classic framework adalah warisan lama Windows yang sudah tidak lagi direkomendasikan, dan aplikasi baru seharusnya tidak dibangun di atasnya
Saya berharap ini membahas biner "sungguhan", yaitu executable .NET yang dikompilasi native seperti .NET AOT
https://learn.microsoft.com/en-us/dotnet/core/deploying/nati...
Build hasil kompilasi AOT yang paling banyak diperkecil tampaknya akan segera menembus batas 1MB:
https://github.com/AustinWise/SmallestDotnetHelloWorlds
Selamat. Anda pada dasarnya membuang dukungan Unicode dan membuat pemanggilan C ke
putsBukankah akan lebih mudah kalau langsung dikompilasi dengan compiler C saja?
thats_just_c_with_extra_steps.jpgBaru saja saya coba dengan Oberon+ IDE, dan modul ini menghasilkan Hello.dll 2048 byte
module Hellobeginprintln("hello world")end HelloMenggunakan generator assembly https://github.com/rochus-keller/Pelib
Saya merasa begitu saat pertama kali memakai Lazarus/FreePascal, dan semakin banyak saya membaca karyanya, semakin terasa bahwa dia benar
Belakangan saya berpikir WASM mungkin bisa menjadi jalan keluar dari "siklus reinkarnasi" cara berpikir seperti ini, tetapi saya tidak bisa begitu saja menyerah pada hardware
Saya terlalu primitif untuk menerima mesin spiritual yang tidak bisa saya hancurkan dengan palu di atas meja
Komputer global yang tak bisa dihancurkan seperti Ethereum, yang berjalan dengan menghabiskan ribuan dolar waktu komputasi di hardware milik orang lain dan lebih lambat daripada RPi, terasa seperti sesuatu yang tabu bagi saya dalam cara yang sangat mendasar
Saya masih ingin membuat kaus bertuliskan "Wirth was right" dengan wajah Wirth di atasnya
Ada terlalu banyak sisa-sisa ekosistem C++
Ini mengingatkan saya pada proyek forge hosting perangkat lunak open source yang dipromosikan sebagai alternatif GitHub dan GitLab. Tetapi saat benar-benar pergi untuk clone kode atau melaporkan bug, semuanya ternyata ada di GitHub
Sepertinya ada opsi untuk menonaktifkan itu
Fakta bahwa file
.exebisa diperkecil hingga di bawah 1KB benar-benar membuka wawasanKalau ada yang bisa mengecilkan file
.exeHello World dari Flutter sampai 1MB, itu bakal benar-benar luar biasa