1 poin oleh GN⁺ 2023-07-10 | 1 komentar | Bagikan ke WhatsApp

-.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 ke ucrtbase!puts agar stream UTF-16 #US bisa 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 mscorlib sebagai 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 kelas Program
  • 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 main di tipe <Module> dan hanya berisi body CIL dengan Ldstr "Hello World", Console.WriteLine, dan Ret
  • 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, .text dan .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 0x200 byte, sehingga ukuran file membengkak menjadi minimal 1KB
  • File executable .NET 64-bit tidak memerlukan non-managed entry point, sehingga dua directory itu dan section .reloc bisa dihapus dengan pengaturan berikut
    • module.PEKind = OptionalHeaderMagic.PE64
    • module.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 #Strings adalah 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 null dan program tetap berjalan normal; langkah ini menghemat 32 byte
  • Stream #GUID berisi GUID yang dirujuk Mvid pada Module table, tetapi pada eksperimen ini identifikasi versi tidak dibutuhkan sehingga dihapus
    • Mvid sepanjang 16 byte dihapus
    • Seluruh stream #GUID yang kosong beserta header 16 byte juga dihapus
    • Total penghematan 32 byte
  • mscorlib yang dirujuk untuk memanggil Console::WriteLine biasanya 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 #US milik .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 0xFF sehingga bisa direpresentasikan sebagai ASCII 1 byte, tetapi tidak ada cara untuk membuat CLR menafsirkan string dari stream #US sebagai ASCII
  • Sebagai pengganti Console::WriteLine, dipakai pemanggilan P/Invoke ke fungsi unmanaged ucrtbase!puts
    • Bentuk deklarasi C#-nya adalah [DllImport("ucrtbase")] static extern int puts(nint str);
    • puts menerima pointer ke string ASCII yang diakhiri zero sebagai argumen
  • Agar alamat runtime string diketahui saat kompilasi, flag DynamicBase pada DllCharacteristics di 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 referensi System.Console::WriteLine, lalu menggantinya dengan definisi puts dan metadata P/Invoke
  • Hasilnya ukuran turun menjadi 889 byte (0x379)

Mikro-optimasi terakhir

  • Definisi standar puts mengembalikan jumlah karakter yang dicetak sebagai int32, 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, instruksi pop untuk membuang nilai balik di metode main bisa 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::WriteLine tidak lagi dirujuk, referensi mscorlib juga dihapus
    • Satu tabel di tables stream #~ berkurang
    • Nama mscorlib juga hilang dari stream #Strings
  • VersionString pada .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

 
GN⁺ 2023-07-10
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"

    • Menyambung soal C# yang "Go-inspired", .NET punya System.Threading.Channels
      https://learn.microsoft.com/en-us/dotnet/core/extensions/cha...
    • Jika aplikasi C# bisa menjadi cukup kecil sampai pengguna merasa waktu unduhnya seketika, saya penasaran apakah ini bisa dipakai untuk membuat C# WebAssembly lebih realistis
      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
    • Berkat Michal dan tim CoreRT, saya jadi melirik C# lagi, dan kalau NativeAOT tidak ada saat ini, rasanya saya tidak akan pernah mempertimbangkan C#, apalagi dengan adanya Go
  • 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

    • Tidak juga. exe hasil akhirnya pada praktiknya hampir tidak memakai .NET Framework selain untuk memanggil main
      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 biner native yang ditautkan statis penuh dengan biner yang memuat sebagian atau seluruh library secara dinamis juga sangat tidak adil
      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
    • hello world saya memerlukan framework "bash" dan ukurannya 0 byte
      Cukup panggil seperti ini: echo "hello world"
    • .NET Framework sudah tertanam di Windows, dan setidaknya sejak Vista merupakan komponen wajib
      Secara spesifik, .NET Framework 4.7.2 kemungkinan sudah disertakan secara default pada versi Windows yang didukung
    • Saya tidak yakin apakah yang seharusnya kita pedulikan itu "keadilan" atau "realitas"
      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

    • Saya pernah ke McMurdo dalam perjalanan riset
      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
    • Dalam kasus ini, saya tidak paham kalimat "menghabiskan hari Sabtu dengan bodoh"
      Memangnya ada hal lain yang bisa dilakukan di sana?
    • Layak melihat https://brr.fyi/
    • Penasaran bagaimana cara menemukannya
      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

    • exe hasilnya hanya bergantung pada detail implementasi pustaka ucrt.dll tingkat bawah yang diekspos melalui panggilan pinvoke unmanaged, dan tidak benar-benar "menggunakan" framework tersebut
    • Pengecualian mungkin ada jika Anda membuat aplikasi yang bergantung pada API Office/COM
  • 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...

  • Selamat. Anda pada dasarnya membuang dukungan Unicode dan membuat pemanggilan C ke puts
    Bukankah akan lebih mudah kalau langsung dikompilasi dengan compiler C saja?
    thats_just_c_with_extra_steps.jpg

    • Agak antiklimaks kalau pada dasarnya malah melewati proses kompilasi
  • Baru saja saya coba dengan Oberon+ IDE, dan modul ini menghasilkan Hello.dll 2048 byte
    module Hello
    begin
    println("hello world")
    end Hello
    Menggunakan generator assembly https://github.com/rochus-keller/Pelib

    • Nicklaus Wirth memang benar
      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
    • Kalau Oberon+, rasanya seharusnya self-hosting, bukan?
      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
    • Penasaran apakah akhir filenya diisi dengan byte 0
      Sepertinya ada opsi untuk menonaktifkan itu
  • Fakta bahwa file .exe bisa diperkecil hingga di bawah 1KB benar-benar membuka wawasan
    Kalau ada yang bisa mengecilkan file .exe Hello World dari Flutter sampai 1MB, itu bakal benar-benar luar biasa

    • Langkah pertama sepertinya adalah meyakinkan Microsoft agar menyertakan Flutter di Windows
    • Saya penasaran apakah cukup diakui kalau hanya mengompilasi "Hello world" dengan Dart