3 poin oleh GN⁺ 2023-07-15 | 3 komentar | Bagikan ke WhatsApp
  • Untuk membangun lebih banyak perangkat lunak C yang sudah ada dengan Cosmopolitan Libc, ternyata tidak cukup hanya "menipu" build system; compiler juga harus otomatis mentransformasikan kode bermasalah lewat patch GCC sekitar 2.000 baris
  • Hambatannya adalah nilai sistem seperti SIGTERM dan EINVAL bukan konstanta waktu kompilasi, sehingga bertabrakan dengan aturan label switch case dan inisialisasi static/const struct dalam C
  • Sudah dicoba mulai dari sed, regex Python, plugin GCC, sampai mencegat makro preprocessor, tetapi karena error label case terjadi saat parsing, plugin saja sulit untuk mencegahnya
  • Patch akhirnya diaktifkan dengan flag -fportcosmo, dan tepat sebelum GCC mengeluarkan error, switch diubah menjadi if/goto sementara inisialisasi struct dilengkapi dengan pengisian nilai saat runtime
  • Masih ada batasan seperti struct anonim, enum, sebagian const int, dan indeks array di dalam ekspresi inisialisasi, tetapi ini membuka jalan untuk membangun perangkat lunak seperti bash, curl, git, ninja, dan gcc tanpa mengubah source code

Tujuan patch GCC

  • Ditulis sekitar 2.000 baris patch gcc agar lebih mudah membuat Actually Portable Executables dengan Cosmopolitan Libc
  • Setelah patch diterapkan, software berikut bisa dibangun dengan build system ./configure atau cmake
    • bash
    • curl
    • git
    • ninja
    • gcc itu sendiri
  • File executable yang dihasilkan seharusnya bisa berjalan di Linux, FreeBSD, MacOS, OpenBSD, NetBSD, dan Windows
    • Ada catatan bahwa eksekusi di Windows belum diuji langsung
  • Binary hasil build di GitHub Actions bisa diunduh dari rilis superconfigure

Titik hambatan saat porting Cosmopolitan Libc

  • Setelah Cosmopolitan Libc menarik perhatian lewat web server redbean pada 2021, pertanyaan pentingnya menjadi seberapa baik perangkat lunak C yang sudah ada bisa berjalan di atas libc ini
  • Pola yang sama berulang saat mem-porting runtime bahasa seperti Lua, Wren, Janet, dan quickjs
  • Saat konstanta sistem seperti SIGTERM dan EINVAL dipakai pada label case dalam switch, hal ini berbenturan dengan aturan kompilasi C
  • Standar C mengharuskan label case berupa konstanta waktu kompilasi
    • Di Cosmopolitan Libc, makro preprocessor C yang terkait antarmuka sistem harus bersifat simbolik
    • Akibatnya, kode umum seperti switch(errno) { case EINVAL: ... } bisa gagal dikompilasi

Mengubah switch menjadi if dan goto

  • Jika label seperti case EINVAL atau case ENOSYS di dalam switch(errno) bukan konstanta waktu kompilasi, GCC akan mengeluarkan error case label does not reduce to an integer constant
  • Patch ini otomatis mentransformasikan pernyataan switch menjadi struktur berikut
    • Menambahkan percabangan untuk tiap nilai case dalam bentuk if (errno == EINVAL) goto caselabel_EINVAL;
    • Menambahkan goto untuk pindah ke label default
    • Blok case asli disusun ulang menjadi label dan goto endofthis_switch
  • Cara ini memang bukan yang paling elegan, tetapi berhasil menangani berbagai kasus yang ditemukan di banyak codebase
  • Karena juga harus menangani switch yang bercampur ifdef dan fallthrough, transformasi berbasis goto menjadi pilihan yang sederhana

Inisialisasi struct juga terkena batasan yang sama

  • Masalah konstanta waktu kompilasi juga muncul pada inisialisasi static atau const struct
  • Jika EINVAL bukan konstanta waktu kompilasi, sebagian inisialisasi berikut tidak valid dalam C
    • inisialisasi struct lokal biasa di dalam fungsi masih bisa
    • const struct
    • static struct
    • const struct global
    • static struct global
  • Salah satu contoh nyata ada di modul faulthandler milik CPython
  • Solusinya adalah menginisialisasi struct dengan nilai dummy lebih dulu, lalu menambahkan kode untuk mengisi nilai yang benar sebelum dipakai

Batasan substitusi teks dan plugin GCC

  • Awalnya transformasi ingin diotomatisasi dengan shell script sed, lalu dilanjutkan dengan script Python dan regex agar bisa menangani fallthrough
  • Namun, karena adanya preprocessor C dan ifdef, substitusi teks sulit bekerja secara lengkap
    • Sebab sulit mengetahui lebih dulu ifdef mana yang akan aktif saat kompilasi
  • Percobaan berikutnya adalah memakai transformasi AST melalui plugin GCC
    • Plugin GCC bisa dimuat dengan -fplugin=your-plugin.so
    • AST bisa diakses pada event seperti PLUGIN_PRE_GENERICIZE, PLUGIN_FINISH_PARSE_FUNCTION, dan PLUGIN_FINISH_DECL
    • AST dapat dicetak dengan debug_tree, dan ditelusuri dengan walk_tree_without_duplicates
  • Masalahnya, validasi label case dilakukan GCC saat parsing berlangsung
    • Plugin baru dapat mengakses AST setelah parsing selesai
    • Label salah seperti case SIGTERM: tidak tersisa di AST
    • Karena GCC sudah lebih dulu mengeluarkan error, plugin sulit memperbaikinya

Eksperimen mencegat makro preprocessor

  • Karena plugin saja sulit mencegah error sebelum parsing, dicoba pendekatan yang berinteraksi dengan preprocessor C
  • Melalui struktur cpp_reader di header plugin GCC, callback bisa dipasang saat makro di-#define, di-#undef, atau digunakan
  • Dengan memanfaatkan struktur makro nilai sistem di Cosmopolitan Libc, konstanta sementara bisa disisipkan
    • Contoh: extern const int SIGTERM;
    • Contoh: #define SIGTERM SYMBOLIC(SIGTERM)
    • Untuk plugin, disiapkan nilai sementara seperti static const int __tmpcosmo_SIGTERM = ...
  • Plugin mencegat penggunaan SYMBOLIC(SIGTERM) dan menggantinya dengan nilai sementara, lalu pada tahap PLUGIN_PRE_GENERICIZE AST diubah kembali menjadi VAR_DECL yang benar
  • Hack makro ini berhasil mengakali error pada inisialisasi struct dan switch``
  • Dengan pendekatan ini, build minimal CPython 3.11 juga menjadi mungkin

Mengapa memperbaiki GCC langsung alih-alih plugin

  • Setelah berdiskusi dengan Justine Tunney, dan jelas bahwa masalah ini bisa dipecahkan, diusulkan pendekatan yang lebih sederhana: memodifikasi codebase GCC secara langsung
  • Plugin dan hack makro menciptakan banyak pekerjaan yang sebenarnya tidak perlu, dan penanganan edge case maupun crash jadi rumit
  • Misalnya, walaupun yang perlu diubah hanya case ENOSYS, akibat substitusi makro plugin juga harus menangani penggunaan normal seperti ENOSYS + 1 atau printf("ENOSYS = %d", ENOSYS)
  • Pada pendekatan patch GCC, fitur ini diaktifkan dengan flag -fportcosmo alih-alih -fplugin terpisah
  • Tepat sebelum GCC mengeluarkan error case is not constant, compiler memeriksa apakah flag_portcosmo aktif; jika aktif, substitusi yang dibutuhkan dijalankan
  • Kode transformasi AST dari plugin sebelumnya dipindahkan ke dalam GCC agar dijalankan sebelum callback plugin lain dipanggil
  • Perubahan ini memungkinkan penghapusan kode hack terkait makro

Build nyata dan perluasan patch

  • Per 5 Juni 2023, GCC yang sudah ditambal lulus semua test case plugin sebelumnya, dan binary baru ditambahkan ke monorepo Cosmopolitan Libc
  • Seiring makin banyak kode yang dikompilasi, perbaikan dan penyempurnaan terus dilakukan
  • Build lua menjadi lebih sederhana, dan ninja juga bisa dibangun setelah menemukan lokasi munculnya error case constant di g++
  • Saat mencoba memasangkan ncurses ke Python 3.11, ditemukan masalah baru
    • Di ncurses, ada kode seperti DATA2(PARODD | PARENB, PARODD) yang memasukkan ekspresi biner ke dalam nilai inisialisasi struct
    • Patch sebelumnya bisa menangani inisialisasi konstanta sederhana, tetapi belum bisa menangani ekspresi seperti PARODD | PARENB
  • Setelah itu, patch diperluas agar label case dan elemen inisialisasi struct di C dapat berupa ekspresi arbitrer
    • Menurut standar C, label case tetap harus berupa konstanta waktu kompilasi
    • Compiler yang sudah ditambal akan memberi peringatan untuk penggunaan seperti ini, tetapi kompilasi tetap dilanjutkan
    • C++ dikecualikan dari substitusi ekspresi arbitrer karena tidak cocok dengan penanganan constexpr di g++11
  • Setelah perubahan ini, ncurses berhasil dibangun tanpa masalah

Batasan yang masih ada dan kesimpulan praktis

  • Patch ini belum sempurna
    • Belum bisa menangani sebagian struct anonim
    • Belum bisa menangani enum dan sebagian kasus const int
    • Belum bisa menangani kasus SIGTERM sebagai indeks array di dalam ekspresi inisialisasi
    • Masih ada kemungkinan crash compiler yang jarang pada inisialisasi static yang aneh atau saat SIGTERM dimasukkan ke static const int8_t
  • Meski begitu, banyak contoh tandingan yang jelas sudah berhasil dihilangkan, dan banyak software populer bisa dibangun dengan mulus
  • Lingkungan pengujian yang lebih ketat mungkin akan memperlihatkan area lain yang masih perlu diperbaiki
  • Jika mau, pernyataan switch dan ekspresi inisialisasi struct masih bisa diperbaiki secara manual, tetapi dalam banyak kasus compiler kini bisa menanganinya secara otomatis
  • Jika perangkat lunak C bisa dibangun secara statik, dan terutama jika juga bisa dibangun dengan musl, besar kemungkinan ia juga bisa dibangun dengan Cosmopolitan Libc

3 komentar

 
ryuheechul 2023-07-15

Sekarang neo memakai format yang membungkus pesan dengan blok kode? Agak disayangkan karena jadi harus scroll horizontal dan warna teks bernuansa merah juga sedikit kurang cocok...

 
xguru 2023-07-16

Aduh, ini terbungkus sebagai blok kode adalah bug T_T

 
GN⁺ 2023-07-15
Komentar Hacker News
  • Saya yang menulis artikel ini, dan judulnya seharusnya adalah “Patching GCC to build Actually Portable Executables”. Ini merujuk pada format Actually Portable Executable milik Cosmopolitan Libc dan jart.
    Dengan patch GCC saya, sekarang perangkat lunak seperti vim, emacs, ninja, bash, git, dan gcc bisa dibangun dengan Cosmopolitan Libc melalui sistem build bergaya autotools/cmake seperti biasa. File executable yang dihasilkan seharusnya bisa dijalankan di Linux, FreeBSD, MacOS, OpenBSD, NetBSD, dan Windows. Namun, Windows belum saya uji.
    Daftar perangkat lunak yang sudah saya coba build dengan teknik ini ada di sini: https://github.com/ahgamut/superconfigure
    Skrip superconfigure hanyalah pembungkus untuk skrip configure umum yang dipakai membangun perangkat lunak, dan menambahkan flag seperti --enable-static.
    Jika ingin mencoba membangun GCC dengan Cosmopolitan Libc, Anda bisa memakai repositori ini: https://github.com/ahgamut/musl-cross-make/tree/gccbuild

    • Saya penasaran apakah patch ini punya peluang masuk ke upstream GCC. APE executable dan cosmopolitan libc adalah teknologi yang sangat keren, jadi akan bagus kalau lebih mudah digunakan.
    • Bagus. Saya penasaran apakah ada potensi masalah saat ini dipakai di belakang layar oleh bahasa yang ditranspilasi seperti Nim.
    • Saya penasaran apakah Anda juga sudah mencoba build busybox.
    • Saya sempat melihat di lobste.rs bahwa Anda diblokir karena dianggap sockpuppet milik jart, dan baru sekarang sadar bahwa Anda memang orang yang berbeda.
      Semoga Anda mendapat perlakuan yang lebih baik di HN.
    • Fakta bahwa ini bisa di-build saja bukan pencapaian yang terlalu besar; yang lebih menarik adalah apakah ini benar-benar berjalan.
      Misalnya, saya penasaran apakah sudah ada pengujian yang cukup untuk membuktikan tidak ada regresi dibanding versi glibc.
  • Ada issue terkait ini di repositori Go: https://github.com/golang/go/issues/51900

  • Secara keseluruhan ini cukup keren, tapi saya penasaran dengan dasar teorinya. Jika membuat target sistem operasi baru, secara logis tampaknya lebih tepat untuk mendefinisikan konstanta baru.
    Untuk setiap sistem operasi turunan, alih-alih meneruskan konstanta milik OS itu apa adanya, Anda bisa mengubah konstanta tiap OS agar menjadi sama di mana-mana. Dengan begitu, kode yang memakai konstanta sebagai indeks array atau di case statement juga akan tetap bekerja.
    Mungkin ada overhead lebih besar dibanding pendekatan yang dijelaskan dalam artikel, tetapi tampaknya jauh lebih mudah dan lebih sederhana dipakai dari sudut pandang orang yang ingin mengompilasi program baru ke Cosmopolitan libc.

    • Membuat konstanta baru memang akan membuat kompilasi jauh lebih mudah, tetapi menurut saya overhead untuk mengonversi antara konstanta lama dan konstanta baru saat runtime terlalu besar.
      Daftar konstantanya bisa dilihat di sini: https://github.com/jart/cosmopolitan/blob/master/libc/sysv/c...
      Setiap kali salah satu konstanta ini dipakai, Anda harus memasukkan banyak konstanta ke dalam biner sebagai tabel lookup besar, lalu setiap pemeriksaan di program harus melewati tabel itu. Mungkin tidak terlalu lambat, tetapi saya rasa pasti akan terasa.
      Tujuannya adalah mempermudah porting tanpa banyak mengubah source code libc atau perangkat lunak yang ingin dipindahkan, sambil tetap menghasilkan biner dengan performa yang setara atau lebih baik. Dengan batasan seperti itu, patch GCC ini tampaknya cara terbaik untuk menyederhanakan prosesnya.
      Jika saya cukup sering menemukan codebase yang memakai SIGHUP untuk inisialisasi indeks array, saya akan mencoba pendekatan yang Anda usulkan untuk mengukur titik komprominya. Atau Anda juga bisa mencobanya sendiri dan memberi tahu saya apakah kumpulan konstanta terpisah memang lebih baik.
  • Jika “cukup banyak waktu telah dihabiskan untuk menghilangkan contoh tandingan yang jelas, dan banyak perangkat lunak populer bisa di-build dengan mulus”, akan membantu jika ada daftar perangkat lunak yang berhasil dikompilasi. Mungkin sebenarnya sudah ada.

    • Perangkat lunak seperti vim, emacs, ninja, bash, git, dan gcc bisa di-build. Daftar yang sudah saya coba bangun dengan teknik ini ada di sini: https://github.com/ahgamut/superconfigure
      Skrip superconfigure hanyalah pembungkus untuk skrip configure umum yang dipakai membangun perangkat lunak, dan menambahkan flag seperti --enable-static.
  • Bertanya-tanya apakah APE punya tujuan praktis. Apakah ada pihak yang benar-benar mendistribusikan satu berkas executable yang harus berjalan di banyak sistem operasi?

    • Di Logfile Navigator(https://lnav.org), executable APE digunakan sebagai agen yang berkomunikasi dengan host jarak jauh
      lnav sendiri tidak dibangun sebagai APE, tetapi agen tertanam di dalamnya dibangun sebagai APE. Saat pengguna ingin membaca log dari host tersebut, agen itu dikirim ke host jarak jauh
      Dengan begitu tidak perlu langkah tambahan untuk mendeteksi jenis sistem operasi atau menyertakan beberapa versi executable. Ada tulisan singkat terkait di sini: https://lnav.org/2021/05/03/tailing-remote-files.html
    • Terbayang programmer Java/Clojure/Kotlin, programmer .Net, JS, Python, Ruby, Erlang, dan semua programmer bahasa berbasis mesin virtual
      Program yang dikompilasi statis memang menjadi fondasi semuanya, tetapi di level aplikasi jumlahnya minoritas
      APE mengajukan pertanyaan besar: “Jika bahasa AOT tingkat rendah seperti C bisa dikompilasi sekali lalu dijalankan di mana saja, siapa yang masih butuh mesin virtual?”
    • Misalnya, ini tampaknya bisa sedikit mempermudah distribusi executable di tempat yang repot menaruh banyak artefak untuk kombinasi id/version yang sama, seperti repositori maven. PyPI sepertinya punya masalah serupa
      Selain itu, cross-compilation itu menyakitkan. Proyek open source kecil yang tidak punya build farm dengan semua BSD sering kali sama sekali tidak mendistribusikan biner untuk platform seperti itu
      Bahkan kubectl punya biner untuk Linux, tetapi tidak untuk BSD. Kecuali jika OSX dihitung sebagai BSD. Jadi untuk proyek seperti itu, ini terlihat seperti keuntungan yang mudah didapat
    • Ke depan saya ingin merilis side project dengan cara seperti itu. Saya juga membangun executable Redbean untuk SPA yang hanya sisi klien. Gagasan bahwa hal-hal seperti ini bisa langsung berjalan di berbagai sistem terasa menakjubkan
    • Ada kompiler shader yang akan terbantu jika ada format executable yang bisa langsung dijalankan tanpa terikat platform
      Terlalu besar untuk dibangun secara ad hoc di mesin pengguna. Ada banyak dependensi C++ pihak ketiga yang besar, dan waktu build berkisar dari 2 hingga 20 menit tergantung perangkat keras
      Harus berjalan di macOS x86-64+ARM, Linux x86-64, dan Windows x86-64. Akan bagus jika ARM Linux juga didukung
      Saat ini sedang mempertimbangkan WASM, tetapi itu memerlukan runtime WASI yang terpasang
  • Untuk yang bingung: “Actually Portable Executables” https://justine.lol/ape.html

    • Saya sudah menambahkan komentar yang menyebutkan ini, tetapi belum berubah. Sepertinya akan segera berubah. Apakah pantas memanggil @dang di sini?
  • Mengganti switch menjadi if terasa cukup kasar. Sepertinya ini bisa diperbaiki dengan memetakan ke himpunan konstanta netral platform, lalu melakukan dereference simbol runtime sebagai fungsi
    Misalnya ditulis seperti switch(CosmoMappErrno(EINVAL)) dan case COSMO_EINVAL: sehingga kode tidak perlu diubah menjadi gumpalan goto

    • Tapi itu tidak membantu untuk mengompilasi kode yang tidak dimodifikasi. Goto yang dihasilkan di dalam compiler sendiri tidak perlu dikhawatirkan. Compiler memang melakukan itu di mana-mana
      Tentu, lain ceritanya jika yang dimaksud adalah compiler mengenali switch seperti ini dan, alih-alih menuliskannya ulang menjadi pohon if, mengubah label menjadi konstanta khusus cosmo untuk tiap nilai, lalu membungkus nilai input switch dengan pemanggilan fungsi yang memetakan nilai platform runtime saat ini ke konstanta khusus cosmo yang sesuai. Nilai lain dibiarkan lewat tanpa perubahan
      Itu malah bisa jadi transformasi compiler yang lebih sederhana. Hanya saja muncul kompleksitas untuk mengenali himpunan konstanta mana yang sedang dipakai, lalu menerapkan pemanggilan pemetaan yang tepat pada input switch. Di sisi library juga perlu membuat pemetaan seperti itu, jadi kompleksitasnya memang ada
      Sudah ada implementasi yang bekerja dengan metode transformasi lain, jadi saya tidak tahu apakah penulisnya ingin mengimplementasikan ini
      Terakhir, pendekatan pemetaan seperti ini berisiko salah memetakan, dan juga ada kerumitan seperti harus membalik input sebelum pemetaan untuk kode yang memakai nilai errno negatif
    • Cara melakukan dereference simbol runtime sebagai fungsi terdengar menarik. Kalau ada contoh yang bekerja, saya akan dengan senang hati mencobanya
      Alasan saya menyukai array if-else-goto adalah karena itu sangat pas dengan bagian lain di GCC. Jika melihat patch saya[1], Anda bisa melihat bahwa untuk menambahkan fitur ini saya hanya mengubah sangat sedikit kode yang sudah ada di GCC
      [1]: https://github.com/ahgamut/gcc/tree/portcosmo-11.2
    • Apakah memang perlu sampai memikirkan apakah penggantiannya dilakukan secara otomatis?
  • Saya tidak mengerti kenapa orang begitu terkejut dengan kode yang memakai SIGxxx sebagai indeks. Nomor sinyal itu terdefinisi dengan baik, umumnya berurutan, dan diketahui mulai dari 1

    • Sebelum mencoba busybox, saya sudah membangun banyak codebase seperti git, bash, gcc, dan curl, tetapi belum pernah melihat kode seperti itu
      Selain itu, error itu muncul bersama error compiler dari patch saya yang butuh waktu lama untuk saya debug, jadi itu menjadi kejutan yang sangat membekas
      Saya penasaran apakah pola kode seperti itu dianggap umum. Saya juga ingin tahu codebase lain mana yang memilikinya selain busybox. Kalau contohnya memang banyak, mungkin saya bisa meluangkan waktu untuk memperbarui patch agar menangani pola seperti itu
  • Kalau errno memang masalah sebesar itu, saya tidak tahu bagaimana harapan untuk menjalankan pustaka standar C++ dan std::error_code
    Di sana tidak ada cara untuk memakai makro ajaib seperti itu

    • Sepertinya masalahnya justru pada makro ajaib itu. Biasanya makro mengembang menjadi konstanta, tetapi sekarang tidak lagi, jadi sebagian hal menjadi rusak
      std::error_code bukan konstanta, jadi sepertinya tidak akan mengalami masalah yang sama